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.
|
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
@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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user