diff --git a/Package.swift b/Package.swift index b10082d..a0f63bb 100644 --- a/Package.swift +++ b/Package.swift @@ -45,6 +45,12 @@ let package = Package( ), .target(name: "SharedModels"), .target(name: "Styleguide"), + .target( + name: "TCAHelpers", + dependencies: [ + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ] + ), .testTarget( name: "EstimatedPressureTests", dependencies: [ diff --git a/Sources/TCAHelpers/Effect+fail.swift b/Sources/TCAHelpers/Effect+fail.swift new file mode 100644 index 0000000..9a9ef10 --- /dev/null +++ b/Sources/TCAHelpers/Effect+fail.swift @@ -0,0 +1,25 @@ +import ComposableArchitecture +import OSLog + +extension Effect { + public static func fail( + prefix: String = "Failed error:", + error: Error, + logger: Logger? = nil + ) -> Self { + let message = "\(prefix) \(error)" + XCTFail("\(message)") + logger?.error("\(message)") + return .none + } + + public static func fail( + _ message: String, + logger: Logger? = nil + ) -> Self { + XCTFail("\(message)") + logger?.error("\(message)") + return .none + } +} + diff --git a/Sources/TCAHelpers/Effect+receive.swift b/Sources/TCAHelpers/Effect+receive.swift new file mode 100644 index 0000000..8774a10 --- /dev/null +++ b/Sources/TCAHelpers/Effect+receive.swift @@ -0,0 +1,40 @@ +import ComposableArchitecture + +public protocol ReceiveAction { + associatedtype ReceiveAction + static func receive(_ result: TaskResult) -> Self +} + +extension Effect where Action: ReceiveAction { + + public static func receive( + _ operation: @escaping () async throws -> Action.ReceiveAction + ) -> Self { + .run { send in + await send(.receive( + TaskResult { try await operation() } + )) + } + } + + public static func receive( + _ operation: @escaping () async throws -> T, + transform: @escaping (T) -> Action.ReceiveAction + ) -> Self { + .run { send in + await send(.receive( + TaskResult { try await operation() } + .map(transform) + )) + } + } + + public static func receive( + _ toReceiveAction: CaseKeyPath, + _ operation: @escaping () async throws -> T + ) -> Self { + return .receive(operation) { + AnyCasePath(toReceiveAction).embed($0) + } + } +} diff --git a/Sources/TCAHelpers/ReceiveFailureReducer.swift b/Sources/TCAHelpers/ReceiveFailureReducer.swift new file mode 100644 index 0000000..03045fb --- /dev/null +++ b/Sources/TCAHelpers/ReceiveFailureReducer.swift @@ -0,0 +1,51 @@ +import ComposableArchitecture +import OSLog + +public struct _ReceiveFailureReducer: Reducer { + + @usableFromInline + let toReceiveActionError: (Action) -> Error? + + @usableFromInline + let logger: Logger + + @inlinable + public init( + action toReceiveAction: CaseKeyPath, + logger: Logger + ) { + self.init( + internal: { action in + if case let .receive(.failure(error)) = AnyCasePath(toReceiveAction).extract(from: action) { + return error + } + return nil + }, + logger: logger + ) + } + + @usableFromInline + init(internal toReceiveActionError: @escaping (Action) -> Error?, logger: Logger) { + self.toReceiveActionError = toReceiveActionError + self.logger = logger + } + + @inlinable + public func reduce( + into state: inout State, + action: Action + ) -> Effect { + guard let error = toReceiveActionError(action) else { return .none } + return .fail(error: error, logger: logger) + } +} + +extension Reducer where Action: ReceiveAction, Action: CasePathable { + public func onFail(_ logger: Logger) -> _ReceiveFailureReducer { + _ReceiveFailureReducer( + action: \.receive, + logger: logger + ) + } +}