This commit is contained in:
2024-05-30 22:54:58 -04:00
parent bd0f90894f
commit dac5d932dd
5 changed files with 238 additions and 52 deletions

View File

@@ -4,38 +4,179 @@ import OSLog
extension Effect { extension Effect {
/// 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<Int>)
/// ...
/// }
///
/// @Dependency(\.logger) var logger
///
/// var body: some ReducerOf<Self> {
/// 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<Int>)
/// ...
/// }
///
/// @Dependency(\.logger) var logger
///
/// var body: some ReducerOf<Self> {
/// 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, *) @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public static func fail( public static func fail(
_ message: String, _ message: String,
logger: Logger? = nil logger: Logger? = nil
) -> Self { ) -> Self {
XCTFail("\(message)") .fail(message, log: { logger?.error("\($0)") })
logger?.error("\(message)")
return .none
} }
/// 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<Int>)
/// ...
/// }
///
/// @Dependency(\.logger) var logger
///
/// var body: some ReducerOf<Self> {
/// 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, *) @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public static func fail( public static func fail(
prefix: String = "Failed error:", prefix: String = "Failed error:",
error: any Error, error: any Error,
logger: Logger logger: Logger
) -> Self { ) -> 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<Int>)
/// ...
/// }
///
/// @Dependency(\.logger) var logger
///
/// var body: some ReducerOf<Self> {
/// 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( public static func fail(
prefix: String = "Failed error:", prefix: String = "Failed error:",
error: any Error, error: any Error,
log: ((String) -> Void)? = nil log: (@Sendable (String) -> Void)? = nil
) -> Self { ) -> Self {
let message = "\(prefix) \(error.localizedDescription)" return .fail("\(prefix) \(error.localizedDescription)", log: log)
XCTFail("\(message)")
log?("\(message)")
return .none
} }
} }

View File

@@ -1,31 +1,13 @@
import ComposableArchitecture import ComposableArchitecture
import OSLog
extension Effect { extension Effect {
@usableFromInline @usableFromInline
static func receive<T>( static func receive<Input, Result>(
_ casePath: AnyCasePath<Action, TaskResult<T>>, operation: ReceiveOperation<Action, Input, Result>
_ operation: @escaping @Sendable () async throws -> T
) -> Self { ) -> Self {
.run { send in .run { send in
await send(casePath.embed( await operation(send: send)
TaskResult { try await operation() }
))
}
}
@usableFromInline
static func receive<T, V>(
_ casePath: AnyCasePath<Action, TaskResult<V>>,
_ 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)
))
} }
} }
@@ -34,7 +16,7 @@ extension Effect {
action toResult: CaseKeyPath<Action, TaskResult<T>>, action toResult: CaseKeyPath<Action, TaskResult<T>>,
operation: @escaping @Sendable () async throws -> T operation: @escaping @Sendable () async throws -> T
) -> Self { ) -> Self {
.receive(AnyCasePath(toResult), operation) .receive(operation: .case(AnyCasePath(toResult), operation))
} }
@inlinable @inlinable
@@ -43,7 +25,7 @@ extension Effect {
operation: @escaping @Sendable () async throws -> T, operation: @escaping @Sendable () async throws -> T,
transform: @escaping @Sendable (T) -> V transform: @escaping @Sendable (T) -> V
) -> Self { ) -> 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, _ operation: @escaping @Sendable () async throws -> T,
transform: @escaping @Sendable (T) -> Action.ReceiveAction transform: @escaping @Sendable (T) -> Action.ReceiveAction
) -> Self { ) -> Self {
.receive(AnyCasePath(unsafe: Action.receive), operation, transform) .receive(operation: .case(AnyCasePath(unsafe: Action.receive), operation, transform))
} }
@inlinable @inlinable
@@ -68,3 +50,51 @@ extension Effect where Action: ReceiveAction {
} }
} }
@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)
}
}

View File

@@ -30,3 +30,20 @@ enum SetAction<State, Action, Value> {
} }
} }
@usableFromInline
struct SetAction2<State, Action, Value> {
let operation: @Sendable (inout State, Value) -> Void
init(_ operation: @escaping @Sendable (inout State, Value) -> Void) {
self.operation = operation
}
static func keyPath(
_ keyPath: WritableKeyPath<State, Value>
) -> Self {
.init({ state, value in
state[keyPath: keyPath] = value
})
}
}

View File

@@ -3,7 +3,7 @@ import Foundation
import OSLog import OSLog
public enum OnFailAction<State, Action> { public enum OnFailAction<State, Action> {
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<Action>) case operation((inout State, Error) -> Effect<Action>)
@usableFromInline @usableFromInline

View File

@@ -188,13 +188,15 @@ public struct _OnReceiveReducer<Parent: Reducer, Value>: Reducer {
action: Parent.Action action: Parent.Action
) -> Effect<Parent.Action> { ) -> Effect<Parent.Action> {
let baseEffects = parent.reduce(into: &state, action: action) let baseEffects = parent.reduce(into: &state, action: action)
var setEffects = Effect<Action>.none
if let value = receiveAction(action) { guard let value = receiveAction(action) else {
setEffects = setAction(state: &state, value: value) return baseEffects
} }
return .merge(baseEffects, setEffects) return .merge(
baseEffects,
setAction(state: &state, value: value)
)
} }
} }
@@ -208,13 +210,13 @@ public struct _ReceiveOnTriggerReducer<
let parent: Parent let parent: Parent
@usableFromInline @usableFromInline
let triggerAction: (Parent.Action) -> TriggerAction? let triggerAction: @Sendable (Parent.Action) -> TriggerAction?
@usableFromInline @usableFromInline
let toReceiveAction: (TaskResult<Value>) -> Parent.Action let toReceiveAction: @Sendable (TaskResult<Value>) -> Parent.Action
@usableFromInline @usableFromInline
let resultHandler: () async throws -> Value let resultHandler: @Sendable () async throws -> Value
@usableFromInline @usableFromInline
init( init(
@@ -242,11 +244,7 @@ public struct _ReceiveOnTriggerReducer<
return .merge( return .merge(
baseEffects, baseEffects,
.run { send in .receive(operation: .init(embed: toReceiveAction, operation: resultHandler))
await send(toReceiveAction(
TaskResult { try await resultHandler() }
))
}
) )
} }
} }