diff --git a/Sources/ComposableSubscriber/Effect+receive.swift b/Sources/ComposableSubscriber/Effect+receive.swift index 8ff8022..8c5e2fc 100644 --- a/Sources/ComposableSubscriber/Effect+receive.swift +++ b/Sources/ComposableSubscriber/Effect+receive.swift @@ -1,33 +1,66 @@ import ComposableArchitecture import OSLog -extension Effect where Action: ReceiveAction { - - public static func receive( - _ operation: @escaping () async throws -> Action.ReceiveAction +extension Effect { + + @usableFromInline + static func receive( + _ casePath: AnyCasePath>, + _ operation: @escaping @Sendable () async throws -> T ) -> Self { .run { send in - await send(.receive( + await send(casePath.embed( TaskResult { try await operation() } )) } } - - public static func receive( - _ operation: @escaping () async throws -> T, - transform: @escaping (T) -> Action.ReceiveAction + + @usableFromInline + static func receive( + _ casePath: AnyCasePath>, + _ operation: @escaping @Sendable () async throws -> T, + _ transform: @escaping @Sendable (T) -> V ) -> Self { .run { send in - await send(.receive( + await send(casePath.embed( TaskResult { try await operation() } .map(transform) )) } } + @inlinable + public static func receive( + action toResult: CaseKeyPath>, + operation: @escaping @Sendable () async throws -> T + ) -> Self { + .receive(AnyCasePath(toResult), operation) + } + + @inlinable + public static func receive( + action toResult: CaseKeyPath>, + operation: @escaping @Sendable () async throws -> T, + transform: @escaping @Sendable (T) -> V + ) -> Self { + .receive(AnyCasePath(toResult), operation, transform) + } +} + +extension Effect where Action: ReceiveAction { + + @inlinable + public static func receive( + _ operation: @escaping @Sendable () async throws -> T, + transform: @escaping @Sendable (T) -> Action.ReceiveAction + ) -> Self { + .receive(AnyCasePath(unsafe: Action.receive), operation, transform) + } + + @inlinable public static func receive( _ toReceiveAction: CaseKeyPath, - _ operation: @escaping () async throws -> T + _ operation: @escaping @Sendable () async throws -> T ) -> Self { return .receive(operation) { AnyCasePath(toReceiveAction).embed($0) diff --git a/Sources/ComposableSubscriber/Internal/SetAction.swift b/Sources/ComposableSubscriber/Internal/SetAction.swift new file mode 100644 index 0000000..54e18dc --- /dev/null +++ b/Sources/ComposableSubscriber/Internal/SetAction.swift @@ -0,0 +1,32 @@ +import ComposableArchitecture +import Foundation + +@usableFromInline +enum SetAction { + case operation(f: (inout State, Value) -> Effect) + case keyPath(WritableKeyPath, effect: Effect) + case optionalKeyPath(WritableKeyPath, effect: Effect) + + @usableFromInline + func callAsFunction(state: inout State, value: Value) -> Effect { + 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 + }) + } + +} diff --git a/Sources/ComposableSubscriber/OnFailAction.swift b/Sources/ComposableSubscriber/OnFailAction.swift index 68963d0..49ab9f8 100644 --- a/Sources/ComposableSubscriber/OnFailAction.swift +++ b/Sources/ComposableSubscriber/OnFailAction.swift @@ -4,13 +4,7 @@ import OSLog public enum OnFailAction { case fail(prefix: String? = nil, log: ((String) -> Void)? = nil) - case handle((inout State, Error) -> Void) - - @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)") }) - } + case operation((inout State, Error) -> Effect) @usableFromInline func callAsFunction(state: inout State, error: Error) -> Effect { @@ -21,10 +15,45 @@ public enum OnFailAction { } else { return .fail(error: error, log: log) } - case let .handle(handler): - handler(&state, error) - return .none + 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 + ) -> Self { + .operation( + SetAction.operation(f: operation).callAsFunction(state:value:) + ) + } + + @inlinable + public static func set( + keyPath: WritableKeyPath, + effect: Effect = .none + ) -> Self { + .operation( + SetAction.optionalKeyPath( + keyPath, + effect: effect + ).callAsFunction(state:value:) + ) + } } diff --git a/Sources/ComposableSubscriber/ReceiveReducer.swift b/Sources/ComposableSubscriber/Reducer+onReceive.swift similarity index 72% rename from Sources/ComposableSubscriber/ReceiveReducer.swift rename to Sources/ComposableSubscriber/Reducer+onReceive.swift index 2fe3ded..5df6b2c 100644 --- a/Sources/ComposableSubscriber/ReceiveReducer.swift +++ b/Sources/ComposableSubscriber/Reducer+onReceive.swift @@ -3,55 +3,73 @@ import OSLog extension Reducer { - @inlinable - public func onReceive( + @usableFromInline + func onReceive( action toReceiveAction: CaseKeyPath, - set setAction: @escaping (inout State, V) -> Effect - ) -> _ReceiveReducer { + set setAction: SetAction + ) -> _OnReceiveReducer { .init( parent: self, receiveAction: { AnyCasePath(toReceiveAction).extract(from: $0) }, setAction: setAction ) } + + @inlinable + public func onReceive( + action toReceiveAction: CaseKeyPath, + set setAction: @escaping (inout State, V) -> Effect + ) -> _OnReceiveReducer { + self.onReceive( + action: toReceiveAction, + set: .operation(f: setAction) + ) + } @inlinable public func onReceive( action toReceiveAction: CaseKeyPath, set setAction: @escaping (inout State, V) -> Void - ) -> _ReceiveReducer { - .init( - parent: self, - receiveAction: { AnyCasePath(toReceiveAction).extract(from: $0) }, - setAction: { state, value in + ) -> _OnReceiveReducer { + self.onReceive( + action: toReceiveAction, + set: .operation(f: { state, value in setAction(&state, value) return .none - } + }) ) } @inlinable public func onReceive( action toReceiveAction: CaseKeyPath, - set toStateKeyPath: WritableKeyPath - ) -> _ReceiveReducer { - self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:)) + set toStateKeyPath: WritableKeyPath, + effect: Effect = .none + ) -> _OnReceiveReducer { + self.onReceive( + action: toReceiveAction, + set: .keyPath(toStateKeyPath, effect: effect) + ) } @inlinable public func onReceive( action toReceiveAction: CaseKeyPath, - set toStateKeyPath: WritableKeyPath - ) -> _ReceiveReducer { - self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:)) + set toStateKeyPath: WritableKeyPath, + effect: Effect = .none + ) -> _OnReceiveReducer { + self.onReceive( + action: toReceiveAction, + set: .optionalKeyPath(toStateKeyPath, effect: effect) + ) } - @inlinable - public func onReceive( + @usableFromInline + func onReceive( action toReceiveAction: CaseKeyPath>, onFail: OnFailAction? = nil, - onSuccess setAction: @escaping (inout State, V) -> Void - ) -> _ReceiveReducer> { + onSuccess setAction: SetAction + ) -> _OnReceiveReducer> { self.onReceive(action: toReceiveAction) { state, result in switch result { case let .failure(error): @@ -60,50 +78,50 @@ extension Reducer { } return .none case let .success(value): - setAction(&state, value) - return .none + return setAction(state: &state, value: value) } } } + @inlinable + public func onReceive( + action toReceiveAction: CaseKeyPath>, + onSuccess setAction: @escaping (inout State, V) -> Void, + onFail: OnFailAction? = nil + ) -> _OnReceiveReducer> { + self.onReceive( + action: toReceiveAction, + onFail: onFail, + onSuccess: .operation(setAction) + ) + } + @inlinable public func onReceive( action toReceiveAction: CaseKeyPath>, set toStateKeyPath: WritableKeyPath, - onFail: OnFailAction? = nil - ) -> _ReceiveReducer> { - 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): - toStateKeyPath(root: &state, value: value) - return .none - } - } + onFail: OnFailAction? = nil, + effect: Effect = .none + ) -> _OnReceiveReducer> { + self.onReceive( + action: toReceiveAction, + onFail: onFail, + onSuccess: .keyPath(toStateKeyPath, effect: effect) + ) } @inlinable public func onReceive( action toReceiveAction: CaseKeyPath>, set toStateKeyPath: WritableKeyPath, - onFail: OnFailAction? = nil - ) -> _ReceiveReducer> { - 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): - toStateKeyPath(root: &state, value: value) - return .none - } - } + onFail: OnFailAction? = nil, + effect: Effect = .none + ) -> _OnReceiveReducer> { + self.onReceive( + action: toReceiveAction, + onFail: onFail, + onSuccess: .optionalKeyPath(toStateKeyPath, effect: effect) + ) } @inlinable @@ -126,7 +144,7 @@ extension Reducer where Action: ReceiveAction { @inlinable public func receive( on triggerAction: CaseKeyPath, - with embedCasePath: CaseKeyPath, + case embedCasePath: CaseKeyPath, result resultHandler: @escaping @Sendable () async throws -> Value ) -> _ReceiveOnTriggerReducer { .init( @@ -142,16 +160,7 @@ extension Reducer where Action: ReceiveAction { } } -extension WritableKeyPath { - - @usableFromInline - func callAsFunction(root: inout Root, value: Value) { - root[keyPath: self] = value - } - -} - -public struct _ReceiveReducer: Reducer { +public struct _OnReceiveReducer: Reducer { @usableFromInline let parent: Parent @@ -160,13 +169,13 @@ public struct _ReceiveReducer: Reducer { let receiveAction: (Parent.Action) -> Value? @usableFromInline - let setAction: (inout Parent.State, Value) -> Effect + let setAction: SetAction @usableFromInline init( parent: Parent, receiveAction: @escaping (Parent.Action) -> Value?, - setAction: @escaping (inout Parent.State, Value) -> Effect + setAction: SetAction ) { self.parent = parent self.receiveAction = receiveAction @@ -182,7 +191,7 @@ public struct _ReceiveReducer: Reducer { var setEffects = Effect.none if let value = receiveAction(action) { - setEffects = setAction(&state, value) + setEffects = setAction(state: &state, value: value) } return .merge(baseEffects, setEffects) diff --git a/Sources/ComposableSubscriber/SubscriberReducer.swift b/Sources/ComposableSubscriber/Reducer+subscribe.swift similarity index 98% rename from Sources/ComposableSubscriber/SubscriberReducer.swift rename to Sources/ComposableSubscriber/Reducer+subscribe.swift index 2f08693..3e1b643 100644 --- a/Sources/ComposableSubscriber/SubscriberReducer.swift +++ b/Sources/ComposableSubscriber/Reducer+subscribe.swift @@ -286,9 +286,14 @@ extension Reducer { /// return .none /// } /// } - /// .subscribe(using: \.number, to: numberFactStream, on: \.task, with: \.receive) { numberFact in - /// "\(numberFact) Appended with my custom transformation." - /// } + /// .subscribe( + /// to: numberFactStream, + /// using: \.number, + /// on: \.task, + /// with: \.receiveNumberFact + /// ) { numberFact in + /// "\(numberFact) Appended with my custom transformation." + /// } /// } /// } /// ``` @@ -513,6 +518,7 @@ public struct _SubscribeReducer Value + @usableFromInline init( parent: Parent, on triggerAction: CaseKeyPath, diff --git a/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift b/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift index 53ce0b5..aeb6600 100644 --- a/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift +++ b/Tests/swift-composable-subscriberTests/TCAExtrasTests.swift @@ -127,29 +127,16 @@ struct ReducerWithReceiveAction { return .none } } - .receive(on: \.task, with: \.currentNumber) { + .receive(on: \.task, case: \.currentNumber) { try await numberClient.currentNumber() } - -// Reduce { state, action in -// switch action { -// -// case .receive: -// return .none -// -// case .task: -// return .receive(\.currentNumber) { -// try await numberClient.currentNumber() -// } -// } -// } } } -@MainActor final class TCAExtrasTests: XCTestCase { + @MainActor func testSubscribeWithArg() async throws { let store = TestStore( initialState: ReducerWithArg.State(number: 19), @@ -167,6 +154,7 @@ final class TCAExtrasTests: XCTestCase { await store.finish() } + @MainActor func testSubscribeWithArgAndTransform() async throws { let store = TestStore( initialState: ReducerWithTransform.State(number: 10), @@ -184,6 +172,7 @@ final class TCAExtrasTests: XCTestCase { await store.finish() } + @MainActor func testReceiveAction() async throws { let store = TestStore( initialState: ReducerWithReceiveAction.State(number: 19), @@ -193,7 +182,7 @@ final class TCAExtrasTests: XCTestCase { } let task = await store.send(.task) - await store.receive(\.receive) { + await store.receive(\.receive.success.currentNumber) { $0.currentNumber = 69420 }