Skip to content

Instantly share code, notes, and snippets.

@mdiep
Last active February 13, 2024 21:01
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdiep/068e223f5ae59782a44ba4f2d611ba23 to your computer and use it in GitHub Desktop.
Save mdiep/068e223f5ae59782a44ba4f2d611ba23 to your computer and use it in GitHub Desktop.
Diff values with Codable, CaseIterable, and KeyPaths
import Foundation
// Diff objects for better test assertions.
//
// Implemented in a way that:
// 1. The compiler generates as much code as possible
// 2. You'll get a compiler error if you forget a property
//
// Nested support and collections left as an exercise for the reader.
// A way to diff an individual property
struct Property<Value> {
let diff: (Value, Value) -> String?
init<T: Equatable>(name: String, keyPath: KeyPath<Value, T>) {
diff = { a, b in
let aValue = a[keyPath: keyPath]
let bValue = b[keyPath: keyPath]
if aValue == bValue {
return nil
}
return """
• .\(name) doesn't match
\tActual:
\t\t\(aValue)
\tExpected:
\t\t\(bValue)
"""
}
}
}
struct M: Equatable {
let id: Int
let foo: String
}
// Conform to `Codable` so `CodingKeys` will have a case for each property
extension M: Codable {
// Implement CodingKeys to conform to CaseIterable
private enum CodingKeys: String, CodingKey, CaseIterable {
case id
case foo
}
private static func property(for key: CodingKeys) -> Property<M> {
let name = key.rawValue
// Switch over the CodingKey so you can't forget a property
switch key {
case .id:
return Property(name: name, keyPath: \.id)
case .foo:
return Property(name: name, keyPath: \.foo)
}
}
static func diff(_ a: M, _ b: M) -> String? {
let reasons = CodingKeys
.allCases
.map(property(for:))
.compactMap { $0.diff(a, b) }
.map { reason in
"\t" + reason.replacingOccurrences(of: "\n", with: "\n\t")
}
let isEqual = reasons.isEmpty
// Make sure no mistakes were made
precondition(isEqual == (a == b))
if isEqual { return nil }
return """
`M` values don't match:
\(reasons.joined(separator: "\n"))
"""
}
}
let a = M(id: 1, foo: "bar")
let b = M(id: 2, foo: "baz")
print(M.diff(a, b) ?? "==")
// Prints:
//
// `M` values don't match:
// • .id doesn't match
// Actual:
// 1
// Expected:
// 2
// • .foo doesn't match
// Actual:
// bar
// Expected:
// baz
@mdiep
Copy link
Author

mdiep commented Feb 4, 2020

You can accomplish this same thing with Mirror and Hashable! https://gist.github.com/mdiep/fa69bd35339974d4d4e7b57009a9d0a1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment