feat: Adding more helpers, wip

This commit is contained in:
2024-05-30 10:00:04 -04:00
parent 4a85915ebe
commit ee983f7337
4 changed files with 345 additions and 31 deletions

View File

@@ -0,0 +1,41 @@
import ComposableArchitecture
import OSLog
extension Effect {
/// An effect that throws a runtime warning and optionally logs an error 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
}
/// An effect that throws a runtime warning and optionally logs an error 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)") })
}
/// An effect that throws a runtime warning and optionally logs an error message.
public static func fail(
prefix: String = "Failed error:",
error: any Error,
log: ((String) -> Void)? = nil
) -> Self {
let message = "\(prefix) \(error.localizedDescription)"
XCTFail("\(message)")
log?("\(message)")
return .none
}
}

View File

@@ -0,0 +1,51 @@
import ComposableArchitecture
import OSLog
public protocol ReceiveAction<ReceiveAction> {
associatedtype ReceiveAction
static func receive(_ result: TaskResult<ReceiveAction>) -> Self
var result: TaskResult<ReceiveAction>? { get }
}
extension ReceiveAction {
public var result: TaskResult<ReceiveAction>? {
AnyCasePath(unsafe: Self.receive).extract(from: 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<T>(
_ 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<T>(
_ toReceiveAction: CaseKeyPath<Action.ReceiveAction, T>,
_ operation: @escaping () async throws -> T
) -> Self {
return .receive(operation) {
AnyCasePath(toReceiveAction).embed($0)
}
}
}

View File

@@ -1,18 +1,36 @@
import ComposableArchitecture
import OSLog
extension Reducer {
@inlinable
public func onReceive<V>(
action toReceiveAction: CaseKeyPath<Action, V>,
set setAction: @escaping (inout State, V) -> Effect<Action>
) -> _ReceiveReducer<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) -> Void
) -> _ReceiveReducer<Self, V> {
.init(
parent: self,
receiveAction: AnyCasePath(toReceiveAction),
setAction: setAction
receiveAction: { AnyCasePath(toReceiveAction).extract(from: $0) },
setAction: { state, value in
setAction(&state, value)
return .none
}
)
}
@inlinable
public func onReceive<V>(
action toReceiveAction: CaseKeyPath<Action, V>,
set toStateKeyPath: WritableKeyPath<State, V>
@@ -20,6 +38,7 @@ extension Reducer {
self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:))
}
@inlinable
public func onReceive<V>(
action toReceiveAction: CaseKeyPath<Action, V>,
set toStateKeyPath: WritableKeyPath<State, V?>
@@ -27,80 +46,98 @@ extension Reducer {
self.onReceive(action: toReceiveAction, set: toStateKeyPath.callAsFunction(root:value:))
}
@inlinable
public func onReceive<V>(
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
onFail: OnFailAction<State>? = nil,
onFail: OnFailAction<State, Action>? = nil,
onSuccess setAction: @escaping (inout State, V) -> Void
) -> _ReceiveReducer<Self, TaskResult<V>> {
self.onReceive(action: toReceiveAction) { state, result in
switch result {
case let .failure(error):
if let onFail {
onFail(state: &state, error: error)
return onFail(state: &state, error: error)
}
return .none
case let .success(value):
setAction(&state, value)
return .none
}
}
}
@inlinable
public func onReceive<V>(
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
set toStateKeyPath: WritableKeyPath<State, V>,
onFail: OnFailAction<State>? = nil
onFail: OnFailAction<State, Action>? = nil
) -> _ReceiveReducer<Self, TaskResult<V>> {
self.onReceive(action: toReceiveAction) { state, result in
switch result {
case let .failure(error):
if let onFail {
onFail(state: &state, error: error)
return onFail(state: &state, error: error)
}
return .none
case let .success(value):
toStateKeyPath(root: &state, value: value)
return .none
}
}
}
@inlinable
public func onReceive<V>(
action toReceiveAction: CaseKeyPath<Action, TaskResult<V>>,
set toStateKeyPath: WritableKeyPath<State, V?>,
onFail: OnFailAction<State>? = nil
onFail: OnFailAction<State, Action>? = nil
) -> _ReceiveReducer<Self, TaskResult<V>> {
self.onReceive(action: toReceiveAction) { state, result in
switch result {
case let .failure(error):
if let onFail {
onFail(state: &state, error: error)
return onFail(state: &state, error: error)
}
return .none
case let .success(value):
toStateKeyPath(root: &state, value: value)
return .none
}
}
}
}
public enum OnFailAction<State> {
case xctFail(prefix: String? = nil)
public enum OnFailAction<State, Action> {
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)") })
}
@usableFromInline
func callAsFunction(state: inout State, error: Error) {
func callAsFunction(state: inout State, error: Error) -> Effect<Action> {
switch self {
case let .xctFail(prefix):
case let .fail(prefix, log):
if let prefix {
XCTFail("\(prefix): \(error)")
return .fail(prefix: prefix, error: error, log: log)
} else {
XCTFail("\(error)")
return .fail(error: error, log: log)
}
case let .handle(handler):
handler(&state, error)
return .none
}
}
}
extension WritableKeyPath {
public func callAsFunction(root: inout Root, value: Value) {
@usableFromInline
func callAsFunction(root: inout Root, value: Value) {
root[keyPath: self] = value
}
@@ -112,18 +149,133 @@ public struct _ReceiveReducer<Parent: Reducer, Value>: Reducer {
let parent: Parent
@usableFromInline
let receiveAction: AnyCasePath<Parent.Action, Value>
let receiveAction: (Parent.Action) -> Value?
@usableFromInline
let setAction: (inout Parent.State, Value) -> Void
let setAction: (inout Parent.State, Value) -> Effect<Parent.Action>
public func reduce(into state: inout Parent.State, action: Parent.Action) -> Effect<Parent.Action> {
@usableFromInline
init(
parent: Parent,
receiveAction: @escaping (Parent.Action) -> Value?,
setAction: @escaping (inout Parent.State, Value) -> Effect<Parent.Action>
) {
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)
var setEffects = Effect<Action>.none
if let value = receiveAction.extract(from: action) {
setAction(&state, value)
if let value = receiveAction(action) {
setEffects = setAction(&state, value)
}
return baseEffects
return .merge(baseEffects, setEffects)
}
}
public struct _OnRecieveReducer<Parent: Reducer, TriggerAction, Value>: Reducer {
@usableFromInline
let parent: Parent
@usableFromInline
let triggerAction: (Parent.Action) -> TriggerAction?
@usableFromInline
let toReceiveAction: (TaskResult<Value>) -> Parent.Action
@usableFromInline
let resultHandler: () async throws -> Value
@usableFromInline
init(
parent: Parent,
triggerAction: @escaping (Parent.Action) -> TriggerAction?,
toReceiveAction: @escaping (TaskResult<Value>) -> Parent.Action,
resultHandler: @escaping () -> 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,
.run { send in
await send(toReceiveAction(
TaskResult { try await 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

@@ -6,6 +6,16 @@ import XCTest
struct NumberClient {
var numberStreamWithoutArg: @Sendable () async -> AsyncStream<Int> = { .never }
var numberStreamWithArg: @Sendable (Int) async -> AsyncStream<Int> = { _ in .never }
var currentNumber: @Sendable () async throws -> Int
func currentNumber(fail: Bool = false) async throws -> Int {
if fail {
struct CurrentNumberError: Error { }
throw CurrentNumberError()
}
return try await currentNumber()
}
}
extension NumberClient: TestDependencyKey {
@@ -23,7 +33,8 @@ extension NumberClient: TestDependencyKey {
continuation.yield(number)
continuation.finish()
}
}
},
currentNumber: { 69420 }
)
}
@@ -91,8 +102,50 @@ struct ReducerWithTransform {
}
}
@Reducer
struct ReducerWithReceiveAction {
typealias State = NumberState
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: .fail()) { state, action in
switch action {
case let .currentNumber(number):
state.currentNumber = number
return .none
}
}
Reduce<State, Action> { state, action in
switch action {
case .receive:
return .none
case .task:
return .receive(\.currentNumber) {
try await numberClient.currentNumber()
}
}
}
}
}
@MainActor
final class swift_composable_subscriberTests: XCTestCase {
final class TCAExtrasTests: XCTestCase {
func testSubscribeWithArg() async throws {
let store = TestStore(
@@ -128,4 +181,21 @@ final class swift_composable_subscriberTests: XCTestCase {
await store.finish()
}
func testReceiveAction() async throws {
let store = TestStore(
initialState: ReducerWithReceiveAction.State(number: 19),
reducer: ReducerWithReceiveAction.init
) {
$0.numberClient = .live
}
let task = await store.send(.task)
await store.receive(\.receive) {
$0.currentNumber = 69420
}
await task.cancel()
await store.finish()
}
}