native macOS codings agent orchestrator
1import ComposableArchitecture
2import CustomDump
3import Foundation
4import Sentry
5
6extension Reducer where State: Equatable {
7 @ReducerBuilder<State, Action>
8 func logActions() -> some Reducer<State, Action> {
9 LogActionsReducer(base: self)
10 }
11}
12
13struct LogActionsReducer<Base: Reducer>: Reducer where Base.State: Equatable {
14 let base: Base
15
16 private let logger = SupaLogger("TCA")
17
18 func reduce(into state: inout Base.State, action: Base.Action) -> Effect<Base.Action> {
19 #if DEBUG
20 let actionLabel = debugCaseOutput(action)
21 logger.debug("Action: \(actionLabel)")
22 let previousState = state
23 let effects = base.reduce(into: &state, action: action)
24 if previousState != state, let diff = CustomDump.diff(previousState, state) {
25 print(diff)
26 }
27 return effects
28 #else
29 let actionLabel = releaseActionLabel(action)
30 logger.debug("Action: \(actionLabel)")
31 SentrySDK.logger.info("Action: \(actionLabel)")
32 let breadcrumb = Breadcrumb(level: .debug, category: "action")
33 breadcrumb.message = actionLabel
34 SentrySDK.addBreadcrumb(breadcrumb)
35 return base.reduce(into: &state, action: action)
36 #endif
37 }
38}
39
40func debugCaseOutput(
41 _ value: Any,
42 abbreviated: Bool = false
43) -> String {
44 func debugCaseOutputHelp(_ value: Any) -> String {
45 let mirror = Mirror(reflecting: value)
46 switch mirror.displayStyle {
47 case .enum:
48 guard let child = mirror.children.first else {
49 let childOutput = "\(value)"
50 return childOutput == "\(typeName(type(of: value)))" ? "" : ".\(childOutput)"
51 }
52 let childOutput = debugCaseOutputHelp(child.value)
53 return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")"
54 case .tuple:
55 return mirror.children.map { label, value in
56 let childOutput = debugCaseOutputHelp(value)
57 let labelValue = label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? ""
58 let suffix = childOutput.isEmpty ? "" : " \(childOutput)"
59 return "\(labelValue)\(suffix)"
60 }
61 .joined(separator: ", ")
62 default:
63 return ""
64 }
65 }
66
67 return (value as? any CustomDebugStringConvertible)?.debugDescription
68 ?? "\(abbreviated ? "" : typeName(type(of: value)))\(debugCaseOutputHelp(value))"
69}
70
71func releaseActionLabel(_ value: Any) -> String {
72 let rootType = shortTypeName(type(of: value))
73 let casePath = releaseEnumCasePath(value)
74 guard !casePath.isEmpty else {
75 return rootType
76 }
77 return "\(rootType).\(casePath.joined(separator: "."))"
78}
79
80private func isUnlabeledArgument(_ label: String) -> Bool {
81 label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil
82}
83
84private func releaseEnumCasePath(_ value: Any) -> [String] {
85 var labels: [String] = []
86 var currentValue = value
87
88 while true {
89 let mirror = Mirror(reflecting: currentValue)
90 guard mirror.displayStyle == .enum else {
91 return labels
92 }
93 if let child = mirror.children.first, let label = child.label {
94 labels.append(label)
95 let childMirror = Mirror(reflecting: child.value)
96 guard childMirror.displayStyle == .enum else {
97 return labels
98 }
99 currentValue = child.value
100 } else {
101 labels.append(caseName(String(describing: currentValue)))
102 return labels
103 }
104 }
105}
106
107private func caseName(_ description: String) -> String {
108 if let parenIndex = description.firstIndex(of: "(") {
109 return String(description[..<parenIndex])
110 }
111 return description
112}
113
114private func shortTypeName(_ type: Any.Type) -> String {
115 let components = String(reflecting: type)
116 .split(separator: ".")
117 .filter { !$0.hasPrefix("(unknown context at $") }
118 .suffix(2)
119 return components.isEmpty ? String(reflecting: type) : components.joined(separator: ".")
120}
121
122private func typeName(
123 _ type: Any.Type,
124 qualified: Bool = true,
125 genericsAbbreviated: Bool = true
126) -> String {
127 var name = _typeName(type, qualified: qualified)
128 .replacing(#/\(unknown context at \$[0-9A-Fa-f]+\)\./#, with: "")
129 for _ in 1...10 {
130 let abbreviated =
131 name
132 .replacing(#/\bSwift\.Optional<([^><]+)>/#) { match in
133 "\(match.1)?"
134 }
135 .replacing(#/\bSwift\.Array<([^><]+)>/#) { match in
136 "[\(match.1)]"
137 }
138 .replacing(#/\bSwift\.Dictionary<([^,<]+), ([^><]+)>/#) { match in
139 "[\(match.1): \(match.2)]"
140 }
141 if abbreviated == name { break }
142 name = abbreviated
143 }
144 name = name.replacing(#/\w+\.([\w.]+)/#) { match in
145 "\(match.1)"
146 }
147 if genericsAbbreviated {
148 name = name.replacing(#/<.+>/#, with: "")
149 }
150 return name
151}