feat: Adds documentation and removes some unused code.
This commit is contained in:
@@ -11,6 +11,38 @@ extension Effect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A convenience effect for sending an action that receives a task result from the given operation.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// @Reducer
|
||||||
|
/// struct MyFeature {
|
||||||
|
/// ...
|
||||||
|
/// enum Action {
|
||||||
|
/// case receive(TaskResult<Int>)
|
||||||
|
/// case task
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @Dependency(\.numberClient) var numberClient
|
||||||
|
///
|
||||||
|
/// var body: some Reducer<State, Action> {
|
||||||
|
/// Reduce { state, action in
|
||||||
|
/// switch action {
|
||||||
|
/// ...
|
||||||
|
/// case .task:
|
||||||
|
/// return .receive(action: \.receive) {
|
||||||
|
/// try await numberClient.numberFact()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - action: The action to embed the task result in.
|
||||||
|
/// - operation: The operation to call to create the task result.
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func receive<T>(
|
public static func receive<T>(
|
||||||
action toResult: CaseKeyPath<Action, TaskResult<T>>,
|
action toResult: CaseKeyPath<Action, TaskResult<T>>,
|
||||||
@@ -19,6 +51,42 @@ extension Effect {
|
|||||||
.receive(operation: .case(AnyCasePath(toResult), operation))
|
.receive(operation: .case(AnyCasePath(toResult), operation))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A convenience effect for sending an action that receives a task result from the given operation and then
|
||||||
|
/// transforming the output of the operation.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// @Reducer
|
||||||
|
/// struct MyFeature {
|
||||||
|
/// ...
|
||||||
|
/// enum Action {
|
||||||
|
/// case receive(TaskResult<String>)
|
||||||
|
/// case task
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @Dependency(\.numberClient) var numberClient
|
||||||
|
///
|
||||||
|
/// var body: some Reducer<State, Action> {
|
||||||
|
/// Reduce { state, action in
|
||||||
|
/// switch action {
|
||||||
|
/// ...
|
||||||
|
/// case .task:
|
||||||
|
/// return .receive(action: \.receive) {
|
||||||
|
/// try await numberClient.numberFact()
|
||||||
|
/// } transform: { number in
|
||||||
|
/// "The current number fact is: \(number)"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - action: The action to embed the task result in.
|
||||||
|
/// - operation: The operation to call to create the task result.
|
||||||
|
/// - transform: The operation used to transform the output of the operation.
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func receive<T, V>(
|
public static func receive<T, V>(
|
||||||
action toResult: CaseKeyPath<Action, TaskResult<V>>,
|
action toResult: CaseKeyPath<Action, TaskResult<V>>,
|
||||||
@@ -31,70 +99,52 @@ extension Effect {
|
|||||||
|
|
||||||
extension Effect where Action: ReceiveAction {
|
extension Effect where Action: ReceiveAction {
|
||||||
|
|
||||||
@inlinable
|
/// A convenience effect for sending an action that receives a task result from the given operation and then
|
||||||
public static func receive<T>(
|
/// transforming the output of the operation.
|
||||||
_ operation: @escaping @Sendable () async throws -> T,
|
///
|
||||||
transform: @escaping @Sendable (T) -> Action.ReceiveAction
|
/// ## Example
|
||||||
) -> Self {
|
///
|
||||||
.receive(operation: .case(AnyCasePath(unsafe: Action.receive), operation, transform))
|
/// ```swift
|
||||||
}
|
/// @Reducer
|
||||||
|
/// struct MyFeature {
|
||||||
|
/// ...
|
||||||
|
/// enum Action: ReceiveAction {
|
||||||
|
/// case receive(TaskResult<ReceiveAction>)
|
||||||
|
/// case task
|
||||||
|
///
|
||||||
|
/// @CasePathable
|
||||||
|
/// enum ReceiveAction {
|
||||||
|
/// case numberFact(String)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @Dependency(\.numberClient) var numberClient
|
||||||
|
///
|
||||||
|
/// var body: some Reducer<State, Action> {
|
||||||
|
/// Reduce { state, action in
|
||||||
|
/// switch action {
|
||||||
|
/// ...
|
||||||
|
/// case .task:
|
||||||
|
/// return .receive(\.numberFact) {
|
||||||
|
/// try await numberClient.numberFact()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - action: The action to embed the task result in.
|
||||||
|
/// - operation: The operation to call to create the task result.
|
||||||
@inlinable
|
@inlinable
|
||||||
public static func receive<T>(
|
public static func receive<T>(
|
||||||
_ toReceiveAction: CaseKeyPath<Action.ReceiveAction, T>,
|
_ toReceiveAction: CaseKeyPath<Action.ReceiveAction, T>,
|
||||||
_ operation: @escaping @Sendable () async throws -> T
|
_ operation: @escaping @Sendable () async throws -> T
|
||||||
) -> Self {
|
) -> Self {
|
||||||
return .receive(operation) {
|
self.receive(operation: .case(
|
||||||
AnyCasePath(toReceiveAction).embed($0)
|
AnyCasePath(toReceiveAction),
|
||||||
}
|
operation
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
struct ReceiveOperation<Action, Input, Result> {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let embed: @Sendable (TaskResult<Result>) -> Action
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let operation: @Sendable () async throws -> Input
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let transform: @Sendable (Input) -> Result
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
func callAsFunction(send: Send<Action>) async {
|
|
||||||
await send(embed(
|
|
||||||
TaskResult { try await operation() }
|
|
||||||
.map(transform)
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
static func `case`(
|
|
||||||
_ casePath: AnyCasePath<Action, TaskResult<Result>>,
|
|
||||||
_ operation: @escaping @Sendable () async throws -> Input,
|
|
||||||
_ transform: @escaping @Sendable (Input) -> Result
|
|
||||||
) -> Self {
|
|
||||||
.init(embed: { casePath.embed($0) }, operation: operation, transform: transform)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ReceiveOperation where Input == Result {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
init(
|
|
||||||
embed: @escaping @Sendable (TaskResult<Result>) -> Action,
|
|
||||||
operation: @escaping @Sendable () async throws -> Input
|
|
||||||
) {
|
|
||||||
self.init(embed: embed, operation: operation, transform: { $0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
static func `case`(
|
|
||||||
_ casePath: AnyCasePath<Action, TaskResult<Result>>,
|
|
||||||
_ operation: @escaping @Sendable () async throws -> Input
|
|
||||||
) -> Self {
|
|
||||||
.init(embed: { casePath.embed($0) }, operation: operation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
78
Sources/ComposableSubscriber/Internal/ReceiveOperation.swift
Normal file
78
Sources/ComposableSubscriber/Internal/ReceiveOperation.swift
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
// A container that holds onto the required data for embedding a task result into
|
||||||
|
// an action and optionally transforming the output.
|
||||||
|
@usableFromInline
|
||||||
|
struct ReceiveOperation<Action, Value, Result> {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let embed: @Sendable (TaskResult<Result>) -> Action
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let operation: @Sendable () async throws -> Value
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let transform: @Sendable (Value) -> Result
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
func callAsFunction(send: Send<Action>) async {
|
||||||
|
await send(embed(
|
||||||
|
TaskResult { try await operation() }
|
||||||
|
.map(transform)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
static func `case`(
|
||||||
|
_ casePath: AnyCasePath<Action, TaskResult<Result>>,
|
||||||
|
_ operation: @escaping @Sendable () async throws -> Value,
|
||||||
|
_ transform: @escaping @Sendable (Value) -> Result
|
||||||
|
) -> Self {
|
||||||
|
.init(embed: { casePath.embed($0) }, operation: operation, transform: transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
static func `case`(
|
||||||
|
_ casePath: AnyCasePath<Action, TaskResult<Result>>,
|
||||||
|
operation: @escaping @Sendable () async throws -> Value,
|
||||||
|
embedIn embedInCase: AnyCasePath<Result, Value>
|
||||||
|
) -> Self {
|
||||||
|
.case(casePath, operation) {
|
||||||
|
embedInCase.embed($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReceiveOperation where Value == Result {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(
|
||||||
|
embed: @escaping @Sendable (TaskResult<Result>) -> Action,
|
||||||
|
operation: @escaping @Sendable () async throws -> Value
|
||||||
|
) {
|
||||||
|
self.init(embed: embed, operation: operation, transform: { $0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
static func `case`(
|
||||||
|
_ casePath: AnyCasePath<Action, TaskResult<Result>>,
|
||||||
|
_ operation: @escaping @Sendable () async throws -> Value
|
||||||
|
) -> Self {
|
||||||
|
.init(embed: { casePath.embed($0) }, operation: operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReceiveOperation where Action: ReceiveAction, Result == Action.ReceiveAction {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
static func `case`(
|
||||||
|
_ embedInCase: AnyCasePath<Action.ReceiveAction, Value>,
|
||||||
|
_ operation: @escaping @Sendable () async throws -> Value
|
||||||
|
) -> Self {
|
||||||
|
.case(
|
||||||
|
AnyCasePath(unsafe: Action.receive),
|
||||||
|
operation: operation,
|
||||||
|
embedIn: embedInCase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,25 @@ import ComposableArchitecture
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
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> {
|
public enum OnFailAction<State, Action> {
|
||||||
|
|
||||||
|
/// Throw a runtime warning and optionally log the error.
|
||||||
case fail(prefix: String? = nil, log: (@Sendable (String) -> Void)? = nil)
|
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>)
|
case operation((inout State, Error) -> Effect<Action>)
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
func callAsFunction(state: inout State, error: Error) -> Effect<Action> {
|
func callAsFunction(state: inout State, error: Error) -> Effect<Action> {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .ignore:
|
||||||
|
return .none
|
||||||
case let .fail(prefix, log):
|
case let .fail(prefix, log):
|
||||||
if let prefix {
|
if let prefix {
|
||||||
return .fail(prefix: prefix, error: error, log: log)
|
return .fail(prefix: prefix, error: error, log: log)
|
||||||
@@ -17,6 +29,7 @@ public enum OnFailAction<State, Action> {
|
|||||||
}
|
}
|
||||||
case let .operation(handler):
|
case let .operation(handler):
|
||||||
return handler(&state, error)
|
return handler(&state, error)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
/// An action type that exposes a `receive` method that accepts task result, generally from
|
||||||
|
/// calling external dependencies.
|
||||||
|
///
|
||||||
|
/// This allows for multiple receive actions to be nested under
|
||||||
|
/// one single action that handle the `failure` and `success` cases more conveniently
|
||||||
|
/// by using some of the higher order reducers provided by this package.
|
||||||
public protocol ReceiveAction<ReceiveAction> {
|
public protocol ReceiveAction<ReceiveAction> {
|
||||||
|
|
||||||
|
/// The success cases.
|
||||||
associatedtype ReceiveAction
|
associatedtype ReceiveAction
|
||||||
|
|
||||||
|
/// 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 }
|
var result: TaskResult<ReceiveAction>? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
85
Sources/ComposableSubscriber/ReceiveReducer.swift
Normal file
85
Sources/ComposableSubscriber/ReceiveReducer.swift
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import ComposableArchitecture
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// A reducer that can handle `receive` actions when the action type implements the ``ReceiveAction`` protocol.
|
||||||
|
///
|
||||||
|
/// This allows you to handle the `success` and `failure` cases.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```swift
|
||||||
|
/// @Reducer
|
||||||
|
/// struct MyFeature {
|
||||||
|
/// ...
|
||||||
|
/// enum Action: ReceiveAction {
|
||||||
|
/// case receive(TaskResult<ReceiveAction>)
|
||||||
|
///
|
||||||
|
/// @CasePathable
|
||||||
|
/// enum ReceiveAction {
|
||||||
|
/// case numberFact(String)
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @Dependency(\.logger) var logger
|
||||||
|
///
|
||||||
|
/// public var body: some ReducerOf<Self> {
|
||||||
|
/// ReceiveReducer(onFail: .fail(logger: logger)) { state, action in
|
||||||
|
/// // Handle the success cases by switching on the receive action.
|
||||||
|
/// switch action {
|
||||||
|
/// case let .numberFact(fact):
|
||||||
|
/// state.numberFact = fact
|
||||||
|
/// return .none
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
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 let .success(value):
|
||||||
|
return onSuccess(&state, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
import OSLog
|
|
||||||
|
|
||||||
extension Reducer {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
|
||||||
set setAction: SetAction<State, Action, V>
|
|
||||||
) -> _OnReceiveReducer<Self, V> {
|
|
||||||
.init(
|
|
||||||
parent: self,
|
|
||||||
receiveAction: { AnyCasePath(toReceiveAction).extract(from: $0) },
|
|
||||||
setAction: setAction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
|
||||||
set setAction: @escaping (inout State, V) -> Effect<Action>
|
|
||||||
) -> _OnReceiveReducer<Self, V> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
set: .operation(f: setAction)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
|
||||||
set setAction: @escaping (inout State, V) -> Void
|
|
||||||
) -> _OnReceiveReducer<Self, V> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
set: .operation(f: { state, value in
|
|
||||||
setAction(&state, value)
|
|
||||||
return .none
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
|
||||||
set toStateKeyPath: WritableKeyPath<State, V>,
|
|
||||||
effect: Effect<Action> = .none
|
|
||||||
) -> _OnReceiveReducer<Self, V> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
set: .keyPath(toStateKeyPath, effect: effect)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, V>,
|
|
||||||
set toStateKeyPath: WritableKeyPath<State, V?>,
|
|
||||||
effect: Effect<Action> = .none
|
|
||||||
) -> _OnReceiveReducer<Self, V> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
set: .optionalKeyPath(toStateKeyPath, effect: effect)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
|
||||||
onFail: OnFailAction<State, Action>? = nil,
|
|
||||||
onSuccess setAction: SetAction<State, Action, V>
|
|
||||||
) -> _OnReceiveReducer<Self, TaskResult<V>> {
|
|
||||||
self.onReceive(action: toReceiveAction) { state, result in
|
|
||||||
switch result {
|
|
||||||
case let .failure(error):
|
|
||||||
if let onFail {
|
|
||||||
return onFail(state: &state, error: error)
|
|
||||||
}
|
|
||||||
return .none
|
|
||||||
case let .success(value):
|
|
||||||
return setAction(state: &state, value: value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
|
||||||
onSuccess setAction: @escaping (inout State, V) -> Void,
|
|
||||||
onFail: OnFailAction<State, Action>? = nil
|
|
||||||
) -> _OnReceiveReducer<Self, TaskResult<V>> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
onFail: onFail,
|
|
||||||
onSuccess: .operation(setAction)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
|
||||||
set toStateKeyPath: WritableKeyPath<State, V>,
|
|
||||||
onFail: OnFailAction<State, Action>? = nil,
|
|
||||||
effect: Effect<Action> = .none
|
|
||||||
) -> _OnReceiveReducer<Self, TaskResult<V>> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
onFail: onFail,
|
|
||||||
onSuccess: .keyPath(toStateKeyPath, effect: effect)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func onReceive<V>(
|
|
||||||
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
|
|
||||||
set toStateKeyPath: WritableKeyPath<State, V?>,
|
|
||||||
onFail: OnFailAction<State, Action>? = nil,
|
|
||||||
effect: Effect<Action> = .none
|
|
||||||
) -> _OnReceiveReducer<Self, TaskResult<V>> {
|
|
||||||
self.onReceive(
|
|
||||||
action: toReceiveAction,
|
|
||||||
onFail: onFail,
|
|
||||||
onSuccess: .optionalKeyPath(toStateKeyPath, effect: effect)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func receive<TriggerAction, Value>(
|
|
||||||
on triggerAction: CaseKeyPath<Action, TriggerAction>,
|
|
||||||
with receiveAction: CaseKeyPath<Action, TaskResult<Value>>,
|
|
||||||
result resultHandler: @escaping @Sendable () async throws -> Value
|
|
||||||
) -> _ReceiveOnTriggerReducer<Self, TriggerAction, Value> {
|
|
||||||
.init(
|
|
||||||
parent: self,
|
|
||||||
triggerAction: { AnyCasePath(triggerAction).extract(from: $0) },
|
|
||||||
toReceiveAction: { AnyCasePath(receiveAction).embed($0) },
|
|
||||||
resultHandler: resultHandler
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Reducer where Action: ReceiveAction {
|
|
||||||
@inlinable
|
|
||||||
public func receive<TriggerAction, Value>(
|
|
||||||
on triggerAction: CaseKeyPath<Action, TriggerAction>,
|
|
||||||
case embedCasePath: CaseKeyPath<Action.ReceiveAction, Value>,
|
|
||||||
result resultHandler: @escaping @Sendable () async throws -> Value
|
|
||||||
) -> _ReceiveOnTriggerReducer<Self, TriggerAction, Action.ReceiveAction> {
|
|
||||||
.init(
|
|
||||||
parent: self,
|
|
||||||
triggerAction: { AnyCasePath(triggerAction).extract(from: $0) },
|
|
||||||
toReceiveAction: { AnyCasePath(unsafe: Action.receive).embed($0) },
|
|
||||||
resultHandler: {
|
|
||||||
try await AnyCasePath(embedCasePath).embed(
|
|
||||||
resultHandler()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct _OnReceiveReducer<Parent: Reducer, Value>: Reducer {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let parent: Parent
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let receiveAction: (Parent.Action) -> Value?
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let setAction: SetAction<Parent.State, Parent.Action, Value>
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
init(
|
|
||||||
parent: Parent,
|
|
||||||
receiveAction: @escaping (Parent.Action) -> Value?,
|
|
||||||
setAction: SetAction<Parent.State, Parent.Action, Value>
|
|
||||||
) {
|
|
||||||
self.parent = parent
|
|
||||||
self.receiveAction = receiveAction
|
|
||||||
self.setAction = setAction
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func reduce(
|
|
||||||
into state: inout Parent.State,
|
|
||||||
action: Parent.Action
|
|
||||||
) -> Effect<Parent.Action> {
|
|
||||||
let baseEffects = parent.reduce(into: &state, action: action)
|
|
||||||
|
|
||||||
guard let value = receiveAction(action) else {
|
|
||||||
return baseEffects
|
|
||||||
}
|
|
||||||
|
|
||||||
return .merge(
|
|
||||||
baseEffects,
|
|
||||||
setAction(state: &state, value: value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct _ReceiveOnTriggerReducer<
|
|
||||||
Parent: Reducer,
|
|
||||||
TriggerAction,
|
|
||||||
Value
|
|
||||||
>: Reducer {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let parent: Parent
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let triggerAction: @Sendable (Parent.Action) -> TriggerAction?
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let toReceiveAction: @Sendable (TaskResult<Value>) -> Parent.Action
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let resultHandler: @Sendable () async throws -> Value
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
init(
|
|
||||||
parent: Parent,
|
|
||||||
triggerAction: @escaping @Sendable (Parent.Action) -> TriggerAction?,
|
|
||||||
toReceiveAction: @escaping @Sendable (TaskResult<Value>) -> Parent.Action,
|
|
||||||
resultHandler: @escaping @Sendable () async throws -> Value
|
|
||||||
) {
|
|
||||||
self.parent = parent
|
|
||||||
self.triggerAction = triggerAction
|
|
||||||
self.toReceiveAction = toReceiveAction
|
|
||||||
self.resultHandler = resultHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func reduce(
|
|
||||||
into state: inout Parent.State,
|
|
||||||
action: Parent.Action) -> Effect<Parent.Action>
|
|
||||||
{
|
|
||||||
let baseEffects = parent.reduce(into: &state, action: action)
|
|
||||||
|
|
||||||
guard triggerAction(action) != nil else {
|
|
||||||
return baseEffects
|
|
||||||
}
|
|
||||||
|
|
||||||
return .merge(
|
|
||||||
baseEffects,
|
|
||||||
.receive(operation: .init(embed: toReceiveAction, operation: resultHandler))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
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>? = nil,
|
|
||||||
onSuccess: @escaping (inout State, Action.ReceiveAction) -> Effect<Action>
|
|
||||||
) {
|
|
||||||
self.init(
|
|
||||||
internal: {
|
|
||||||
AnyCasePath(unsafe: Action.receive).extract(from: $0)
|
|
||||||
},
|
|
||||||
onFail: onFail,
|
|
||||||
onSuccess: onSuccess
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
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):
|
|
||||||
guard let onFail else { return .none }
|
|
||||||
return onFail(state: &state, error: error)
|
|
||||||
case let .success(value):
|
|
||||||
return onSuccess(&state, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
149
Sources/ComposableSubscriber/Reducer+receive.swift
Normal file
149
Sources/ComposableSubscriber/Reducer+receive.swift
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import ComposableArchitecture
|
||||||
|
|
||||||
|
extension Reducer {
|
||||||
|
|
||||||
|
/// A higher order reducer that sends a receive action on a trigger action.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```swift
|
||||||
|
///
|
||||||
|
/// @Reducer
|
||||||
|
/// struct MyFeature {
|
||||||
|
/// ...
|
||||||
|
///
|
||||||
|
/// enum Action {
|
||||||
|
/// case receive(TaskResult<Int>)
|
||||||
|
/// case task
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @Dependency(\.numberClient) var numberClient
|
||||||
|
///
|
||||||
|
/// var body: some ReducerOf<Self> {
|
||||||
|
/// Reduce { state, action in
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// .receive(on: \.task, with: \.receive) {
|
||||||
|
/// try await numberClient.fetchCurrentNumber()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - triggerAction: The action that triggers calling the result handler.
|
||||||
|
/// - receiveAction: The action that receives the task result.
|
||||||
|
/// - resultHandler: The operations that is called that returns the value to be received.
|
||||||
|
///
|
||||||
|
@inlinable
|
||||||
|
public func receive<TriggerAction, Value>(
|
||||||
|
on triggerAction: CaseKeyPath<Action, TriggerAction>,
|
||||||
|
with receiveAction: CaseKeyPath<Action, TaskResult<Value>>,
|
||||||
|
result resultHandler: @escaping @Sendable () async throws -> Value
|
||||||
|
) -> _ReceiveOnTriggerReducer<Self, TriggerAction, Value, Value> {
|
||||||
|
.init(
|
||||||
|
parent: self,
|
||||||
|
triggerAction: { AnyCasePath(triggerAction).extract(from: $0) },
|
||||||
|
receiveOperation: .case(AnyCasePath(receiveAction), resultHandler)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Reducer where Action: ReceiveAction {
|
||||||
|
/// A higher order reducer that sends a receive action on a trigger action.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```swift
|
||||||
|
///
|
||||||
|
/// @Reducer
|
||||||
|
/// struct MyFeature {
|
||||||
|
/// ...
|
||||||
|
///
|
||||||
|
/// enum Action: ReceiveAction {
|
||||||
|
/// case receive(TaskResult<ReceiveAction>)
|
||||||
|
/// case task
|
||||||
|
///
|
||||||
|
/// @CasePathable
|
||||||
|
/// enum ReceiveAction {
|
||||||
|
/// case currentNumber(Int)
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @Dependency(\.numberClient) var numberClient
|
||||||
|
///
|
||||||
|
/// var body: some ReducerOf<Self> {
|
||||||
|
/// Reduce { state, action in
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// .receive(on: \.task, case: \.currentNumber) {
|
||||||
|
/// try await numberClient.fetchCurrentNumber()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - triggerAction: The action that triggers calling the result handler.
|
||||||
|
/// - embedCasePath: The case path to embed the result into.
|
||||||
|
/// - resultHandler: The operations that is called that returns the value to be received.
|
||||||
|
@inlinable
|
||||||
|
public func receive<TriggerAction, Value>(
|
||||||
|
on triggerAction: CaseKeyPath<Action, TriggerAction>,
|
||||||
|
case embedCasePath: CaseKeyPath<Action.ReceiveAction, Value>,
|
||||||
|
result resultHandler: @escaping @Sendable () async throws -> Value
|
||||||
|
) -> _ReceiveOnTriggerReducer<Self, TriggerAction, Value, Action.ReceiveAction> {
|
||||||
|
.init(
|
||||||
|
parent: self,
|
||||||
|
triggerAction: { AnyCasePath(triggerAction).extract(from: $0) },
|
||||||
|
receiveOperation: .case(AnyCasePath(embedCasePath), resultHandler)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _ReceiveOnTriggerReducer<
|
||||||
|
Parent: Reducer,
|
||||||
|
TriggerAction,
|
||||||
|
Value,
|
||||||
|
Result
|
||||||
|
>: Reducer {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let parent: Parent
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let triggerAction: @Sendable (Parent.Action) -> TriggerAction?
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let receiveOperation: ReceiveOperation<Parent.Action, Value, Result>
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(
|
||||||
|
parent: Parent,
|
||||||
|
triggerAction: @escaping @Sendable (Parent.Action) -> TriggerAction?,
|
||||||
|
receiveOperation: ReceiveOperation<Parent.Action, Value, Result>
|
||||||
|
) {
|
||||||
|
self.parent = parent
|
||||||
|
self.triggerAction = triggerAction
|
||||||
|
self.receiveOperation = receiveOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func reduce(
|
||||||
|
into state: inout Parent.State,
|
||||||
|
action: Parent.Action) -> Effect<Parent.Action>
|
||||||
|
{
|
||||||
|
let baseEffects = parent.reduce(into: &state, action: action)
|
||||||
|
|
||||||
|
guard triggerAction(action) != nil else {
|
||||||
|
return baseEffects
|
||||||
|
}
|
||||||
|
|
||||||
|
return .merge(
|
||||||
|
baseEffects,
|
||||||
|
.receive(operation: receiveOperation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -482,7 +482,7 @@ extension Reducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
enum Operation<Action, Value> {
|
enum SubscribeOperation<Action, Value> {
|
||||||
case action(action: AnyCasePath<Action, Value>, animation: Animation?)
|
case action(action: AnyCasePath<Action, Value>, animation: Animation?)
|
||||||
case operation(f: (_ send: Send<Action>, Value) async throws -> Void)
|
case operation(f: (_ send: Send<Action>, Value) async throws -> Void)
|
||||||
}
|
}
|
||||||
@@ -513,7 +513,7 @@ public struct _SubscribeReducer<Parent: Reducer, TriggerAction, StreamElement, V
|
|||||||
let stream: Stream<Parent.State, StreamElement>
|
let stream: Stream<Parent.State, StreamElement>
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let operation: Operation<Parent.Action, Value>
|
let operation: SubscribeOperation<Parent.Action, Value>
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let transform: (StreamElement) -> Value
|
let transform: (StreamElement) -> Value
|
||||||
@@ -523,7 +523,7 @@ public struct _SubscribeReducer<Parent: Reducer, TriggerAction, StreamElement, V
|
|||||||
parent: Parent,
|
parent: Parent,
|
||||||
on triggerAction: CaseKeyPath<Parent.Action, TriggerAction>,
|
on triggerAction: CaseKeyPath<Parent.Action, TriggerAction>,
|
||||||
to stream: Stream<Parent.State, StreamElement>,
|
to stream: Stream<Parent.State, StreamElement>,
|
||||||
with operation: Operation<Parent.Action, Value>,
|
with operation: SubscribeOperation<Parent.Action, Value>,
|
||||||
transform: @escaping @Sendable (StreamElement) -> Value
|
transform: @escaping @Sendable (StreamElement) -> Value
|
||||||
) {
|
) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ struct NumberClient {
|
|||||||
|
|
||||||
func currentNumber(fail: Bool = false) async throws -> Int {
|
func currentNumber(fail: Bool = false) async throws -> Int {
|
||||||
if fail {
|
if fail {
|
||||||
struct CurrentNumberError: Error { }
|
|
||||||
throw CurrentNumberError()
|
throw CurrentNumberError()
|
||||||
}
|
}
|
||||||
return try await currentNumber()
|
return try await currentNumber()
|
||||||
@@ -18,6 +17,8 @@ struct NumberClient {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CurrentNumberError: Error { }
|
||||||
|
|
||||||
extension NumberClient: TestDependencyKey {
|
extension NumberClient: TestDependencyKey {
|
||||||
|
|
||||||
static var live: NumberClient {
|
static var live: NumberClient {
|
||||||
@@ -68,9 +69,16 @@ struct ReducerWithArg {
|
|||||||
@Dependency(\.numberClient) var numberClient
|
@Dependency(\.numberClient) var numberClient
|
||||||
|
|
||||||
var body: some Reducer<State, Action> {
|
var body: some Reducer<State, Action> {
|
||||||
|
Reduce { state, action in
|
||||||
|
switch action {
|
||||||
|
case let .receive(currentNumber):
|
||||||
|
state.currentNumber = currentNumber
|
||||||
|
return .none
|
||||||
|
|
||||||
EmptyReducer()
|
case .task:
|
||||||
.onReceive(action: \.receive, set: \.currentNumber)
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
.subscribe(
|
.subscribe(
|
||||||
to: numberClient.numberStreamWithArg,
|
to: numberClient.numberStreamWithArg,
|
||||||
using: \.number,
|
using: \.number,
|
||||||
@@ -89,8 +97,16 @@ struct ReducerWithTransform {
|
|||||||
@Dependency(\.numberClient) var numberClient
|
@Dependency(\.numberClient) var numberClient
|
||||||
|
|
||||||
var body: some Reducer<State, Action> {
|
var body: some Reducer<State, Action> {
|
||||||
EmptyReducer()
|
Reduce { state, action in
|
||||||
.onReceive(action: \.receive, set: \.currentNumber)
|
switch action {
|
||||||
|
case let .receive(currentNumber):
|
||||||
|
state.currentNumber = currentNumber
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .task:
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
.subscribe(
|
.subscribe(
|
||||||
to: numberClient.numberStreamWithArg,
|
to: numberClient.numberStreamWithArg,
|
||||||
using: \.number,
|
using: \.number,
|
||||||
@@ -134,6 +150,44 @@ struct ReducerWithReceiveAction {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Reducer
|
||||||
|
struct FailingReducer {
|
||||||
|
|
||||||
|
struct State: Equatable {
|
||||||
|
static func == (lhs: FailingReducer.State, rhs: FailingReducer.State) -> Bool {
|
||||||
|
lhs.error?.localizedDescription == rhs.error?.localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
var error: Error?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action: ReceiveAction {
|
||||||
|
|
||||||
|
case receive(TaskResult<ReceiveAction>)
|
||||||
|
case task
|
||||||
|
|
||||||
|
@CasePathable
|
||||||
|
enum ReceiveAction {
|
||||||
|
case currentNumber(Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dependency(\.numberClient) var numberClient
|
||||||
|
|
||||||
|
public var body: some Reducer<State, Action> {
|
||||||
|
ReceiveReducer(onFail: .set(keyPath: \.error)) { state, action in
|
||||||
|
switch action {
|
||||||
|
case .currentNumber(_):
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.receive(on: \.task, case: \.currentNumber) {
|
||||||
|
try await numberClient.currentNumber(fail: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
final class TCAExtrasTests: XCTestCase {
|
final class TCAExtrasTests: XCTestCase {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -190,4 +244,24 @@ final class TCAExtrasTests: XCTestCase {
|
|||||||
await store.finish()
|
await store.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testFailEffect() async throws {
|
||||||
|
|
||||||
|
let store = TestStore(
|
||||||
|
initialState: FailingReducer.State(),
|
||||||
|
reducer: FailingReducer.init
|
||||||
|
) {
|
||||||
|
$0.numberClient = .live
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = await store.send(.task)
|
||||||
|
await store.receive(\.receive.failure) {
|
||||||
|
$0.error = CurrentNumberError()
|
||||||
|
}
|
||||||
|
|
||||||
|
await task.cancel()
|
||||||
|
await store.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user