From dac5d932dd59435c69ceb70332c3dd063bcabf5e Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 30 May 2024 22:54:58 -0400 Subject: [PATCH] wip --- .../ComposableSubscriber/Effect+fail.swift | 169 ++++++++++++++++-- .../ComposableSubscriber/Effect+receive.swift | 78 +++++--- .../Internal/SetAction.swift | 17 ++ .../ComposableSubscriber/OnFailAction.swift | 2 +- .../Reducer+onReceive.swift | 24 ++- 5 files changed, 238 insertions(+), 52 deletions(-) diff --git a/Sources/ComposableSubscriber/Effect+fail.swift b/Sources/ComposableSubscriber/Effect+fail.swift index 12d2b68..4522dfb 100644 --- a/Sources/ComposableSubscriber/Effect+fail.swift +++ b/Sources/ComposableSubscriber/Effect+fail.swift @@ -2,40 +2,181 @@ import ComposableArchitecture import OSLog extension Effect { - + /// An effect that throws a runtime warning and optionally logs an error message. + /// + /// This effect uses `XCTFail` to throw a runtime warning and will also log the message + /// if a logger is supplied. + /// + /// ## Example + /// + ///```swift + /// @Reducer + /// struct MyFeature { + /// ... + /// enum Action { + /// case receive(TaskResult) + /// ... + /// } + /// + /// @Dependency(\.logger) var logger + /// + /// var body: some ReducerOf { + /// Reduce { state, action in + /// switch action { + /// case .receive(.failure(_)): + /// return .fail("Failed retreiving number fact.", log: { logger.debug("\($0)") }) + /// ... + /// + /// } + /// } + /// } + /// } + ///``` + /// + /// - Parameters: + /// - message: The message for the failure reason. + /// - logger: An optional logger to use to log the message. + public static func fail( + _ message: String, + log: (@Sendable (String) -> Void)? = nil + ) -> Self { + XCTFail("\(message)") + log?(message) + return .none + } + + /// An effect that throws a runtime warning and optionally logs an error message. + /// + /// This effect uses `XCTFail` to throw a runtime warning and will also log the message + /// if a logger is supplied. + /// + /// ## Example + /// + ///```swift + /// @Reducer + /// struct MyFeature { + /// ... + /// enum Action { + /// case receive(TaskResult) + /// ... + /// } + /// + /// @Dependency(\.logger) var logger + /// + /// var body: some ReducerOf { + /// Reduce { state, action in + /// switch action { + /// case .receive(.failure(_)): + /// return .fail("Failed retreiving number fact.", logger: logger) + /// ... + /// + /// } + /// } + /// } + /// } + ///``` + /// + /// - Parameters: + /// - message: The message for the failure reason. + /// - logger: An optional logger to use to log the message. @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) public static func fail( _ message: String, logger: Logger? = nil ) -> Self { - XCTFail("\(message)") - logger?.error("\(message)") - return .none + .fail(message, log: { logger?.error("\($0)") }) } + + /// An effect that throws a runtime warning and optionally logs an error message. + /// + /// + /// This effect uses `XCTFail` to throw a runtime warning and will also log the message + /// if a logger is supplied. + /// + /// ## Example + /// + ///```swift + /// @Reducer + /// struct MyFeature { + /// ... + /// enum Action { + /// case receive(TaskResult) + /// ... + /// } + /// + /// @Dependency(\.logger) var logger + /// + /// var body: some ReducerOf { + /// Reduce { state, action in + /// switch action { + /// case let .receive(.failure(error)): + /// return .fail(error: error, logger: logger) + /// ... + /// + /// } + /// } + /// } + /// } + ///``` + /// + /// - Parameters: + /// - prefix: The prefix for the error message for the failure. + /// - error: The error for the failure.. + /// - logger: A logger to use to log the message. @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) public static func fail( prefix: String = "Failed error:", error: any Error, logger: Logger ) -> Self { - return .fail(prefix: prefix, error: error, log: { logger.error("\($0)") }) + .fail(prefix: prefix, error: error, log: { logger.error("\($0)") }) } - - /// An effect that throws a runtime warning and optionally logs an error message. + + /// An effect that throws a runtime warning and optionally logs an error message. + /// + /// + /// This effect uses `XCTFail` to throw a runtime warning and will also log the message + /// if a logger is supplied. + /// + /// ## Example + /// + ///```swift + /// @Reducer + /// struct MyFeature { + /// ... + /// enum Action { + /// case receive(TaskResult) + /// ... + /// } + /// + /// @Dependency(\.logger) var logger + /// + /// var body: some ReducerOf { + /// Reduce { state, action in + /// switch action { + /// case .receive(.failure(_)): + /// return .fail("Failed retreiving number fact.", log: { logger.debug("\($0)") }) + /// ... + /// + /// } + /// } + /// } + /// } + ///``` + /// + /// - Parameters: + /// - prefix: The prefix for the error message for the failure. + /// - error: The error for the failure.. + /// - log: A log handler to use to log the message. public static func fail( prefix: String = "Failed error:", error: any Error, - log: ((String) -> Void)? = nil + log: (@Sendable (String) -> Void)? = nil ) -> Self { - let message = "\(prefix) \(error.localizedDescription)" - XCTFail("\(message)") - log?("\(message)") - return .none + return .fail("\(prefix) \(error.localizedDescription)", log: log) } - - } diff --git a/Sources/ComposableSubscriber/Effect+receive.swift b/Sources/ComposableSubscriber/Effect+receive.swift index 8c5e2fc..346f787 100644 --- a/Sources/ComposableSubscriber/Effect+receive.swift +++ b/Sources/ComposableSubscriber/Effect+receive.swift @@ -1,31 +1,13 @@ import ComposableArchitecture -import OSLog extension Effect { @usableFromInline - static func receive( - _ casePath: AnyCasePath>, - _ operation: @escaping @Sendable () async throws -> T + static func receive( + operation: ReceiveOperation ) -> Self { .run { send in - await send(casePath.embed( - TaskResult { try await operation() } - )) - } - } - - @usableFromInline - static func receive( - _ casePath: AnyCasePath>, - _ operation: @escaping @Sendable () async throws -> T, - _ transform: @escaping @Sendable (T) -> V - ) -> Self { - .run { send in - await send(casePath.embed( - TaskResult { try await operation() } - .map(transform) - )) + await operation(send: send) } } @@ -34,7 +16,7 @@ extension Effect { action toResult: CaseKeyPath>, operation: @escaping @Sendable () async throws -> T ) -> Self { - .receive(AnyCasePath(toResult), operation) + .receive(operation: .case(AnyCasePath(toResult), operation)) } @inlinable @@ -43,7 +25,7 @@ extension Effect { operation: @escaping @Sendable () async throws -> T, transform: @escaping @Sendable (T) -> V ) -> Self { - .receive(AnyCasePath(toResult), operation, transform) + .receive(operation: .case(AnyCasePath(toResult), operation, transform)) } } @@ -54,7 +36,7 @@ extension Effect where Action: ReceiveAction { _ operation: @escaping @Sendable () async throws -> T, transform: @escaping @Sendable (T) -> Action.ReceiveAction ) -> Self { - .receive(AnyCasePath(unsafe: Action.receive), operation, transform) + .receive(operation: .case(AnyCasePath(unsafe: Action.receive), operation, transform)) } @inlinable @@ -68,3 +50,51 @@ extension Effect where Action: ReceiveAction { } } +@usableFromInline +struct ReceiveOperation { + + @usableFromInline + let embed: @Sendable (TaskResult) -> Action + + @usableFromInline + let operation: @Sendable () async throws -> Input + + @usableFromInline + let transform: @Sendable (Input) -> Result + + @usableFromInline + func callAsFunction(send: Send) async { + await send(embed( + TaskResult { try await operation() } + .map(transform) + )) + } + + @usableFromInline + static func `case`( + _ casePath: AnyCasePath>, + _ 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) -> Action, + operation: @escaping @Sendable () async throws -> Input + ) { + self.init(embed: embed, operation: operation, transform: { $0 }) + } + + @usableFromInline + static func `case`( + _ casePath: AnyCasePath>, + _ operation: @escaping @Sendable () async throws -> Input + ) -> Self { + .init(embed: { casePath.embed($0) }, operation: operation) + } +} diff --git a/Sources/ComposableSubscriber/Internal/SetAction.swift b/Sources/ComposableSubscriber/Internal/SetAction.swift index 54e18dc..c9bf621 100644 --- a/Sources/ComposableSubscriber/Internal/SetAction.swift +++ b/Sources/ComposableSubscriber/Internal/SetAction.swift @@ -30,3 +30,20 @@ enum SetAction { } } + +@usableFromInline +struct SetAction2 { + let operation: @Sendable (inout State, Value) -> Void + + init(_ operation: @escaping @Sendable (inout State, Value) -> Void) { + self.operation = operation + } + + static func keyPath( + _ keyPath: WritableKeyPath + ) -> Self { + .init({ state, value in + state[keyPath: keyPath] = value + }) + } +} diff --git a/Sources/ComposableSubscriber/OnFailAction.swift b/Sources/ComposableSubscriber/OnFailAction.swift index 49ab9f8..b8c3fcd 100644 --- a/Sources/ComposableSubscriber/OnFailAction.swift +++ b/Sources/ComposableSubscriber/OnFailAction.swift @@ -3,7 +3,7 @@ import Foundation import OSLog public enum OnFailAction { - case fail(prefix: String? = nil, log: ((String) -> Void)? = nil) + case fail(prefix: String? = nil, log: (@Sendable (String) -> Void)? = nil) case operation((inout State, Error) -> Effect) @usableFromInline diff --git a/Sources/ComposableSubscriber/Reducer+onReceive.swift b/Sources/ComposableSubscriber/Reducer+onReceive.swift index 5df6b2c..eb41d41 100644 --- a/Sources/ComposableSubscriber/Reducer+onReceive.swift +++ b/Sources/ComposableSubscriber/Reducer+onReceive.swift @@ -188,13 +188,15 @@ public struct _OnReceiveReducer: Reducer { action: Parent.Action ) -> Effect { let baseEffects = parent.reduce(into: &state, action: action) - var setEffects = Effect.none - - if let value = receiveAction(action) { - setEffects = setAction(state: &state, value: value) + + guard let value = receiveAction(action) else { + return baseEffects } - return .merge(baseEffects, setEffects) + return .merge( + baseEffects, + setAction(state: &state, value: value) + ) } } @@ -208,13 +210,13 @@ public struct _ReceiveOnTriggerReducer< let parent: Parent @usableFromInline - let triggerAction: (Parent.Action) -> TriggerAction? + let triggerAction: @Sendable (Parent.Action) -> TriggerAction? @usableFromInline - let toReceiveAction: (TaskResult) -> Parent.Action + let toReceiveAction: @Sendable (TaskResult) -> Parent.Action @usableFromInline - let resultHandler: () async throws -> Value + let resultHandler: @Sendable () async throws -> Value @usableFromInline init( @@ -242,11 +244,7 @@ public struct _ReceiveOnTriggerReducer< return .merge( baseEffects, - .run { send in - await send(toReceiveAction( - TaskResult { try await resultHandler() } - )) - } + .receive(operation: .init(embed: toReceiveAction, operation: resultHandler)) ) } }