Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions Sources/LoggerMiddleware/LoggerMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public final class LoggerMiddleware<M: Middleware>: Middleware where M.StateType
self.queue.async {
let actionMessage = self.actionTransform.transform(action: action, source: dispatcher)
self.actionPrinter.log(action: actionMessage)
self.stateDiffPrinter.log(state: self.stateDiffTransform.transform(oldState: stateBefore, newState: stateAfter))
if let diffString = self.stateDiffTransform.transform(oldState: stateBefore, newState: stateAfter) {
self.stateDiffPrinter.log(state: diffString)
}
}
}
}
Expand Down Expand Up @@ -123,9 +125,10 @@ extension LoggerMiddleware {
public enum StateDiffTransform {
case diff(linesOfContext: Int = 2, prefixLines: String = "🏛 ")
case newStateOnly
case custom((StateType?, StateType) -> String)
case recursive(prefixLines: String = "🏛 ", stateName: String)
case custom((StateType?, StateType) -> String?)

func transform(oldState: StateType?, newState: StateType) -> String {
func transform(oldState: StateType?, newState: StateType) -> String? {
switch self {
case let .diff(linesOfContext, prefixLines):
let stateBefore = dumpToString(oldState)
Expand All @@ -134,11 +137,93 @@ extension LoggerMiddleware {
?? "\(prefixLines) No state mutation"
case .newStateOnly:
return dumpToString(newState)
case let .recursive(prefixLines, stateName):
return recursiveDiff(prefixLines: prefixLines, stateName: stateName, before: oldState, after: newState)
case let .custom(closure):
return closure(oldState, newState)
}
}
}

public static func recursiveDiff(prefixLines: String, stateName: String, before: StateType?, after: StateType) -> String? {
// cuts the redundant newline character from the output
diff(prefix: prefixLines, name: stateName, lhs: before, rhs: after)?.trimmingCharacters(in: .whitespacesAndNewlines)
}

private static func diff<A>(prefix: String, name: String, level: Int = 0, lhs: A?, rhs: A?) -> String? {

guard let rightHandSide = rhs, let leftHandSide = lhs else {
if let rightHandSide = rhs {
return "\(prefix).\(name): nil → \(rightHandSide)"
}

if let leftHandSide = lhs {
return "\(prefix).\(name): \(leftHandSide) → nil"
}

// nil == lhs == rhs
return nil
}

// special handling for Dictionaries: stringify and order the keys before comparing
if let left = leftHandSide as? Dictionary<AnyHashable, Any>, let right = rightHandSide as? Dictionary<AnyHashable, Any> {

let leftSorted = left.sorted { a, b in "\(a.key)" < "\(b.key)" }
let rightSorted = right.sorted { a, b in "\(a.key)" < "\(b.key)" }

let leftPrintable = leftSorted.map { key, value in "\(key): \(value)" }.joined(separator: ", ")
let rightPrintable = rightSorted.map { key, value in "\(key): \(value)" }.joined(separator: ", ")

// .difference(from:) gives unpleasant results
if leftPrintable == rightPrintable {
return nil
}

return "\(prefix).\(name): 📦 [\(leftPrintable)] → [\(rightPrintable)]"
}

// special handling for sets as well: order the contents, compare as strings
if let left = leftHandSide as? Set<AnyHashable>, let right = rightHandSide as? Set<AnyHashable> {
let leftSorted = left.map { "\($0)" }.sorted { a, b in a < b }
let rightSorted = right.map { "\($0)" }.sorted { a, b in a < b }

let leftPrintable = leftSorted.joined(separator: ", ")
let rightPrintable = rightSorted.joined(separator: ", ")

// .difference(from:) gives unpleasant results
if leftPrintable == rightPrintable {
return nil
}
return "\(prefix).\(name): 📦 <\(leftPrintable)> → <\(rightPrintable)>"
}

let leftMirror = Mirror(reflecting: leftHandSide)
let rightMirror = Mirror(reflecting: rightHandSide)

// if there are no children, compare leftHandSide and rightHandSide directly
if 0 == leftMirror.children.count {
if "\(leftHandSide)" == "\(rightHandSide)" {
return nil
} else {
return "\(prefix).\(name): \(leftHandSide) → \(rightHandSide)"
}
}

// there are children -> diff the object graph recursively
let strings: [String] = leftMirror.children.map({ leftChild in
let toDotOrNotToDot = (level > 0) ? "." : " "
return Self.diff(prefix: "\(prefix)\(toDotOrNotToDot)\(name)",
name: leftChild.label ?? "#", // label might be missing for items in collections, # represents a collection element
level: level + 1,
lhs: leftChild.value,
rhs: rightMirror.children.first(where: { $0.label == leftChild.label })?.value)
}).compactMap { $0 }

if strings.count > 0 {
return strings.joined(separator: "\n")
}
return nil
}
}

// MARK: - Action
Expand Down
66 changes: 64 additions & 2 deletions Tests/LoggerMiddlewareTests/LoggerMiddlewareTests.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,73 @@
import XCTest
import SwiftRex
@testable import LoggerMiddleware

struct TestState: Equatable {
public let a: Substate
public let b: [Int]
public let c: String
public let d: String?
public let e: String?
}

struct Substate: Equatable {
public let x: Set<String>
public let y1: [String: Int]
public let y2: [String: Int?]
public let z: Bool
}

struct TestMiddleware: Middleware {
func receiveContext(getState: @escaping GetState<TestState>, output: AnyActionHandler<Int>) {
}

func handle(action: Int, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
}

typealias InputActionType = Int
typealias OutputActionType = Int
typealias StateType = TestState
}

final class LoggerMiddlewareTests: XCTestCase {
func testExample() {

func testStateDiff() {
// given
let beforeState: LoggerMiddleware<TestMiddleware>.StateType = TestState(a: Substate(x: ["SetB", "SetA"],
y1: ["one": 1, "eleven": 11],
y2: ["one": 1, "eleven": 11, "zapp": 42],
z: true),
b: [0, 1],
c: "Foo",
d: "✨",
e: nil)
let afterState: LoggerMiddleware<TestMiddleware>.StateType = TestState(a: Substate(x: ["SetB", "SetC"],
y1: ["one": 1, "twelve": 12],
y2: ["one": 1, "twelve": 12, "zapp": nil],
z: false),
b: [0],
c: "Bar",
d: nil,
e: "🥚")

// when
let result: String? = LoggerMiddleware<TestMiddleware>.recursiveDiff(prefixLines: "🏛", stateName: "TestState", before: beforeState, after: afterState)

// then
let expected = """
🏛 TestState.a.x: 📦 <SetA, SetB> → <SetB, SetC>
🏛 TestState.a.y1: 📦 [eleven: 11, one: 1] → [one: 1, twelve: 12]
🏛 TestState.a.y2: 📦 [eleven: Optional(11), one: Optional(1), zapp: Optional(42)] → [one: Optional(1), twelve: Optional(12), zapp: nil]
🏛 TestState.a.z: true → false
🏛 TestState.b.#: 1 → 0
🏛 TestState.c: Foo → Bar
🏛 TestState.d.some: ✨ → nil
🏛 TestState.e: nil → Optional("🥚")
"""
XCTAssertEqual(result, expected)
}

static var allTests = [
("testExample", testExample),
("testStateDiff", testStateDiff),
]
}