feat: Adds documentation and removes some unused code.

This commit is contained in:
2024-05-31 16:24:26 -04:00
parent dac5d932dd
commit ab915f88db
9 changed files with 542 additions and 382 deletions

View File

@@ -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
public static func receive<T>(
action toResult: CaseKeyPath<Action, TaskResult<T>>,
@@ -19,6 +51,42 @@ extension Effect {
.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
public static func receive<T, V>(
action toResult: CaseKeyPath<Action, TaskResult<V>>,
@@ -31,70 +99,52 @@ extension Effect {
extension Effect where Action: ReceiveAction {
@inlinable
public static func receive<T>(
_ operation: @escaping @Sendable () async throws -> T,
transform: @escaping @Sendable (T) -> Action.ReceiveAction
) -> Self {
.receive(operation: .case(AnyCasePath(unsafe: Action.receive), operation, transform))
}
/// 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: 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
public static func receive<T>(
_ toReceiveAction: CaseKeyPath<Action.ReceiveAction, T>,
_ operation: @escaping @Sendable () async throws -> T
) -> Self {
return .receive(operation) {
AnyCasePath(toReceiveAction).embed($0)
}
}
}
@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)
self.receive(operation: .case(
AnyCasePath(toReceiveAction),
operation
))
}
@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)
}
}

View 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
)
}
}

View File

@@ -2,13 +2,25 @@ 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)
@@ -17,6 +29,7 @@ public enum OnFailAction<State, Action> {
}
case let .operation(handler):
return handler(&state, error)
}
}

View File

@@ -1,9 +1,20 @@
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> {
/// The success cases.
associatedtype 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 }
}

View 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)
}
}
}

View File

@@ -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)
}
}
}

View 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)
)
}
}

View File

@@ -482,7 +482,7 @@ extension Reducer {
}
@usableFromInline
enum Operation<Action, Value> {
enum SubscribeOperation<Action, Value> {
case action(action: AnyCasePath<Action, Value>, animation: Animation?)
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>
@usableFromInline
let operation: Operation<Parent.Action, Value>
let operation: SubscribeOperation<Parent.Action, Value>
@usableFromInline
let transform: (StreamElement) -> Value
@@ -523,7 +523,7 @@ public struct _SubscribeReducer<Parent: Reducer, TriggerAction, StreamElement, V
parent: Parent,
on triggerAction: CaseKeyPath<Parent.Action, TriggerAction>,
to stream: Stream<Parent.State, StreamElement>,
with operation: Operation<Parent.Action, Value>,
with operation: SubscribeOperation<Parent.Action, Value>,
transform: @escaping @Sendable (StreamElement) -> Value
) {
self.parent = parent

View File

@@ -10,7 +10,6 @@ struct NumberClient {
func currentNumber(fail: Bool = false) async throws -> Int {
if fail {
struct CurrentNumberError: Error { }
throw CurrentNumberError()
}
return try await currentNumber()
@@ -18,6 +17,8 @@ struct NumberClient {
}
struct CurrentNumberError: Error { }
extension NumberClient: TestDependencyKey {
static var live: NumberClient {
@@ -68,9 +69,16 @@ struct ReducerWithArg {
@Dependency(\.numberClient) var numberClient
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .receive(currentNumber):
state.currentNumber = currentNumber
return .none
EmptyReducer()
.onReceive(action: \.receive, set: \.currentNumber)
case .task:
return .none
}
}
.subscribe(
to: numberClient.numberStreamWithArg,
using: \.number,
@@ -89,8 +97,16 @@ struct ReducerWithTransform {
@Dependency(\.numberClient) var numberClient
var body: some Reducer<State, Action> {
EmptyReducer()
.onReceive(action: \.receive, set: \.currentNumber)
Reduce { state, action in
switch action {
case let .receive(currentNumber):
state.currentNumber = currentNumber
return .none
case .task:
return .none
}
}
.subscribe(
to: numberClient.numberStreamWithArg,
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 {
@MainActor
@@ -190,4 +244,24 @@ final class TCAExtrasTests: XCTestCase {
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()
}
}