feat: Adds onFailure reducer.
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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:)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,5 @@ public protocol ReceiveAction<ReceiveAction> {
|
||||
/// The root receive case that is used to handle the results.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import Foundation
|
||||
/// @Dependency(\.logger) var logger
|
||||
///
|
||||
/// 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.
|
||||
/// 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<State, Action: ReceiveAction>: Reducer {
|
||||
@usableFromInline
|
||||
let toResult: (Action) -> TaskResult<Action.ReceiveAction>?
|
||||
|
||||
@usableFromInline
|
||||
let onFail: OnFailAction<State, Action>
|
||||
|
||||
@usableFromInline
|
||||
let onSuccess: (inout State, Action.ReceiveAction) -> Effect<Action>
|
||||
|
||||
@usableFromInline
|
||||
init(
|
||||
internal toResult: @escaping (Action) -> TaskResult<Action.ReceiveAction>?,
|
||||
onFail: OnFailAction<State, Action>,
|
||||
onSuccess: @escaping(inout State, Action.ReceiveAction) -> Effect<Action>
|
||||
) {
|
||||
self.toResult = toResult
|
||||
self.onFail = onFail
|
||||
self.onSuccess = onSuccess
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
onFail: OnFailAction<State, Action> = .ignore,
|
||||
onSuccess: @escaping (inout State, Action.ReceiveAction) -> Effect<Action>
|
||||
) {
|
||||
self.init(
|
||||
internal: {
|
||||
AnyCasePath(unsafe: Action.receive).extract(from: $0)
|
||||
},
|
||||
onFail: onFail,
|
||||
onSuccess: onSuccess
|
||||
)
|
||||
}
|
||||
@@ -75,8 +70,8 @@ public struct ReceiveReducer<State, Action: ReceiveAction>: Reducer {
|
||||
public func reduce(into state: inout State, action: Action) -> Effect<Action> {
|
||||
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)
|
||||
}
|
||||
|
||||
144
Sources/ComposableSubscriber/Reducer+onFailure.swift
Normal file
144
Sources/ComposableSubscriber/Reducer+onFailure.swift
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ struct ReducerWithReceiveAction {
|
||||
@Dependency(\.numberClient) var numberClient
|
||||
|
||||
public var body: some Reducer<State, Action> {
|
||||
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<State, Action> {
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user