From d661685a96c91516eb032261e417c6999262f620 Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Wed, 21 May 2025 09:30:23 +0200 Subject: [PATCH 1/5] make Reducer generic over errors --- Sources/ImmutableData/Store.swift | 13 +++++++------ Tests/ImmutableDataTests/StoreTests.swift | 14 +++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Sources/ImmutableData/Store.swift b/Sources/ImmutableData/Store.swift index 245293e..c0c7d5b 100644 --- a/Sources/ImmutableData/Store.swift +++ b/Sources/ImmutableData/Store.swift @@ -23,7 +23,8 @@ /// - Throws: An `Error` indicating this `(State, Action)` pair led to a recoverable error and state was not transformed. /// /// - SeeAlso: The `Reducer` type serves a similar role as the [Reducer](https://redux.js.org/understanding/thinking-in-redux/glossary#reducer) type in Redux. -public typealias Reducer = @Sendable (State, Action) throws -> State where State: Sendable, Action: Sendable +public typealias Reducer = @Sendable (State, Action) throws(Error) -> State +where State: Sendable, Action: Sendable, Error: Swift.Error /// An object to save the current state of our data models. /// @@ -50,19 +51,19 @@ public typealias Reducer = @Sendable (State, Action) throws -> St /// ``` /// /// - SeeAlso: The `Store` object serves a similar role as the [Store](https://redux.js.org/understanding/thinking-in-redux/glossary#store) object in Redux. -@MainActor final public class Store where State: Sendable, Action: Sendable { +@MainActor final public class Store where State: Sendable, Action: Sendable, Error: Swift.Error { private let registrar = StreamRegistrar<(oldState: State, action: Action)>() private var state: State - private let reducer: Reducer - + private let reducer: Reducer + /// Constructs a `Store`. /// /// - Parameter state: The initial state of our application. /// - Parameter reducer: The root reducer of our application. public init( initialState state: State, - reducer: @escaping Reducer + reducer: @escaping Reducer ) { self.state = state self.reducer = reducer @@ -70,7 +71,7 @@ public typealias Reducer = @Sendable (State, Action) throws -> St } extension Store: Dispatcher { - public func dispatch(action: Action) throws { + public func dispatch(action: Action) throws(Error) { let oldState = self.state self.state = try self.reducer(self.state, action) self.registrar.yield((oldState: oldState, action: action)) diff --git a/Tests/ImmutableDataTests/StoreTests.swift b/Tests/ImmutableDataTests/StoreTests.swift index 3331af1..944aec2 100644 --- a/Tests/ImmutableDataTests/StoreTests.swift +++ b/Tests/ImmutableDataTests/StoreTests.swift @@ -43,7 +43,7 @@ extension ReducerTestDouble { @Sendable func reduce( state: StateTestDouble, action: ActionTestDouble - ) throws -> StateTestDouble { + ) throws(Swift.Error) -> StateTestDouble { self.parameterState = state self.parameterAction = action if let returnError = self.returnError { @@ -54,15 +54,15 @@ extension ReducerTestDouble { } final fileprivate class ThunkTestDouble : @unchecked Sendable { - var parameterDispatcher: Store? - var parameterSelector: Store? + var parameterDispatcher: Store? + var parameterSelector: Store? var returnError: Error? } extension ThunkTestDouble { @Sendable func thunk( - dispatcher: Store, - selector: Store + dispatcher: Store, + selector: Store ) throws { self.parameterDispatcher = dispatcher self.parameterSelector = selector @@ -74,8 +74,8 @@ extension ThunkTestDouble { extension ThunkTestDouble { @Sendable func asyncThunk( - dispatcher: Store, - selector: Store + dispatcher: Store, + selector: Store ) async throws { self.parameterDispatcher = dispatcher self.parameterSelector = selector From 17ebf09b2d0a8c984ed52070909aecd219676910 Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Wed, 21 May 2025 10:05:57 +0200 Subject: [PATCH 2/5] make Dispatcher generic over errors --- Sources/ImmutableData/Dispatcher.swift | 19 ++++++++++++------- Sources/ImmutableUI/Dispatcher.swift | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/ImmutableData/Dispatcher.swift b/Sources/ImmutableData/Dispatcher.swift index 491cb5f..6865441 100644 --- a/Sources/ImmutableData/Dispatcher.swift +++ b/Sources/ImmutableData/Dispatcher.swift @@ -15,8 +15,8 @@ // /// An interface that dispatches events that could affect change on the global state of our application. -public protocol Dispatcher: Sendable { - +public protocol Dispatcher: Sendable { + /// The global state of our application. /// /// - Important: `State` values are modeled as immutable value-types -- *not* mutable reference-types. @@ -37,13 +37,18 @@ public protocol Dispatcher: Sendable { /// - SeeAlso: The `Action` type serves a similar role as the [Action](https://redux.js.org/understanding/thinking-in-redux/glossary#action) type in Redux. associatedtype Action: Sendable + /// The error thrown while dispatching the ``Action``. + /// + /// Action dispatchers that do not fail can set this to `Never`. + associatedtype Error: Swift.Error + /// A ``/ImmutableData/Dispatcher`` type for affecting change on the global state of our application. /// /// This type is passed to the `dispatch(thunk:)` functions. /// /// This type can be the same type we just dispatched from -- but this is not explicitly required. - associatedtype Dispatcher: ImmutableData.Dispatcher - + associatedtype Dispatcher: ImmutableData.Dispatcher + /// A ``/ImmutableData/Selector`` type for selecting a slice of the global state of our application. /// /// This type is passed to the `dispatch(thunk:)` functions. @@ -56,11 +61,11 @@ public protocol Dispatcher: Sendable { /// /// - Parameter action: An event that could affect change on the global state of our application. /// - /// - Throws: An `Error` from the root reducer. + /// - Throws: ``Error`` from the root reducer, if any. /// /// - SeeAlso: The `dispatch(action:)` function serves a similar role as the [`dispatch(action)`](https://redux.js.org/api/store#dispatchaction) function in Redux. - @MainActor func dispatch(action: Action) throws - + @MainActor func dispatch(action: Action) throws(Self.Error) + /// Dispatch a unit of work that could include side effects. /// /// - Parameter thunk: A unit of work that could include side effects. diff --git a/Sources/ImmutableUI/Dispatcher.swift b/Sources/ImmutableUI/Dispatcher.swift index 30e991d..e0810ae 100644 --- a/Sources/ImmutableUI/Dispatcher.swift +++ b/Sources/ImmutableUI/Dispatcher.swift @@ -75,7 +75,7 @@ import SwiftUI } /// The current store of the environment property. - public var wrappedValue: some ImmutableData.Dispatcher { + public var wrappedValue: some ImmutableData.Dispatcher { self.store } } From a4c7f1a5e05295781c72c0e71c0576f62b80332a Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Wed, 21 May 2025 10:06:33 +0200 Subject: [PATCH 3/5] use typed throwing Store in Counter example to get rid of errors --- .../CounterUI/Sources/CounterUI/Content.swift | 52 ++++++++----------- .../Sources/CounterUI/Dispatch.swift | 6 +-- .../CounterUI/Sources/CounterUI/Select.swift | 4 +- .../Sources/CounterUI/StoreKey.swift | 10 ++-- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/Samples/CounterUI/Sources/CounterUI/Content.swift b/Samples/CounterUI/Sources/CounterUI/Content.swift index 8e404cd..04314db 100644 --- a/Samples/CounterUI/Sources/CounterUI/Content.swift +++ b/Samples/CounterUI/Sources/CounterUI/Content.swift @@ -51,21 +51,13 @@ extension Content : View { extension Content { private func didTapIncrementButton() { - do { - try self.dispatch(.didTapIncrementButton) - } catch { - print(error) - } + self.dispatch(.didTapIncrementButton) } } extension Content { private func didTapDecrementButton() { - do { - try self.dispatch(.didTapDecrementButton) - } catch { - print(error) - } + self.dispatch(.didTapDecrementButton) } } @@ -84,23 +76,23 @@ extension Content { } } -fileprivate struct CounterError : Swift.Error { - let state: CounterState - let action: CounterAction -} - -#Preview { - @Previewable @State var store = ImmutableData.Store( - initialState: CounterState(), - reducer: { (state: CounterState, action: CounterAction) -> (CounterState) in - throw CounterError( - state: state, - action: action - ) - } - ) - - Provider(store) { - Content() - } -} +//fileprivate struct CounterError : Swift.Error { +// let state: CounterState +// let action: CounterAction +//} +// +//#Preview { +// @Previewable @State var store = ImmutableData.Store( +// initialState: CounterState(), +// reducer: { (state: CounterState, action: CounterAction) throws(CounterError) -> (CounterState) in +// throw CounterError( +// state: state, +// action: action +// ) +// } +// ) +// +// Provider(store) { +// Content() +// } +//} diff --git a/Samples/CounterUI/Sources/CounterUI/Dispatch.swift b/Samples/CounterUI/Sources/CounterUI/Dispatch.swift index 4466062..c7da610 100644 --- a/Samples/CounterUI/Sources/CounterUI/Dispatch.swift +++ b/Samples/CounterUI/Sources/CounterUI/Dispatch.swift @@ -20,13 +20,13 @@ import ImmutableUI import SwiftUI @MainActor @propertyWrapper struct Dispatch : DynamicProperty { - @ImmutableUI.Dispatcher() private var dispatcher - + @ImmutableUI.Dispatcher() private var dispatcher + init() { } - var wrappedValue: (CounterAction) throws -> () { + var wrappedValue: (CounterAction) -> () { self.dispatcher.dispatch } } diff --git a/Samples/CounterUI/Sources/CounterUI/Select.swift b/Samples/CounterUI/Sources/CounterUI/Select.swift index 51f2b66..c81bd78 100644 --- a/Samples/CounterUI/Sources/CounterUI/Select.swift +++ b/Samples/CounterUI/Sources/CounterUI/Select.swift @@ -38,7 +38,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, outputSelector: @escaping @Sendable (Store.State) -> Output - ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { + ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { self.init( id: id, label: label, @@ -55,7 +55,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, outputSelector: @escaping @Sendable (Store.State) -> Output - ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { + ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { self.init( label: label, filter: isIncluded, diff --git a/Samples/CounterUI/Sources/CounterUI/StoreKey.swift b/Samples/CounterUI/Sources/CounterUI/StoreKey.swift index ac5cfa6..7de33f3 100644 --- a/Samples/CounterUI/Sources/CounterUI/StoreKey.swift +++ b/Samples/CounterUI/Sources/CounterUI/StoreKey.swift @@ -25,7 +25,7 @@ extension ImmutableUI.Provider { public init( _ store: Store, @ViewBuilder content: () -> Content - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, store, @@ -35,7 +35,7 @@ extension ImmutableUI.Provider { } extension ImmutableUI.Dispatcher { - public init() where Store == ImmutableData.Store { + public init() where Store == ImmutableData.Store { self.init(\.store) } } @@ -47,7 +47,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat DependencySelector, outputSelector: OutputSelector - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, id: id, @@ -65,7 +65,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat DependencySelector, outputSelector: OutputSelector - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, label: label, @@ -84,7 +84,7 @@ extension ImmutableUI.Selector { } extension EnvironmentValues { - fileprivate var store: ImmutableData.Store { + /*fileprivate*/ var store: ImmutableData.Store { get { self[StoreKey.self] } From 1e2ebb14590c726287e5ffe56a60dfa88e09694e Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Wed, 21 May 2025 10:23:44 +0200 Subject: [PATCH 4/5] use typed throwing Store in Animals example --- .../Sources/AnimalsData/AnimalsReducer.swift | 28 +++++++++---------- .../Sources/AnimalsData/Listener.swift | 14 +++++----- .../AnimalsData/PersistentSession.swift | 24 ++++++++-------- .../AnimalsDataTests/ListenerTests.swift | 2 +- .../Sources/AnimalsUI/Dispatch.swift | 2 +- .../Sources/AnimalsUI/PreviewStore.swift | 2 +- .../AnimalsUI/Sources/AnimalsUI/Select.swift | 12 ++++---- .../Sources/AnimalsUI/StoreKey.swift | 10 +++---- 8 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Samples/AnimalsData/Sources/AnimalsData/AnimalsReducer.swift b/Samples/AnimalsData/Sources/AnimalsData/AnimalsReducer.swift index 720f802..567a361 100644 --- a/Samples/AnimalsData/Sources/AnimalsData/AnimalsReducer.swift +++ b/Samples/AnimalsData/Sources/AnimalsData/AnimalsReducer.swift @@ -20,7 +20,7 @@ public enum AnimalsReducer { @Sendable public static func reduce( state: AnimalsState, action: AnimalsAction - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .ui(action: let action): return try self.reduce(state: state, action: action) @@ -34,7 +34,7 @@ extension AnimalsReducer { private static func reduce( state: AnimalsState, action: AnimalsAction.UI - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .categoryList(action: let action): return try self.reduce(state: state, action: action) @@ -52,7 +52,7 @@ extension AnimalsReducer { private static func reduce( state: AnimalsState, action: AnimalsAction.UI.CategoryList - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .onAppear: if state.categories.status == nil { @@ -75,17 +75,17 @@ extension AnimalsReducer { } extension AnimalsReducer { - package struct Error: Swift.Error { - package enum Code: Hashable, Sendable { + public struct Error: Swift.Error { + public enum Code: Hashable, Sendable { case animalNotFound } - package let code: Self.Code + public let code: Self.Code } } extension AnimalsState { - fileprivate func onTapDeleteSelectedAnimalButton(animalId: Animal.ID) throws -> Self { + fileprivate func onTapDeleteSelectedAnimalButton(animalId: Animal.ID) throws(AnimalsReducer.Error) -> Self { guard let _ = self.animals.data[animalId] else { throw AnimalsReducer.Error(code: .animalNotFound) } @@ -99,7 +99,7 @@ extension AnimalsReducer { private static func reduce( state: AnimalsState, action: AnimalsAction.UI.AnimalList - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .onAppear: if state.animals.status == nil { @@ -118,7 +118,7 @@ extension AnimalsReducer { private static func reduce( state: AnimalsState, action: AnimalsAction.UI.AnimalDetail - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .onTapDeleteSelectedAnimalButton(animalId: let animalId): return try state.onTapDeleteSelectedAnimalButton(animalId: animalId) @@ -145,7 +145,7 @@ extension AnimalsState { name: String, diet: Animal.Diet, categoryId: Category.ID - ) throws -> Self { + ) throws(AnimalsReducer.Error) -> Self { guard let _ = self.animals.data[animalId] else { throw AnimalsReducer.Error(code: .animalNotFound) } @@ -159,7 +159,7 @@ extension AnimalsReducer { private static func reduce( state: AnimalsState, action: AnimalsAction.UI.AnimalEditor - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .onTapAddAnimalButton(id: let id, name: let name, diet: let diet, categoryId: let categoryId): return state.onTapAddAnimalButton(id: id, name: name, diet: diet, categoryId: categoryId) @@ -173,7 +173,7 @@ extension AnimalsReducer { private static func reduce( state: AnimalsState, action: AnimalsAction.Data - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { switch action { case .persistentSession(.didFetchCategories(result: let result)): return self.persistentSessionDidFetchCategories(state: state, result: result) @@ -288,7 +288,7 @@ extension AnimalsReducer { state: AnimalsState, animalId: Animal.ID, result: AnimalsAction.Data.PersistentSession.UpdateAnimalResult - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { guard let _ = state.animals.data[animalId] else { throw AnimalsReducer.Error(code: .animalNotFound) } @@ -309,7 +309,7 @@ extension AnimalsReducer { state: AnimalsState, animalId: Animal.ID, result: AnimalsAction.Data.PersistentSession.DeleteAnimalResult - ) throws -> AnimalsState { + ) throws(AnimalsReducer.Error) -> AnimalsState { guard let _ = state.animals.data[animalId] else { throw AnimalsReducer.Error(code: .animalNotFound) } diff --git a/Samples/AnimalsData/Sources/AnimalsData/Listener.swift b/Samples/AnimalsData/Sources/AnimalsData/Listener.swift index 79d032f..0b5b87a 100644 --- a/Samples/AnimalsData/Sources/AnimalsData/Listener.swift +++ b/Samples/AnimalsData/Sources/AnimalsData/Listener.swift @@ -39,7 +39,7 @@ extension UserDefaults { } extension Listener { - public func listen(to store: some ImmutableData.Dispatcher & ImmutableData.Selector & ImmutableData.Streamer & AnyObject) { + public func listen(to store: some ImmutableData.Dispatcher & ImmutableData.Selector & ImmutableData.Streamer & AnyObject) { if self.store !== store { self.store = store @@ -65,7 +65,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: AnimalsState, action: AnimalsAction ) async { @@ -80,7 +80,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: AnimalsState, action: AnimalsAction.UI ) async { @@ -99,7 +99,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: AnimalsState, action: AnimalsAction.UI.CategoryList ) async { @@ -134,7 +134,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: AnimalsState, action: AnimalsAction.UI.AnimalList ) async { @@ -167,7 +167,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: AnimalsState, action: AnimalsAction.UI.AnimalDetail ) async { @@ -189,7 +189,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: AnimalsState, action: AnimalsAction.UI.AnimalEditor ) async { diff --git a/Samples/AnimalsData/Sources/AnimalsData/PersistentSession.swift b/Samples/AnimalsData/Sources/AnimalsData/PersistentSession.swift index 36e9fdc..4687677 100644 --- a/Samples/AnimalsData/Sources/AnimalsData/PersistentSession.swift +++ b/Samples/AnimalsData/Sources/AnimalsData/PersistentSession.swift @@ -49,7 +49,7 @@ extension PersistentSession { func fetchAnimalsQuery() -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { + ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { { dispatcher, selector in try await self.fetchAnimalsQuery( dispatcher: dispatcher, @@ -61,7 +61,7 @@ extension PersistentSession { extension PersistentSession { private func fetchAnimalsQuery( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector ) async throws { let animals = try await { @@ -105,7 +105,7 @@ extension PersistentSession { ) -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { + ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { { dispatcher, selector in try await self.addAnimalMutation( dispatcher: dispatcher, @@ -121,7 +121,7 @@ extension PersistentSession { extension PersistentSession { private func addAnimalMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector, id: String, name: String, @@ -175,7 +175,7 @@ extension PersistentSession { ) -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { + ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { { dispatcher, selector in try await self.updateAnimalMutation( dispatcher: dispatcher, @@ -191,7 +191,7 @@ extension PersistentSession { extension PersistentSession { private func updateAnimalMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector, animalId: String, name: String, @@ -243,7 +243,7 @@ extension PersistentSession { ) -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { + ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { { dispatcher, selector in try await self.deleteAnimalMutation( dispatcher: dispatcher, @@ -256,7 +256,7 @@ extension PersistentSession { extension PersistentSession { private func deleteAnimalMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector, animalId: String ) async throws { @@ -300,7 +300,7 @@ extension PersistentSession { func fetchCategoriesQuery() -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { + ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { { dispatcher, selector in try await self.fetchCategoriesQuery( dispatcher: dispatcher, @@ -312,7 +312,7 @@ extension PersistentSession { extension PersistentSession { private func fetchCategoriesQuery( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector ) async throws { let categories = try await { @@ -351,7 +351,7 @@ extension PersistentSession { func reloadSampleDataMutation() -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { + ) async throws -> Void where Dispatcher : ImmutableData.Dispatcher, Selector : ImmutableData.Selector { { dispatcher, selector in try await self.reloadSampleDataMutation( dispatcher: dispatcher, @@ -363,7 +363,7 @@ extension PersistentSession { extension PersistentSession { private func reloadSampleDataMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector ) async throws { let (animals, categories) = try await { diff --git a/Samples/AnimalsData/Tests/AnimalsDataTests/ListenerTests.swift b/Samples/AnimalsData/Tests/AnimalsDataTests/ListenerTests.swift index 81d7067..10f5867 100644 --- a/Samples/AnimalsData/Tests/AnimalsDataTests/ListenerTests.swift +++ b/Samples/AnimalsData/Tests/AnimalsDataTests/ListenerTests.swift @@ -148,7 +148,7 @@ extension PersistentSessionPersistentStoreTestDouble { } extension StoreTestDouble : ImmutableData.Dispatcher { - func dispatch(action: Action) throws { + func dispatch(action: Action) throws(AnimalsReducer.Error) { self.parameterAction.append(action) } diff --git a/Samples/AnimalsUI/Sources/AnimalsUI/Dispatch.swift b/Samples/AnimalsUI/Sources/AnimalsUI/Dispatch.swift index eb5cddf..0331808 100644 --- a/Samples/AnimalsUI/Sources/AnimalsUI/Dispatch.swift +++ b/Samples/AnimalsUI/Sources/AnimalsUI/Dispatch.swift @@ -26,7 +26,7 @@ import SwiftUI } - var wrappedValue: (AnimalsAction) throws -> Void { + var wrappedValue: (AnimalsAction) throws(AnimalsReducer.Error) -> Void { self.dispatcher.dispatch } } diff --git a/Samples/AnimalsUI/Sources/AnimalsUI/PreviewStore.swift b/Samples/AnimalsUI/Sources/AnimalsUI/PreviewStore.swift index 0884e89..ace49a4 100644 --- a/Samples/AnimalsUI/Sources/AnimalsUI/PreviewStore.swift +++ b/Samples/AnimalsUI/Sources/AnimalsUI/PreviewStore.swift @@ -20,7 +20,7 @@ import ImmutableUI import SwiftUI @MainActor struct PreviewStore where Content : View { - @State private var store: ImmutableData.Store = { + @State private var store: ImmutableData.Store = { do { let store = ImmutableData.Store( initialState: AnimalsState(), diff --git a/Samples/AnimalsUI/Sources/AnimalsUI/Select.swift b/Samples/AnimalsUI/Sources/AnimalsUI/Select.swift index 6b98530..4550465 100644 --- a/Samples/AnimalsUI/Sources/AnimalsUI/Select.swift +++ b/Samples/AnimalsUI/Sources/AnimalsUI/Select.swift @@ -39,7 +39,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, outputSelector: @escaping @Sendable (Store.State) -> Output - ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { + ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { self.init( id: id, label: label, @@ -56,7 +56,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, outputSelector: @escaping @Sendable (Store.State) -> Output - ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { + ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { self.init( label: label, filter: isIncluded, @@ -91,7 +91,7 @@ extension ImmutableUI.Selector { } @MainActor @propertyWrapper struct SelectCategory: DynamicProperty { - @ImmutableUI.Selector, AnimalsData.Category?> var wrappedValue: AnimalsData.Category? + @ImmutableUI.Selector, AnimalsData.Category?> var wrappedValue: AnimalsData.Category? init(categoryId: AnimalsData.Category.ID?) { self._wrappedValue = ImmutableUI.Selector( @@ -111,7 +111,7 @@ extension ImmutableUI.Selector { } @MainActor @propertyWrapper struct SelectAnimalsValues: DynamicProperty { - @ImmutableUI.Selector, TreeDictionary, Array> var wrappedValue: Array + @ImmutableUI.Selector, TreeDictionary, Array> var wrappedValue: Array init(categoryId: AnimalsData.Category.ID?) { self._wrappedValue = ImmutableUI.Selector( @@ -139,7 +139,7 @@ extension ImmutableUI.Selector { } @MainActor @propertyWrapper struct SelectAnimal: DynamicProperty { - @ImmutableUI.Selector, Animal?> var wrappedValue: Animal? + @ImmutableUI.Selector, Animal?> var wrappedValue: Animal? init(animalId: Animal.ID?) { self._wrappedValue = ImmutableUI.Selector( @@ -151,7 +151,7 @@ extension ImmutableUI.Selector { } @MainActor @propertyWrapper struct SelectAnimalStatus: DynamicProperty { - @ImmutableUI.Selector, Status?> var wrappedValue: Status? + @ImmutableUI.Selector, Status?> var wrappedValue: Status? init(animalId: Animal.ID?) { self._wrappedValue = ImmutableUI.Selector( diff --git a/Samples/AnimalsUI/Sources/AnimalsUI/StoreKey.swift b/Samples/AnimalsUI/Sources/AnimalsUI/StoreKey.swift index 88a0f95..88f6b5a 100644 --- a/Samples/AnimalsUI/Sources/AnimalsUI/StoreKey.swift +++ b/Samples/AnimalsUI/Sources/AnimalsUI/StoreKey.swift @@ -25,7 +25,7 @@ extension ImmutableUI.Provider { public init( _ store: Store, @ViewBuilder content: () -> Content - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, store, @@ -35,7 +35,7 @@ extension ImmutableUI.Provider { } extension ImmutableUI.Dispatcher { - public init() where Store == ImmutableData.Store { + public init() where Store == ImmutableData.Store { self.init(\.store) } } @@ -47,7 +47,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat DependencySelector, outputSelector: OutputSelector - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, id: id, @@ -65,7 +65,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat DependencySelector, outputSelector: OutputSelector - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, label: label, @@ -84,7 +84,7 @@ extension ImmutableUI.Selector { } extension EnvironmentValues { - fileprivate var store: ImmutableData.Store { + fileprivate var store: ImmutableData.Store { get { self[StoreKey.self] } From fdeb69b4c2e70b8366de543a21d864c497f66118 Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Wed, 21 May 2025 10:42:29 +0200 Subject: [PATCH 5/5] use typed throwing Store in Quakes example --- .../Sources/QuakesData/Listener.swift | 12 +++++------ .../QuakesData/PersistentSession.swift | 20 +++++++++---------- .../Sources/QuakesData/QuakesReducer.swift | 14 ++++++------- .../Tests/QuakesDataTests/ListenerTests.swift | 2 +- .../QuakesUI/Sources/QuakesUI/Dispatch.swift | 2 +- .../Sources/QuakesUI/PreviewStore.swift | 2 +- .../QuakesUI/Sources/QuakesUI/Select.swift | 14 ++++++------- .../QuakesUI/Sources/QuakesUI/StoreKey.swift | 10 +++++----- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Samples/QuakesData/Sources/QuakesData/Listener.swift b/Samples/QuakesData/Sources/QuakesData/Listener.swift index 5fddbc8..42929bc 100644 --- a/Samples/QuakesData/Sources/QuakesData/Listener.swift +++ b/Samples/QuakesData/Sources/QuakesData/Listener.swift @@ -45,7 +45,7 @@ extension UserDefaults { } extension Listener { - public func listen(to store: some ImmutableData.Dispatcher & ImmutableData.Selector & ImmutableData.Streamer & AnyObject) { + public func listen(to store: some ImmutableData.Dispatcher & ImmutableData.Selector & ImmutableData.Streamer & AnyObject) { if self.store !== store { self.store = store @@ -72,7 +72,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: QuakesState, action: QuakesAction ) async { @@ -87,7 +87,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: QuakesState, action: QuakesAction.UI.QuakeList ) async { @@ -136,7 +136,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: QuakesState, action: QuakesAction.Data.PersistentSession ) async { @@ -151,7 +151,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: QuakesState, action: QuakesAction.Data.PersistentSession.LocalStore ) async { @@ -164,7 +164,7 @@ extension Listener { extension Listener { private func onReceive( - from store: some ImmutableData.Dispatcher & ImmutableData.Selector, + from store: some ImmutableData.Dispatcher & ImmutableData.Selector, oldState: QuakesState, action: QuakesAction.Data.PersistentSession.RemoteStore ) async { diff --git a/Samples/QuakesData/Sources/QuakesData/PersistentSession.swift b/Samples/QuakesData/Sources/QuakesData/PersistentSession.swift index c82e339..1ac2254 100644 --- a/Samples/QuakesData/Sources/QuakesData/PersistentSession.swift +++ b/Samples/QuakesData/Sources/QuakesData/PersistentSession.swift @@ -46,7 +46,7 @@ final actor PersistentSession where LocalStore : Persis extension PersistentSession { private func fetchLocalQuakesQuery( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector ) async throws { let quakes = try await { @@ -89,7 +89,7 @@ extension PersistentSession { func fetchLocalQuakesQuery() -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { + ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { { dispatcher, selector in try await self.fetchLocalQuakesQuery( dispatcher: dispatcher, @@ -101,7 +101,7 @@ extension PersistentSession { extension PersistentSession { private func didFetchRemoteQuakesMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector, inserted: Array, updated: Array, @@ -123,7 +123,7 @@ extension PersistentSession { ) -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { + ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { { dispatcher, selector in try await self.didFetchRemoteQuakesMutation( dispatcher: dispatcher, @@ -138,7 +138,7 @@ extension PersistentSession { extension PersistentSession { private func deleteLocalQuakeMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector, quakeId: Quake.ID ) async throws { @@ -150,7 +150,7 @@ extension PersistentSession { func deleteLocalQuakeMutation(quakeId: Quake.ID) async throws -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { + ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { { dispatcher, selector in try await self.deleteLocalQuakeMutation( dispatcher: dispatcher, @@ -163,7 +163,7 @@ extension PersistentSession { extension PersistentSession { private func deleteLocalQuakesMutation( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector ) async throws { try await self.localStore.deleteLocalQuakesMutation() @@ -174,7 +174,7 @@ extension PersistentSession { func deleteLocalQuakesMutation() -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { + ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { { dispatcher, selector in try await self.deleteLocalQuakesMutation( dispatcher: dispatcher, @@ -186,7 +186,7 @@ extension PersistentSession { extension PersistentSession { private func fetchRemoteQuakesQuery( - dispatcher: some ImmutableData.Dispatcher, + dispatcher: some ImmutableData.Dispatcher, selector: some ImmutableData.Selector, range: QuakesAction.UI.QuakeList.RefreshQuakesRange ) async throws { @@ -231,7 +231,7 @@ extension PersistentSession { ) -> @Sendable ( Dispatcher, Selector - ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { + ) async throws -> Void where Dispatcher: ImmutableData.Dispatcher, Selector: ImmutableData.Selector { { dispatcher, selector in try await self.fetchRemoteQuakesQuery( dispatcher: dispatcher, diff --git a/Samples/QuakesData/Sources/QuakesData/QuakesReducer.swift b/Samples/QuakesData/Sources/QuakesData/QuakesReducer.swift index d23d9b6..3cabf3c 100644 --- a/Samples/QuakesData/Sources/QuakesData/QuakesReducer.swift +++ b/Samples/QuakesData/Sources/QuakesData/QuakesReducer.swift @@ -18,7 +18,7 @@ public enum QuakesReducer { @Sendable public static func reduce( state: QuakesState, action: QuakesAction - ) throws -> QuakesState { + ) throws(QuakesReducer.Error) -> QuakesState { switch action { case .ui(.quakeList(action: let action)): return try self.reduce(state: state, action: action) @@ -32,7 +32,7 @@ extension QuakesReducer { private static func reduce( state: QuakesState, action: QuakesAction.UI.QuakeList - ) throws -> QuakesState { + ) throws(QuakesReducer.Error) -> QuakesState { switch action { case .onAppear: return self.onAppear(state: state) @@ -69,12 +69,12 @@ extension QuakesReducer { } extension QuakesReducer { - package struct Error: Swift.Error { - package enum Code: Hashable, Sendable { + public struct Error: Swift.Error { + public enum Code: Hashable, Sendable { case quakeNotFound } - package let code: Self.Code + public let code: Self.Code } } @@ -82,7 +82,7 @@ extension QuakesReducer { private static func deleteSelectedQuake( state: QuakesState, quakeId: Quake.ID - ) throws -> QuakesState { + ) throws(QuakesReducer.Error) -> QuakesState { guard let _ = state.quakes.data[quakeId] else { throw Error(code: .quakeNotFound) } @@ -104,7 +104,7 @@ extension QuakesReducer { private static func reduce( state: QuakesState, action: QuakesAction.Data.PersistentSession - ) throws -> QuakesState { + ) throws(QuakesReducer.Error) -> QuakesState { switch action { case .localStore(.didFetchQuakes(result: let result)): return self.didFetchQuakes(state: state, result: result) diff --git a/Samples/QuakesData/Tests/QuakesDataTests/ListenerTests.swift b/Samples/QuakesData/Tests/QuakesDataTests/ListenerTests.swift index d202a12..13944ab 100644 --- a/Samples/QuakesData/Tests/QuakesDataTests/ListenerTests.swift +++ b/Samples/QuakesData/Tests/QuakesDataTests/ListenerTests.swift @@ -163,7 +163,7 @@ final fileprivate class DidFetchRemoteQuakesMutationLocalStoreTestDouble : @unch } extension StoreTestDouble : ImmutableData.Dispatcher { - func dispatch(action: Action) throws { + func dispatch(action: Action) throws(QuakesReducer.Error) { self.parameterAction.append(action) } diff --git a/Samples/QuakesUI/Sources/QuakesUI/Dispatch.swift b/Samples/QuakesUI/Sources/QuakesUI/Dispatch.swift index f922c25..02c7a28 100644 --- a/Samples/QuakesUI/Sources/QuakesUI/Dispatch.swift +++ b/Samples/QuakesUI/Sources/QuakesUI/Dispatch.swift @@ -26,7 +26,7 @@ import SwiftUI } - var wrappedValue: (QuakesAction) throws -> Void { + var wrappedValue: (QuakesAction) throws(QuakesReducer.Error) -> Void { self.dispatcher.dispatch } } diff --git a/Samples/QuakesUI/Sources/QuakesUI/PreviewStore.swift b/Samples/QuakesUI/Sources/QuakesUI/PreviewStore.swift index 09fd300..7e3f1db 100644 --- a/Samples/QuakesUI/Sources/QuakesUI/PreviewStore.swift +++ b/Samples/QuakesUI/Sources/QuakesUI/PreviewStore.swift @@ -20,7 +20,7 @@ import QuakesData import SwiftUI @MainActor struct PreviewStore where Content : View { - @State private var store: ImmutableData.Store = { + @State private var store: ImmutableData.Store = { do { let store = ImmutableData.Store( initialState: QuakesState(), diff --git a/Samples/QuakesUI/Sources/QuakesUI/Select.swift b/Samples/QuakesUI/Sources/QuakesUI/Select.swift index c4ff8a1..9a065d2 100644 --- a/Samples/QuakesUI/Sources/QuakesUI/Select.swift +++ b/Samples/QuakesUI/Sources/QuakesUI/Select.swift @@ -39,7 +39,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, outputSelector: @escaping @Sendable (Store.State) -> Output - ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { + ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { self.init( id: id, label: label, @@ -56,7 +56,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat @escaping @Sendable (Store.State) -> each Dependency, outputSelector: @escaping @Sendable (Store.State) -> Output - ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { + ) where Store == ImmutableData.Store, repeat each Dependency : Equatable, Output : Equatable { self.init( label: label, filter: isIncluded, @@ -67,7 +67,7 @@ extension ImmutableUI.Selector { } @MainActor @propertyWrapper struct SelectQuakes: DynamicProperty { - @ImmutableUI.Selector, TreeDictionary> var wrappedValue: TreeDictionary + @ImmutableUI.Selector, TreeDictionary> var wrappedValue: TreeDictionary init( searchText: String, @@ -96,7 +96,7 @@ extension SelectQuakes { } @MainActor @propertyWrapper struct SelectQuakesValues: DynamicProperty { - @ImmutableUI.Selector, TreeDictionary, Array> var wrappedValue: Array + @ImmutableUI.Selector, TreeDictionary, Array> var wrappedValue: Array init( searchText: String, @@ -137,7 +137,7 @@ extension SelectQuakesValues { } @MainActor @propertyWrapper struct SelectQuakesCount: DynamicProperty { - @ImmutableUI.Selector, Int>( + @ImmutableUI.Selector, Int>( label: "SelectQuakesCount", outputSelector: QuakesState.selectQuakesCount() ) var wrappedValue: Int @@ -148,7 +148,7 @@ extension SelectQuakesValues { } @MainActor @propertyWrapper struct SelectQuakesStatus: DynamicProperty { - @ImmutableUI.Selector, Status?>( + @ImmutableUI.Selector, Status?>( label: "SelectQuakesStatus", outputSelector: QuakesState.selectQuakesStatus() ) var wrappedValue: Status? @@ -159,7 +159,7 @@ extension SelectQuakesValues { } @MainActor @propertyWrapper struct SelectQuake: DynamicProperty { - @ImmutableUI.Selector, Quake?> var wrappedValue: Quake? + @ImmutableUI.Selector, Quake?> var wrappedValue: Quake? init(quakeId: String?) { self._wrappedValue = ImmutableUI.Selector( diff --git a/Samples/QuakesUI/Sources/QuakesUI/StoreKey.swift b/Samples/QuakesUI/Sources/QuakesUI/StoreKey.swift index 1058701..2dee5a8 100644 --- a/Samples/QuakesUI/Sources/QuakesUI/StoreKey.swift +++ b/Samples/QuakesUI/Sources/QuakesUI/StoreKey.swift @@ -27,7 +27,7 @@ extension ImmutableUI.Provider { public init( _ store: Store, @ViewBuilder content: () -> Content - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, store, @@ -37,7 +37,7 @@ extension ImmutableUI.Provider { } extension ImmutableUI.Dispatcher { - public init() where Store == ImmutableData.Store { + public init() where Store == ImmutableData.Store { self.init(\.store) } } @@ -49,7 +49,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat DependencySelector, outputSelector: OutputSelector - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, id: id, @@ -67,7 +67,7 @@ extension ImmutableUI.Selector { filter isIncluded: (@Sendable (Store.State, Store.Action) -> Bool)? = nil, dependencySelector: repeat DependencySelector, outputSelector: OutputSelector - ) where Store == ImmutableData.Store { + ) where Store == ImmutableData.Store { self.init( \.store, label: label, @@ -86,7 +86,7 @@ extension ImmutableUI.Selector { } extension EnvironmentValues { - fileprivate var store: ImmutableData.Store { + fileprivate var store: ImmutableData.Store { get { self[StoreKey.self] }