Skip to content

Commit abe3c71

Browse files
committed
Recursive diff: attempt to recursively diff any State object
1 parent 976b3c5 commit abe3c71

File tree

2 files changed

+128
-2
lines changed

2 files changed

+128
-2
lines changed

β€ŽSources/LoggerMiddleware/LoggerMiddleware.swiftβ€Ž

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ extension LoggerMiddleware {
125125
public enum StateDiffTransform {
126126
case diff(linesOfContext: Int = 2, prefixLines: String = "πŸ› ")
127127
case newStateOnly
128+
case recursive(prefixLines: String = "πŸ› ", stateName: String)
128129
case custom((StateType?, StateType) -> String?)
129130

130131
func transform(oldState: StateType?, newState: StateType) -> String? {
@@ -136,11 +137,86 @@ extension LoggerMiddleware {
136137
?? "\(prefixLines) No state mutation"
137138
case .newStateOnly:
138139
return dumpToString(newState)
140+
case let .recursive(prefixLines, stateName):
141+
return recursiveDiff(prefixLines: prefixLines, stateName: stateName, before: oldState, after: newState)
139142
case let .custom(closure):
140143
return closure(oldState, newState)
141144
}
142145
}
143146
}
147+
148+
public static func recursiveDiff(prefixLines: String, stateName: String, before: StateType?, after: StateType) -> String? {
149+
// cuts the redundant newline character from the output
150+
diff(prefix: prefixLines, name: stateName, lhs: before, rhs: after)?.trimmingCharacters(in: .whitespacesAndNewlines)
151+
}
152+
153+
private static func diff<A>(prefix: String, name: String, level: Int = 0, lhs: A, rhs: A) -> String? {
154+
let leftMirror = Mirror(reflecting: lhs)
155+
let rightMirror = Mirror(reflecting: rhs)
156+
157+
// special handling for Dictionaries
158+
if let left = lhs as? Dictionary<AnyHashable, Any>, let right = rhs as? Dictionary<AnyHashable, Any> {
159+
160+
let leftSorted = left.sorted { a, b in "\(a.key)" < "\(b.key)" }
161+
let rightSorted = right.sorted { a, b in "\(a.key)" < "\(b.key)" }
162+
163+
let leftPrintable = leftSorted.map { key, value in "\(key): \(value)" }.joined(separator: ", ")
164+
let rightPrintable = rightSorted.map { key, value in "\(key): \(value)" }.joined(separator: ", ")
165+
166+
// .difference(from:) gives unpleasant results
167+
if leftPrintable == rightPrintable {
168+
return nil
169+
}
170+
171+
return "\(prefix).\(name): πŸ“¦ [\(leftPrintable)] β†’ [\(rightPrintable)]"
172+
}
173+
174+
// special handling for sets as well: order the contents, compare as strings
175+
if let left = lhs as? Set<AnyHashable>, let right = rhs as? Set<AnyHashable> {
176+
let leftSorted = left.map { "\($0)" }.sorted { a, b in a < b }
177+
let rightSorted = right.map { "\($0)" }.sorted { a, b in a < b }
178+
179+
let leftPrintable = leftSorted.joined(separator: ", ")
180+
let rightPrintable = rightSorted.joined(separator: ", ")
181+
182+
// .difference(from:) gives unpleasant results
183+
if leftPrintable == rightPrintable {
184+
return nil
185+
}
186+
return "\(prefix).\(name): πŸ“¦ <\(leftPrintable)> β†’ <\(rightPrintable)>"
187+
}
188+
189+
// if there are no children, compare lhs and rhs directly
190+
if 0 == leftMirror.children.count {
191+
if "\(lhs)" == "\(rhs)" {
192+
return nil
193+
} else {
194+
return "\(prefix).\(name): \(lhs) β†’ \(rhs)"
195+
}
196+
}
197+
198+
// there are children -> diff the object graph recursively
199+
let strings: [String] = leftMirror.children.map({ leftChild in
200+
guard let rightChild = rightMirror.children.first(where: { $0.label == leftChild.label }) else {
201+
return nil
202+
}
203+
204+
let leftValue = leftChild.value
205+
let rightValue = rightChild.value
206+
207+
let dot = (level > 0) ? "." : " "
208+
return Self.diff(prefix: "\(prefix)\(dot)\(name)",
209+
name: leftChild.label ?? "",
210+
level: level + 1,
211+
lhs: leftValue,
212+
rhs: rightValue)
213+
}).compactMap { $0 }
214+
215+
if strings.count > 0 {
216+
return strings.joined(separator: "\n")
217+
}
218+
return nil
219+
}
144220
}
145221

146222
// MARK: - Action
Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,61 @@
11
import XCTest
2+
import SwiftRex
23
@testable import LoggerMiddleware
34

5+
struct TestState: Equatable {
6+
public let a: Substate
7+
public let b: [Int]
8+
public let c: String
9+
}
10+
11+
struct Substate: Equatable {
12+
public let x: Set<String>
13+
public let y: [String: Int]
14+
public let z: Bool
15+
}
16+
17+
struct TestMiddleware: Middleware {
18+
func receiveContext(getState: @escaping GetState<TestState>, output: AnyActionHandler<Int>) {
19+
}
20+
21+
func handle(action: Int, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
22+
}
23+
24+
typealias InputActionType = Int
25+
typealias OutputActionType = Int
26+
typealias StateType = TestState
27+
}
28+
429
final class LoggerMiddlewareTests: XCTestCase {
5-
func testExample() {
30+
31+
func testStateDiff() {
32+
// given
33+
let beforeState: LoggerMiddleware<TestMiddleware>.StateType = TestState(a: Substate(x: ["SetB", "SetA"],
34+
y: ["one": 1, "eleven": 11],
35+
z: true),
36+
b: [0, 1],
37+
c: "Foo")
38+
let afterState: LoggerMiddleware<TestMiddleware>.StateType = TestState(a: Substate(x: ["SetB", "SetC"],
39+
y: ["one": 1, "twelve": 12],
40+
z: false),
41+
b: [0],
42+
c: "Bar")
43+
44+
// when
45+
let result: String? = LoggerMiddleware<TestMiddleware>.recursiveDiff(prefixLines: "πŸ›", stateName: "TestState", before: beforeState, after: afterState)
46+
47+
// then
48+
let expected = """
49+
πŸ› TestState.some.a.x: πŸ“¦ <SetA, SetB> β†’ <SetB, SetC>
50+
πŸ› TestState.some.a.y: πŸ“¦ [eleven: 11, one: 1] β†’ [one: 1, twelve: 12]
51+
πŸ› TestState.some.a.z: true β†’ false
52+
πŸ› TestState.some.b: πŸ“¦ [0, 1] β†’ [0]
53+
πŸ› TestState.some.c: Foo β†’ Bar
54+
"""
55+
XCTAssertEqual(result, expected)
656
}
757

858
static var allTests = [
9-
("testExample", testExample),
59+
("testStateDiff", testStateDiff),
1060
]
1161
}

0 commit comments

Comments
Β (0)