Skip to content

Commit e90f875

Browse files
committed
Improve logger
1 parent 8b111b2 commit e90f875

File tree

1 file changed

+172
-27
lines changed

1 file changed

+172
-27
lines changed

Sources/LoggerMiddleware/LoggerMiddleware.swift

Lines changed: 172 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,191 @@ import Foundation
22
import os.log
33
import SwiftRex
44

5-
final class LoggerMiddleware<AppAction, AppState: Equatable>: Middleware {
6-
typealias InputActionType = AppAction
7-
typealias OutputActionType = Never
8-
typealias StateType = AppState
5+
public final class LoggerMiddleware<AppAction, AppState: Equatable>: Middleware {
6+
public typealias InputActionType = AppAction
7+
public typealias OutputActionType = Never
8+
public typealias StateType = AppState
99

10-
private var getState: GetState<StateType>!
11-
private let actionPrint: (AppAction) -> String?
12-
private let statePrint: (AppState) -> String?
10+
private var getState: GetState<StateType>?
11+
private let printer: (StaticString, CVarArg...) -> Void
1312

14-
init(actionPrint: @escaping (AppAction) -> String? = { "\($0)" }, statePrint: @escaping (AppState) -> String? = { "\($0)" }) {
15-
self.actionPrint = actionPrint
16-
self.statePrint = statePrint
13+
public init(printer: ((StaticString, CVarArg...) -> Void)? = nil) {
14+
self.printer = printer ?? { (message: StaticString, args: CVarArg...) -> Void in os_log(.debug, log: .default, message, args) }
1715
}
1816

19-
func receiveContext(getState: @escaping GetState<StateType>, output: AnyActionHandler<OutputActionType>) {
17+
public init(printer: @escaping (String) -> Void) {
18+
self.printer = { (message: StaticString, args: CVarArg...) -> Void in printer(String(format: "\(message)", args)) }
19+
}
20+
21+
public func receiveContext(getState: @escaping GetState<StateType>, output: AnyActionHandler<OutputActionType>) {
2022
self.getState = getState
2123
}
2224

23-
func handle(action: InputActionType, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
24-
let stateBefore = getState()
25+
public func handle(action: InputActionType, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
26+
guard let getState = self.getState else { return }
27+
28+
var stateBefore = ""
29+
dump(getState(), to: &stateBefore, name: nil, indent: 2)
2530
afterReducer = .do {
26-
let stateAfter = self.getState()
31+
var stateAfter = ""
32+
dump(getState(), to: &stateAfter, name: nil, indent: 2)
2733

28-
var enabled = false
29-
if let actionString = self.actionPrint(action) {
30-
enabled = true
31-
let message = "🕹 \(actionString) from \(dispatcher)"
32-
os_log(.debug, log: .default, "%{PUBLIC}@", message)
33-
}
34+
let message = "🕹 \(action) from \(dispatcher)"
35+
self.printer("%{PUBLIC}@", message)
3436

35-
if stateBefore != stateAfter, let stateString = self.statePrint(stateAfter) {
36-
enabled = true
37-
os_log(.debug, log: .default, "🏛 %{PUBLIC}@", stateString)
37+
if let stateString = diff(old: stateBefore, new: stateAfter) {
38+
self.printer("\n%{PUBLIC}@", stateString)
3839
} else {
39-
os_log(.debug, log: .default, "🏛 No state mutation")
40+
self.printer("🏛 No state mutation")
4041
}
4142

42-
if enabled {
43-
os_log(.debug, log: .default, "")
44-
}
43+
self.printer("")
44+
}
45+
}
46+
}
47+
48+
struct Difference<A> {
49+
enum Which {
50+
case first
51+
case second
52+
case both
53+
}
54+
55+
let elements: [A]
56+
let which: Which
57+
}
58+
59+
func diff(old: String, new: String) -> String? {
60+
guard old != new else { return nil }
61+
let oldSplit = old.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
62+
let newSplit = new.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
63+
64+
return chunk(
65+
diff: diff(oldSplit, newSplit),
66+
context: 2 // Lines before and after the changes to be shown as context
67+
).lazy.flatMap { [$0.patchMark] + $0.lines }.map { "🏛 \($0)" }.joined(separator: "\n")
68+
}
69+
70+
func diff(_ fst: [String], _ snd: [String]) -> [Difference<String>] {
71+
var idxsOf = [String: [Int]]()
72+
fst.enumerated().forEach { idxsOf[$1, default: []].append($0) }
73+
74+
let sub = snd.enumerated().reduce((overlap: [Int: Int](), fst: 0, snd: 0, len: 0)) { sub, sndPair in
75+
(idxsOf[sndPair.element] ?? [])
76+
.reduce((overlap: [Int: Int](), fst: sub.fst, snd: sub.snd, len: sub.len)) { innerSub, fstIdx in
77+
78+
var newOverlap = innerSub.overlap
79+
newOverlap[fstIdx] = (sub.overlap[fstIdx - 1] ?? 0) + 1
80+
81+
if let newLen = newOverlap[fstIdx], newLen > sub.len {
82+
return (newOverlap, fstIdx - newLen + 1, sndPair.offset - newLen + 1, newLen)
83+
}
84+
return (newOverlap, innerSub.fst, innerSub.snd, innerSub.len)
4585
}
4686
}
87+
let (_, fstIdx, sndIdx, len) = sub
88+
89+
if len == 0 {
90+
let fstDiff = fst.isEmpty ? [] : [Difference(elements: fst, which: .first)]
91+
let sndDiff = snd.isEmpty ? [] : [Difference(elements: snd, which: .second)]
92+
return fstDiff + sndDiff
93+
} else {
94+
let fstDiff = diff(Array(fst.prefix(upTo: fstIdx)), Array(snd.prefix(upTo: sndIdx)))
95+
let midDiff = [Difference(elements: Array(fst.suffix(from: fstIdx).prefix(len)), which: .both)]
96+
let lstDiff = diff(Array(fst.suffix(from: fstIdx + len)), Array(snd.suffix(from: sndIdx + len)))
97+
return fstDiff + midDiff + lstDiff
98+
}
99+
}
100+
101+
let minus = ""
102+
let plus = "+"
103+
private let figureSpace = "\u{2007}"
104+
105+
struct Hunk {
106+
let fstIdx: Int
107+
let fstLen: Int
108+
let sndIdx: Int
109+
let sndLen: Int
110+
let lines: [String]
111+
112+
var patchMark: String {
113+
let fstMark = "\(minus)\(fstIdx + 1),\(fstLen)"
114+
let sndMark = "\(plus)\(sndIdx + 1),\(sndLen)"
115+
return "@@ \(fstMark) \(sndMark) @@"
116+
}
117+
118+
// Semigroup
119+
static func + (lhs: Hunk, rhs: Hunk) -> Hunk {
120+
return Hunk(
121+
fstIdx: lhs.fstIdx + rhs.fstIdx,
122+
fstLen: lhs.fstLen + rhs.fstLen,
123+
sndIdx: lhs.sndIdx + rhs.sndIdx,
124+
sndLen: lhs.sndLen + rhs.sndLen,
125+
lines: lhs.lines + rhs.lines
126+
)
127+
}
128+
129+
// Monoid
130+
init(fstIdx: Int = 0, fstLen: Int = 0, sndIdx: Int = 0, sndLen: Int = 0, lines: [String] = []) {
131+
self.fstIdx = fstIdx
132+
self.fstLen = fstLen
133+
self.sndIdx = sndIdx
134+
self.sndLen = sndLen
135+
self.lines = lines
136+
}
137+
138+
init(idx: Int = 0, len: Int = 0, lines: [String] = []) {
139+
self.init(fstIdx: idx, fstLen: len, sndIdx: idx, sndLen: len, lines: lines)
140+
}
141+
}
142+
143+
func chunk(diff diffs: [Difference<String>], context ctx: Int = 4) -> [Hunk] {
144+
func prepending(_ prefix: String) -> (String) -> String {
145+
return { prefix + $0 + ($0.hasSuffix(" ") ? "¬" : "") }
146+
}
147+
let changed: (Hunk) -> Bool = { $0.lines.contains(where: { $0.hasPrefix(minus) || $0.hasPrefix(plus) }) }
148+
149+
let (hunk, hunks) = diffs
150+
.reduce((current: Hunk(), hunks: [Hunk]())) { cursor, diff in
151+
let (current, hunks) = cursor
152+
let len = diff.elements.count
153+
154+
switch diff.which {
155+
case .both where len > ctx * 2:
156+
let hunk = current + Hunk(len: ctx, lines: diff.elements.prefix(ctx).map(prepending(figureSpace)))
157+
let next = Hunk(
158+
fstIdx: current.fstIdx + current.fstLen + len - ctx,
159+
fstLen: ctx,
160+
sndIdx: current.sndIdx + current.sndLen + len - ctx,
161+
sndLen: ctx,
162+
lines: (diff.elements.suffix(ctx) as ArraySlice<String>).map(prepending(figureSpace))
163+
)
164+
return (next, changed(hunk) ? hunks + [hunk] : hunks)
165+
case .both where current.lines.isEmpty:
166+
let lines = (diff.elements.suffix(ctx) as ArraySlice<String>).map(prepending(figureSpace))
167+
let count = lines.count
168+
return (current + Hunk(idx: len - count, len: count, lines: lines), hunks)
169+
case .both:
170+
return (current + Hunk(len: len, lines: diff.elements.map(prepending(figureSpace))), hunks)
171+
case .first:
172+
return (current + Hunk(fstLen: len, lines: diff.elements.map(prepending(minus))), hunks)
173+
case .second:
174+
return (current + Hunk(sndLen: len, lines: diff.elements.map(prepending(plus))), hunks)
175+
}
176+
}
177+
178+
return changed(hunk) ? hunks + [hunk] : hunks
47179
}
180+
181+
extension LoggerMiddleware {
182+
public func lift() -> AnyMiddleware<AppAction, AppAction, AppState> {
183+
self.lift(
184+
inputActionMap: identity,
185+
outputActionMap: absurd,
186+
stateMap: identity
187+
).eraseToAnyMiddleware()
188+
}
189+
}
190+
191+
func absurd<T>(_ never: Never) -> T { }
192+
func identity<T>(_ t: T) -> T { t }

0 commit comments

Comments
 (0)