From 36fe70c6888fc58ab0336dfec2fa1d15521d671a Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 31 May 2024 17:40:03 -0400 Subject: [PATCH] feat: Adds onFailure reducer. --- .../Internal/SetAction.swift | 49 ------ .../ComposableSubscriber/OnFailAction.swift | 72 --------- .../ComposableSubscriber/ReceiveAction.swift | 8 - .../ComposableSubscriber/ReceiveReducer.swift | 15 +- .../Reducer+onFailure.swift | 144 ++++++++++++++++++ .../TCAExtrasTests.swift | 7 +- 6 files changed, 153 insertions(+), 142 deletions(-) delete mode 100644 Sources/ComposableSubscriber/Internal/SetAction.swift delete mode 100644 Sources/ComposableSubscriber/OnFailAction.swift create mode 100644 Sources/ComposableSubscriber/Reducer+onFailure.swift diff --git a/Sources/ComposableSubscriber/Internal/SetAction.swift b/Sources/ComposableSubscriber/Internal/SetAction.swift deleted file mode 100644 index c9bf621..0000000 --- a/Sources/ComposableSubscriber/Internal/SetAction.swift +++ /dev/null @@ -1,49 +0,0 @@ -import ComposableArchitecture -import Foundation - -@usableFromInline -enum SetAction { - case operation(f: (inout State, Value) -> Effect) - case keyPath(WritableKeyPath, effect: Effect) - case optionalKeyPath(WritableKeyPath, effect: Effect) - - @usableFromInline - func callAsFunction(state: inout State, value: Value) -> Effect { - switch self { - case let .operation(f: f): - return f(&state, value) - case let .keyPath(keyPath, effect): - state[keyPath: keyPath] = value - return effect - case let .optionalKeyPath(keyPath, effect): - state[keyPath: keyPath] = value - return effect - } - } - - @usableFromInline - static func operation(_ f: @escaping (inout State, Value) -> Void) -> Self { - .operation(f: { state, value in - f(&state, value) - return .none - }) - } - -} - -@usableFromInline -struct SetAction2 { - let operation: @Sendable (inout State, Value) -> Void - - init(_ operation: @escaping @Sendable (inout State, Value) -> Void) { - self.operation = operation - } - - static func keyPath( - _ keyPath: WritableKeyPath - ) -> Self { - .init({ state, value in - state[keyPath: keyPath] = value - }) - } -} diff --git a/Sources/ComposableSubscriber/OnFailAction.swift b/Sources/ComposableSubscriber/OnFailAction.swift deleted file mode 100644 index ff99cc9..0000000 --- a/Sources/ComposableSubscriber/OnFailAction.swift +++ /dev/null @@ -1,72 +0,0 @@ -import ComposableArchitecture -import Foundation -import OSLog - -/// Handle failures, generally used with actions that accept a task result. -/// -/// This is used in some of the higher order reducers to describe how they should handle failures. -public enum OnFailAction { - - /// Throw a runtime warning and optionally log the error. - case fail(prefix: String? = nil, log: (@Sendable (String) -> Void)? = nil) - - /// Ignore the error. - case ignore - - /// Handle the error, generally used to set the error on your state. - case operation((inout State, Error) -> Effect) - - @usableFromInline - func callAsFunction(state: inout State, error: Error) -> Effect { - switch self { - case .ignore: - return .none - case let .fail(prefix, log): - if let prefix { - return .fail(prefix: prefix, error: error, log: log) - } else { - return .fail(error: error, log: log) - } - case let .operation(handler): - return handler(&state, error) - - } - } - - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - @inlinable - static func fail(prefix: String? = nil, logger: Logger) -> Self { - .fail(prefix: prefix, log: { logger.error("\($0)") }) - } - - @inlinable - public static func set( - _ operation: @escaping @Sendable (inout State, Error) -> Void - ) -> Self { - .operation( - SetAction.operation(operation).callAsFunction(state:value:) - ) - } - - @inlinable - public static func set( - _ operation: @escaping @Sendable (inout State, Error) -> Effect - ) -> Self { - .operation( - SetAction.operation(f: operation).callAsFunction(state:value:) - ) - } - - @inlinable - public static func set( - keyPath: WritableKeyPath, - effect: Effect = .none - ) -> Self { - .operation( - SetAction.optionalKeyPath( - keyPath, - effect: effect - ).callAsFunction(state:value:) - ) - } -} diff --git a/Sources/ComposableSubscriber/ReceiveAction.swift b/Sources/ComposableSubscriber/ReceiveAction.swift index c535ee5..7690f87 100644 --- a/Sources/ComposableSubscriber/ReceiveAction.swift +++ b/Sources/ComposableSubscriber/ReceiveAction.swift @@ -14,13 +14,5 @@ public protocol ReceiveAction { /// The root receive case that is used to handle the results. static func receive(_ result: TaskResult) -> Self - /// Extracts the result from the action. - var result: TaskResult? { get } } -extension ReceiveAction { - - public var result: TaskResult? { - AnyCasePath(unsafe: Self.receive).extract(from: self) - } -} diff --git a/Sources/ComposableSubscriber/ReceiveReducer.swift b/Sources/ComposableSubscriber/ReceiveReducer.swift index 32945ec..0e82081 100644 --- a/Sources/ComposableSubscriber/ReceiveReducer.swift +++ b/Sources/ComposableSubscriber/ReceiveReducer.swift @@ -24,7 +24,7 @@ import Foundation /// @Dependency(\.logger) var logger /// /// public var body: some ReducerOf { -/// ReceiveReducer(onFail: .fail(logger: logger)) { state, action in +/// ReceiveReducer { state, action in /// // Handle the success cases by switching on the receive action. /// switch action { /// case let .numberFact(fact): @@ -32,6 +32,8 @@ import Foundation /// return .none /// } /// } +/// .onFail(.log(logger: logger)) +/// /// ... /// } /// @@ -40,33 +42,26 @@ public struct ReceiveReducer: Reducer { @usableFromInline let toResult: (Action) -> TaskResult? - @usableFromInline - let onFail: OnFailAction - @usableFromInline let onSuccess: (inout State, Action.ReceiveAction) -> Effect @usableFromInline init( internal toResult: @escaping (Action) -> TaskResult?, - onFail: OnFailAction, onSuccess: @escaping(inout State, Action.ReceiveAction) -> Effect ) { self.toResult = toResult - self.onFail = onFail self.onSuccess = onSuccess } @inlinable public init( - onFail: OnFailAction = .ignore, onSuccess: @escaping (inout State, Action.ReceiveAction) -> Effect ) { self.init( internal: { AnyCasePath(unsafe: Action.receive).extract(from: $0) }, - onFail: onFail, onSuccess: onSuccess ) } @@ -75,8 +70,8 @@ public struct ReceiveReducer: Reducer { public func reduce(into state: inout State, action: Action) -> Effect { guard let result = toResult(action) else { return .none } switch result { - case let .failure(error): - return onFail(state: &state, error: error) + case .failure: + return .none case let .success(value): return onSuccess(&state, value) } diff --git a/Sources/ComposableSubscriber/Reducer+onFailure.swift b/Sources/ComposableSubscriber/Reducer+onFailure.swift new file mode 100644 index 0000000..a2b328a --- /dev/null +++ b/Sources/ComposableSubscriber/Reducer+onFailure.swift @@ -0,0 +1,144 @@ +import ComposableArchitecture +import Foundation +import OSLog + +extension Reducer { + + @inlinable + public func onFailure( + case toError: CaseKeyPath, + _ onFail: OnFailureAction + ) -> _OnFailureReducer { + .init( + parent: self, + toError: .init(AnyCasePath(toError)), + onFailAction: onFail + ) + } + + @inlinable + public func onFailure( + case toError: CaseKeyPath>, + _ onFail: OnFailureAction + ) -> _OnFailureReducer { + .init( + parent: self, + toError: .init(AnyCasePath(toError)), + onFailAction: onFail + ) + } +} + +extension Reducer where Action: ReceiveAction { + + @inlinable + public func onFailure( + _ onFail: OnFailureAction + ) -> _OnFailureReducer { + .init( + parent: self, + toError: .init(AnyCasePath(unsafe: Action.receive)), + onFailAction: onFail) + } +} + +public struct OnFailureAction: Sendable { + + @usableFromInline + let operation: @Sendable (inout State, Error) -> Effect + + @inlinable + public init(_ operation: @escaping @Sendable (inout State, Error) -> Effect) { + self.operation = operation + } + + @inlinable + public static func set(_ keyPath: WritableKeyPath) -> Self { + .init { state, error in + state[keyPath: keyPath] = error + return .none + } + } + + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + @inlinable + public static func log(prefix: String? = nil, logger: Logger) -> Self { + .fail(prefix: prefix, log: { logger.error("\($0)") }) + } + + @inlinable + public static func fail(prefix: String? = nil, log: (@Sendable (String) -> Void)? = nil) -> Self { + .init { _, error in + guard let prefix else { + return .fail(error: error, log: log) + } + return .fail(prefix: prefix, error: error, log: log) + } + } + + @usableFromInline + func callAsFunction(state: inout State, error: Error) -> Effect { + operation(&state, error) + } +} + +@usableFromInline +struct ToError { + + @usableFromInline + let operation: (Action) -> Error? + + @usableFromInline + init(_ casePath: AnyCasePath) { + self.operation = { casePath.extract(from: $0) } + } + + @usableFromInline + init(_ result: AnyCasePath>) { + self.operation = { + let result = result.extract(from: $0) + guard case let .failure(error) = result else { return nil } + return error + } + } + +} + +public struct _OnFailureReducer: Reducer { + + @usableFromInline + let parent: Parent + + @usableFromInline + let toError: ToError + + @usableFromInline + let onFailAction: OnFailureAction + + @usableFromInline + init( + parent: Parent, + toError: ToError, + onFailAction: OnFailureAction + ) { + self.parent = parent + self.toError = toError + self.onFailAction = onFailAction + } + + public func reduce( + into state: inout Parent.State, + action: Parent.Action + ) -> Effect { + let baseEffects = parent.reduce(into: &state, action: action) + + guard let error = toError.operation(action) else { + return baseEffects + } + + return .merge( + baseEffects, + onFailAction(state: &state, error: error) + ) + } +} diff --git a/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift b/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift index 30435ba..4f1f847 100644 --- a/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift +++ b/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift @@ -136,7 +136,7 @@ struct ReducerWithReceiveAction { @Dependency(\.numberClient) var numberClient public var body: some Reducer { - ReceiveReducer(onFail: .fail()) { state, action in + ReceiveReducer { state, action in switch action { case let .currentNumber(number): state.currentNumber = number @@ -175,7 +175,7 @@ struct FailingReducer { @Dependency(\.numberClient) var numberClient public var body: some Reducer { - ReceiveReducer(onFail: .set(keyPath: \.error)) { state, action in + ReceiveReducer { state, action in switch action { case .currentNumber(_): return .none @@ -184,6 +184,7 @@ struct FailingReducer { .receive(on: \.task, case: \.currentNumber) { try await numberClient.currentNumber(fail: true) } + .onFailure(.set(\.error)) } } @@ -239,7 +240,7 @@ final class TCAExtrasTests: XCTestCase { await store.receive(\.receive.success.currentNumber) { $0.currentNumber = 69420 } - + await task.cancel() await store.finish() }