diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme index 46117df..c81fbb9 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Reducer.xcscheme @@ -77,6 +77,16 @@ ReferencedContainer = "container:"> + + + + State @@ -29,6 +30,8 @@ public extension Reduce { func start() async throws { } + func error(_ error: any Error, action: Action) async { } + func shouldCancel(_ current: Action, _ upcoming: Action) -> Bool { false } @@ -77,6 +80,7 @@ open class ProxyReduce: Reduce { public var initialState: State private let _start: ((@escaping (Mutation) -> Void) async throws -> Void)? + private let _error: ((@escaping (Mutation) -> Void, any Error, Action) async -> Void)? private let _mutate: ((State, Action, @escaping (Mutation) -> Void) async throws -> Void)? private let _reduce: ((State, Mutation) -> State)? private let _shouldCancel: ((Action, Action) -> Bool)? @@ -85,6 +89,7 @@ open class ProxyReduce: Reduce { public init( initialState: State, start: ((@escaping (Mutation) -> Void) async throws -> Void)? = nil, + error: ((@escaping (Mutation) -> Void, any Error, Action) async -> Void)? = nil, mutate: ((State, Action, @escaping (Mutation) -> Void) async throws -> Void)? = nil, reduce: ((State, Mutation) -> State)? = nil, shouldCancel: ((Action, Action) -> Bool)? = nil @@ -92,6 +97,7 @@ open class ProxyReduce: Reduce { self.initialState = initialState self._start = start + self._error = error self._mutate = mutate self._reduce = reduce self._shouldCancel = shouldCancel @@ -103,6 +109,9 @@ open class ProxyReduce: Reduce { start: { _ in try await reduce.start() }, + error: { _, error, action in + await reduce.error(error, action: action) + }, mutate: { _, action, _ in try await reduce.mutate(action: action) }, @@ -126,6 +135,10 @@ open class ProxyReduce: Reduce { try await _start?({ [weak self] mutation in self?.mutator?(mutation) }) } + public func error(_ error: any Error, action: Action) async { + await _error?({ [weak self] mutation in self?.mutator?(mutation) }, error, action) + } + open func mutate(action: Action) async throws { try await _mutate?(currentState, action, { [weak self] mutation in self?.mutator?(mutation) }) } diff --git a/Sources/Reducer/Reducer.swift b/Sources/Reducer/Reducer.swift index ed0c85a..268c769 100644 --- a/Sources/Reducer/Reducer.swift +++ b/Sources/Reducer/Reducer.swift @@ -115,7 +115,11 @@ open class Reducer: ObservableObject, Mutable { action, with: Task { @MainActor in // Mutate state from action. - try? await reduce.mutate(action: action) + do { + try await reduce.mutate(action: action) + } catch { + await reduce.error(error, action: action) + } } )) } diff --git a/Tests/ReducerTests/Model/ErrorReduce.swift b/Tests/ReducerTests/Model/ErrorReduce.swift new file mode 100644 index 0000000..119a962 --- /dev/null +++ b/Tests/ReducerTests/Model/ErrorReduce.swift @@ -0,0 +1,54 @@ +// +// ErrorReduce.swift +// Reducer +// +// Created by JSilver on 11/3/24. +// + +import Foundation +import Reducer + +@Reduce +@MainActor +class ErrorReduce { + enum Action { + case occurError(any Error) + } + + enum Mutation { + case setCount(Int) + } + + struct State { + var count: Int + } + + let initialState: State + + init() { + self.initialState = State( + count: 0 + ) + } + + func error(_ error: any Error, action: Action) async { + mutate(.setCount(currentState.count + 1)) + } + + func mutate(action: Action) async throws { + switch action { + case let .occurError(error): + throw error + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var state = state + + switch mutation { + case let .setCount(count): + state.count = count + return state + } + } +} diff --git a/Tests/ReducerTests/Model/TestError.swift b/Tests/ReducerTests/Model/TestError.swift new file mode 100644 index 0000000..421c4c4 --- /dev/null +++ b/Tests/ReducerTests/Model/TestError.swift @@ -0,0 +1,16 @@ +// +// TestError.swift +// Reducer +// +// Created by JSilver on 11/3/24. +// + +import Foundation + +struct TestError: Error { + let description: String + + init(_ description: String) { + self.description = description + } +} diff --git a/Tests/ReducerTests/ReducerTests.swift b/Tests/ReducerTests/ReducerTests.swift index e1393ce..1bf1797 100644 --- a/Tests/ReducerTests/ReducerTests.swift +++ b/Tests/ReducerTests/ReducerTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import Reducer import Combine +@MainActor final class ReducerTests: XCTestCase { // MARK: - Property @@ -22,7 +23,6 @@ final class ReducerTests: XCTestCase { } // MARK: - Test - @MainActor func test_that_count_increases_when_receiving_increase_action() async throws { // Given let reducer = Reducer( @@ -46,7 +46,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 1, 2]) } - @MainActor func test_that_count_does_not_mutate_when_receiving_same_action_twice() async throws { // Given let reducer = Reducer( @@ -70,7 +69,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 1]) } - @MainActor func test_that_all_action_cancelled_when_reducer_deinit() async throws { // Given var reducer: Reducer? = Reducer( @@ -96,7 +94,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0]) } - @MainActor func test_that_count_increases_when_mutate_in_start() async throws { // Given let reducer = Reducer(TimerReduce( @@ -113,7 +110,6 @@ final class ReducerTests: XCTestCase { XCTAssertGreaterThan(result.last ?? 0, 0) } - @MainActor func test_that_count_increases_when_await_mutate_in_start() async throws { // Given let reducer = Reducer(AwaitStartReduce( @@ -130,7 +126,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 1]) } - @MainActor func test_that_reducer_should_be_able_to_assign_proxy_reduce() async throws { // Given var reducer = Reducer(CountIncreaseReduce( @@ -170,7 +165,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 10]) } - @MainActor func test_that_initial_count_is_100_when_proxy_set_initial_count() async throws { // Given let reducer = Reducer(proxy: .init( @@ -189,7 +183,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [100]) } - @MainActor func test_that_count_increases_when_proxy_receiving_increase_action() async throws { // Given let reducer = Reducer(proxy: .init( @@ -226,7 +219,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 1]) } - @MainActor func test_that_count_increases_10_when_proxy_receiving_increase_action() async throws { // Given let reducer = Reducer(proxy: .init( @@ -264,7 +256,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 10, 20]) } - @MainActor func test_that_count_does_not_mutate_when_proxy_same_action_twice() async throws { // Given let reducer = Reducer(proxy: .init( @@ -304,7 +295,6 @@ final class ReducerTests: XCTestCase { XCTAssertEqual(result, [0, 1]) } - @MainActor func test_that_count_increases_when_proxy_mutate_in_start() async throws { // Given var cancellable: AnyCancellable? = nil @@ -336,7 +326,6 @@ final class ReducerTests: XCTestCase { XCTAssertGreaterThan(result.last ?? 0, 0) } - @MainActor func test_that_reducer_can_assign_proxy_inherited_reduce() async throws { // Given let reducer = Reducer( @@ -357,4 +346,22 @@ final class ReducerTests: XCTestCase { // Then XCTAssertEqual(result, [0, 10]) } + + func test_that_count_increases_when_error_is_thrown_in_mutation() async throws { + // Given + let sut = Reducer(ErrorReduce()) + + // When + Task { + sut.action(.occurError(TestError("increase count"))) + } + + let result = try await wait( + sut.$state.map(\.count), + timeout: 1 + ) + + // Then + XCTAssertEqual(result, [0, 1]) + } }