Created
February 4, 2020 13:02
-
-
Save mdiep/fa69bd35339974d4d4e7b57009a9d0a1 to your computer and use it in GitHub Desktop.
Diff values with Mirror and AnyHashable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// Diff values for better test assertions. | |
// | |
// Enums and collections left as an exercise for the reader. | |
// A difference between two values | |
struct Difference: CustomStringConvertible { | |
let path: String | |
let actual: String | |
let expected: String | |
init(path: String, actual: String, expected: String) { | |
self.path = path | |
self.actual = actual | |
self.expected = expected | |
} | |
init<V>(actual: V, expected: V) { | |
path = "" | |
self.actual = "\(actual)" | |
self.expected = "\(expected)" | |
} | |
// Add a key to the front of the key path | |
func prefix(_ key: String) -> Difference { | |
return Difference( | |
path: path == "" ? key : "\(key).\(path)", | |
actual: actual, | |
expected: expected | |
) | |
} | |
var description: String { | |
""" | |
\(path) doesn't match: | |
\tActual: | |
\t\t\(actual) | |
\tExpected: | |
\t\t\(expected) | |
""" | |
} | |
} | |
func differences<V>(_ a: V, _ b: V) -> [Difference] { | |
guard | |
let aHashable = a as? AnyHashable, | |
let bHashable = b as? AnyHashable | |
else { | |
fatalError("\(V.self) must conform to Hashable") | |
} | |
let areEqual = aHashable == bHashable | |
let mirrorA = Mirror(reflecting: a) | |
let mirrorB = Mirror(reflecting: b) | |
// If there are no children, then must be a primitive value. | |
// Compare in directly. | |
guard !mirrorA.children.isEmpty else { | |
return areEqual | |
? [] | |
: [ Difference(actual: a, expected: b) ] | |
} | |
let diffs = zip(mirrorA.children, mirrorB.children) | |
.flatMap { pair -> [Difference] in | |
let (a, b) = pair | |
return differences(a.value, b.value) | |
.map { $0.prefix(a.label!) } | |
} | |
// Make sure no mistakes were made | |
precondition(diffs.isEmpty == areEqual) | |
return diffs | |
} | |
func diff<V>(_ a: V, _ b: V) -> String? { | |
let diffs = differences(a, b) | |
if diffs.isEmpty { return nil } | |
return diffs | |
.map { "\($0)" } | |
.joined(separator: "\n") | |
} | |
struct M: Hashable { | |
struct Foo: Hashable { | |
var baz: String | |
} | |
let id: Int | |
let foo: Foo | |
} | |
let a = M(id: 1, foo: M.Foo(baz: "bar")) | |
let b = M(id: 2, foo: M.Foo(baz: "baz")) | |
print(diff(a, b) ?? "==") | |
// Prints: | |
// | |
// id doesn't match: | |
// Actual: | |
// 1 | |
// Expected: | |
// 2 | |
// foo.baz doesn't match: | |
// Actual: | |
// bar | |
// Expected: | |
// baz |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment