feat: Adds onFailure reducer.

This commit is contained in:
2024-05-31 17:40:03 -04:00
parent ab915f88db
commit 36fe70c688
6 changed files with 153 additions and 142 deletions

View File

@@ -1,49 +0,0 @@
import ComposableArchitecture
import Foundation
@usableFromInline
enum SetAction<State, Action, Value> {
case operation(f: (inout State, Value) -> Effect<Action>)
case keyPath(WritableKeyPath<State, Value>, effect: Effect<Action>)
case optionalKeyPath(WritableKeyPath<State, Value?>, effect: Effect<Action>)
@usableFromInline
func callAsFunction(state: inout State, value: Value) -> Effect<Action> {
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<State, Action, Value> {
let operation: @Sendable (inout State, Value) -> Void
init(_ operation: @escaping @Sendable (inout State, Value) -> Void) {
self.operation = operation
}
static func keyPath(
_ keyPath: WritableKeyPath<State, Value>
) -> Self {
.init({ state, value in
state[keyPath: keyPath] = value
})
}
}

View File

@@ -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<State, Action> {
/// 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<Action>)
@usableFromInline
func callAsFunction(state: inout State, error: Error) -> Effect<Action> {
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<Action>
) -> Self {
.operation(
SetAction.operation(f: operation).callAsFunction(state:value:)
)
}
@inlinable
public static func set(
keyPath: WritableKeyPath<State, Error?>,
effect: Effect<Action> = .none
) -> Self {
.operation(
SetAction.optionalKeyPath(
keyPath,
effect: effect
).callAsFunction(state:value:)
)
}
}

View File

@@ -14,13 +14,5 @@ public protocol ReceiveAction<ReceiveAction> {
/// The root receive case that is used to handle the results. /// The root receive case that is used to handle the results.
static func receive(_ result: TaskResult<ReceiveAction>) -> Self static func receive(_ result: TaskResult<ReceiveAction>) -> Self
/// Extracts the result from the action.
var result: TaskResult<ReceiveAction>? { get }
} }
extension ReceiveAction {
public var result: TaskResult<ReceiveAction>? {
AnyCasePath(unsafe: Self.receive).extract(from: self)
}
}

View File

@@ -24,7 +24,7 @@ import Foundation
/// @Dependency(\.logger) var logger /// @Dependency(\.logger) var logger
/// ///
/// public var body: some ReducerOf<Self> { /// public var body: some ReducerOf<Self> {
/// ReceiveReducer(onFail: .fail(logger: logger)) { state, action in /// ReceiveReducer { state, action in
/// // Handle the success cases by switching on the receive action. /// // Handle the success cases by switching on the receive action.
/// switch action { /// switch action {
/// case let .numberFact(fact): /// case let .numberFact(fact):
@@ -32,6 +32,8 @@ import Foundation
/// return .none /// return .none
/// } /// }
/// } /// }
/// .onFail(.log(logger: logger))
///
/// ... /// ...
/// } /// }
/// ///
@@ -40,33 +42,26 @@ public struct ReceiveReducer<State, Action: ReceiveAction>: Reducer {
@usableFromInline @usableFromInline
let toResult: (Action) -> TaskResult<Action.ReceiveAction>? let toResult: (Action) -> TaskResult<Action.ReceiveAction>?
@usableFromInline
let onFail: OnFailAction<State, Action>
@usableFromInline @usableFromInline
let onSuccess: (inout State, Action.ReceiveAction) -> Effect<Action> let onSuccess: (inout State, Action.ReceiveAction) -> Effect<Action>
@usableFromInline @usableFromInline
init( init(
internal toResult: @escaping (Action) -> TaskResult<Action.ReceiveAction>?, internal toResult: @escaping (Action) -> TaskResult<Action.ReceiveAction>?,
onFail: OnFailAction<State, Action>,
onSuccess: @escaping(inout State, Action.ReceiveAction) -> Effect<Action> onSuccess: @escaping(inout State, Action.ReceiveAction) -> Effect<Action>
) { ) {
self.toResult = toResult self.toResult = toResult
self.onFail = onFail
self.onSuccess = onSuccess self.onSuccess = onSuccess
} }
@inlinable @inlinable
public init( public init(
onFail: OnFailAction<State, Action> = .ignore,
onSuccess: @escaping (inout State, Action.ReceiveAction) -> Effect<Action> onSuccess: @escaping (inout State, Action.ReceiveAction) -> Effect<Action>
) { ) {
self.init( self.init(
internal: { internal: {
AnyCasePath(unsafe: Action.receive).extract(from: $0) AnyCasePath(unsafe: Action.receive).extract(from: $0)
}, },
onFail: onFail,
onSuccess: onSuccess onSuccess: onSuccess
) )
} }
@@ -75,8 +70,8 @@ public struct ReceiveReducer<State, Action: ReceiveAction>: Reducer {
public func reduce(into state: inout State, action: Action) -> Effect<Action> { public func reduce(into state: inout State, action: Action) -> Effect<Action> {
guard let result = toResult(action) else { return .none } guard let result = toResult(action) else { return .none }
switch result { switch result {
case let .failure(error): case .failure:
return onFail(state: &state, error: error) return .none
case let .success(value): case let .success(value):
return onSuccess(&state, value) return onSuccess(&state, value)
} }

View File

@@ -0,0 +1,144 @@
import ComposableArchitecture
import Foundation
import OSLog
extension Reducer {
@inlinable
public func onFailure(
case toError: CaseKeyPath<Action, Error>,
_ onFail: OnFailureAction<State, Action>
) -> _OnFailureReducer<Self> {
.init(
parent: self,
toError: .init(AnyCasePath(toError)),
onFailAction: onFail
)
}
@inlinable
public func onFailure<T>(
case toError: CaseKeyPath<Action, TaskResult<T>>,
_ onFail: OnFailureAction<State, Action>
) -> _OnFailureReducer<Self> {
.init(
parent: self,
toError: .init(AnyCasePath(toError)),
onFailAction: onFail
)
}
}
extension Reducer where Action: ReceiveAction {
@inlinable
public func onFailure(
_ onFail: OnFailureAction<State, Action>
) -> _OnFailureReducer<Self> {
.init(
parent: self,
toError: .init(AnyCasePath(unsafe: Action.receive)),
onFailAction: onFail)
}
}
public struct OnFailureAction<State, Action>: Sendable {
@usableFromInline
let operation: @Sendable (inout State, Error) -> Effect<Action>
@inlinable
public init(_ operation: @escaping @Sendable (inout State, Error) -> Effect<Action>) {
self.operation = operation
}
@inlinable
public static func set(_ keyPath: WritableKeyPath<State, Error?>) -> 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<Action> {
operation(&state, error)
}
}
@usableFromInline
struct ToError<Action> {
@usableFromInline
let operation: (Action) -> Error?
@usableFromInline
init(_ casePath: AnyCasePath<Action, Error>) {
self.operation = { casePath.extract(from: $0) }
}
@usableFromInline
init<T>(_ result: AnyCasePath<Action, TaskResult<T>>) {
self.operation = {
let result = result.extract(from: $0)
guard case let .failure(error) = result else { return nil }
return error
}
}
}
public struct _OnFailureReducer<Parent: Reducer>: Reducer {
@usableFromInline
let parent: Parent
@usableFromInline
let toError: ToError<Parent.Action>
@usableFromInline
let onFailAction: OnFailureAction<Parent.State, Parent.Action>
@usableFromInline
init(
parent: Parent,
toError: ToError<Parent.Action>,
onFailAction: OnFailureAction<Parent.State, Parent.Action>
) {
self.parent = parent
self.toError = toError
self.onFailAction = onFailAction
}
public func reduce(
into state: inout Parent.State,
action: Parent.Action
) -> Effect<Parent.Action> {
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)
)
}
}

View File

@@ -136,7 +136,7 @@ struct ReducerWithReceiveAction {
@Dependency(\.numberClient) var numberClient @Dependency(\.numberClient) var numberClient
public var body: some Reducer<State, Action> { public var body: some Reducer<State, Action> {
ReceiveReducer(onFail: .fail()) { state, action in ReceiveReducer { state, action in
switch action { switch action {
case let .currentNumber(number): case let .currentNumber(number):
state.currentNumber = number state.currentNumber = number
@@ -175,7 +175,7 @@ struct FailingReducer {
@Dependency(\.numberClient) var numberClient @Dependency(\.numberClient) var numberClient
public var body: some Reducer<State, Action> { public var body: some Reducer<State, Action> {
ReceiveReducer(onFail: .set(keyPath: \.error)) { state, action in ReceiveReducer { state, action in
switch action { switch action {
case .currentNumber(_): case .currentNumber(_):
return .none return .none
@@ -184,6 +184,7 @@ struct FailingReducer {
.receive(on: \.task, case: \.currentNumber) { .receive(on: \.task, case: \.currentNumber) {
try await numberClient.currentNumber(fail: true) try await numberClient.currentNumber(fail: true)
} }
.onFailure(.set(\.error))
} }
} }
@@ -239,7 +240,7 @@ final class TCAExtrasTests: XCTestCase {
await store.receive(\.receive.success.currentNumber) { await store.receive(\.receive.success.currentNumber) {
$0.currentNumber = 69420 $0.currentNumber = 69420
} }
await task.cancel() await task.cancel()
await store.finish() await store.finish()
} }