Skip to content

Commit 4173d31

Browse files
committed
add Document
1 parent 1342395 commit 4173d31

File tree

7 files changed

+134
-24
lines changed

7 files changed

+134
-24
lines changed

Sources/SimplexArchitecture/Internal/ActionTransition.swift

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import Foundation
22

3+
/// ``ActionTransition`` represents a transition between states in a reducer. It captures the previous and next states, the associated side effect,effect context, and the action triggering the transition.
34
struct ActionTransition<Reducer: ReducerProtocol> {
5+
/// Represents a state. It includes the target state and the reducer state.
46
struct State {
57
let state: Reducer.Target.States?
68
let reducerState: Reducer.ReducerState?
79
}
8-
10+
/// The previous state.
911
let previous: Self.State
12+
/// The next state.
1013
let next: Self.State
14+
/// The associated side effect.
1115
let effect: SideEffect<Reducer>
16+
/// The unique effect context that represents parent effect.
1217
let effectContext: UUID
18+
/// The Action that cause a change of state
1319
let action: CombineAction<Reducer>
1420

21+
/// - Parameters:
22+
/// - previous: The previous state.
23+
/// - next: The next state.
24+
/// - effect: The unique effect context that represents parent effect.
25+
/// - effectContext: The unique effect context that represents parent effect.
26+
/// - action: The action responsible for the transition.
1527
init(
1628
previous: Self.State,
1729
next: Self.State,
@@ -26,15 +38,30 @@ struct ActionTransition<Reducer: ReducerProtocol> {
2638
self.action = action
2739
}
2840

41+
/// Converts the `ActionTransition` to a `StateContainer` representing the next state.
42+
///
43+
/// - Parameter target: The target reducer.
44+
/// - Returns: A `StateContainer` representing the next state.
2945
func asNextStateContainer(from target: Reducer.Target) -> StateContainer<Reducer.Target> {
3046
asStateContainer(from: target, state: next)
3147
}
3248

49+
/// Converts the `ActionTransition` to a `StateContainer` representing the previous state.
50+
///
51+
/// - Parameter target: The target reducer.
52+
/// - Returns: A `StateContainer` representing the previous state.
3353
func asPreviousStateContainer(from target: Reducer.Target) -> StateContainer<Reducer.Target> {
3454
asStateContainer(from: target, state: previous)
3555
}
3656

37-
private func asStateContainer(from target: Reducer.Target, state: Self.State) -> StateContainer<Reducer.Target> {
38-
.init(target, states: state.state, reducerState: state.reducerState)
57+
private func asStateContainer(
58+
from target: Reducer.Target,
59+
state: Self.State
60+
) -> StateContainer<Reducer.Target> {
61+
.init(
62+
target,
63+
states: state.state,
64+
reducerState: state.reducerState
65+
)
3966
}
4067
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22

3+
/// Enum to determine parent's Effect
34
enum EffectContext {
45
@TaskLocal static var id: UUID?
56
}

Sources/SimplexArchitecture/Internal/Task+withEffectContext.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22

33
extension Task where Failure == any Error {
4+
// Granted when there is no EffectContext in the Task.
45
@discardableResult
56
static func withEffectContext(
67
priority: TaskPriority? = nil,
@@ -19,6 +20,7 @@ extension Task where Failure == any Error {
1920
}
2021

2122
extension Task where Failure == Never {
23+
// Granted when there is no EffectContext in the Task.
2224
@discardableResult
2325
static func withEffectContext(
2426
priority: TaskPriority? = nil,

Sources/SimplexArchitecture/Send.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// A type that can send actions back into the system when used from run(priority:operation:catch:fileID:line:).
12
public struct Send<Reducer: ReducerProtocol>: Sendable {
23
@usableFromInline
34
let sendAction: @Sendable (Reducer.Action) -> SendTask
@@ -25,12 +26,14 @@ public struct Send<Reducer: ReducerProtocol>: Sendable {
2526
sendReducerAction(action)
2627
}
2728

29+
/// Sends an action back into the system from an effect.
2830
@MainActor
2931
@inlinable
3032
public func callAsFunction(_ action: Reducer.Action) async {
3133
await sendAction(action).wait()
3234
}
3335

36+
/// Sends an reducer action back into the system from an effect.
3437
@_disfavoredOverload
3538
@MainActor
3639
@inlinable

Sources/SimplexArchitecture/StateContainer.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Foundation
22
import XCTestDynamicOverlay
33

4-
// StateContainer is not thread-safe. Therefore, StateContainer must use NSLock or NSRecursiveLock for exclusions when changing values.
5-
// In Send.swift, NSRecursiveLock is used for exclusions when executing the `reduce(into:action)`.
4+
/// StateContainer is not thread-safe. Therefore, StateContainer must use NSLock or NSRecursiveLock for exclusions when changing values.
5+
/// In Store, NSRecursiveLock is used for exclusions when executing the `reduce(into:action)`.
66
@dynamicMemberLookup
77
public final class StateContainer<Target: ActionSendable> {
88
public var reducerState: Target.Reducer.ReducerState {

Sources/SimplexArchitecture/Store/Store+pullback.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Foundation
33

44
// MARK: - Pullback
55

6-
public extension Store {
6+
extension Store {
77
@inlinable
88
func pullback<Parent: ActionSendable>(
99
to casePath: consuming CasePath<Parent.Reducer.Action, Reducer.Action>,

Sources/SimplexArchitecture/TestStore.swift

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ import CustomDump
33
import Dependencies
44
import Foundation
55

6+
/// TestStore is a utility class for testing stores that use Reducer protocols.
7+
/// It provides methods for sending actions and verifying state changes.
68
public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equatable, Reducer.ReducerAction: Equatable {
9+
10+
// MARK: - Properties
11+
12+
/// The running state container.
713
var runningContainer: StateContainer<Reducer.Target>?
14+
15+
/// An array of tested actions.
816
var testedActions: [ActionTransition<Reducer>] = []
917

18+
/// An array of untested actions.
1019
var untestedActions: [ActionTransition<Reducer>] {
1120
target.store.sentFromEffectActions.filter { actionTransition in
1221
!testedActions.contains {
@@ -16,8 +25,15 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
1625
}
1726

1827
let target: Reducer.Target
28+
29+
/// The states of the target.
1930
let states: Reducer.Target.States
2031

32+
/// Initializes a new test store.
33+
///
34+
/// - Parameters:
35+
/// - target: The target Reducer.
36+
/// - states: The states of the target Reducer.
2137
init(
2238
target: Reducer.Target,
2339
states: Reducer.Target.States
@@ -47,6 +63,12 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
4763
}
4864
}
4965

66+
/// Asserts an action was received from an effect and asserts how the state changes.
67+
///
68+
/// - Parameters:
69+
/// - action: An action expected from an effect.
70+
/// - timeout: The amount of time to wait for the expected action.
71+
/// - expected: A closure that asserts state changed by sending the action to the store. The mutable state sent to this closure must be modified to match the state of the store after processing the given action. Do not provide a closure if no change is expected.
5072
public func receive(
5173
_ action: Reducer.ReducerAction,
5274
timeout: TimeInterval = 5,
@@ -63,6 +85,12 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
6385
)
6486
}
6587

88+
/// Asserts an action was received from an effect and asserts how the state changes.
89+
///
90+
/// - Parameters:
91+
/// - action: An action expected from an effect.
92+
/// - timeout: The amount of time to wait for the expected action.
93+
/// - expected: A closure that asserts state changed by sending the action to the store. The mutable state sent to this closure must be modified to match the state of the store after processing the given action. Do not provide a closure if no change is expected.
6694
public func receive(
6795
_ action: Reducer.Action,
6896
timeout: TimeInterval = 5,
@@ -107,9 +135,7 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
107135
break
108136
}
109137

110-
if let firstIndex = untestedActions.firstIndex(where: { $0.action == action }),
111-
let stateTransition = untestedActions[safe: firstIndex]
112-
{
138+
if let stateTransition = untestedActions.first(where: { $0.action == action }) {
113139
let expectedContainer = stateTransition.asNextStateContainer(from: target)
114140
let actualContainer = stateTransition.asPreviousStateContainer(from: target)
115141

@@ -126,7 +152,8 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
126152
}
127153
}
128154

129-
func receiveWithoutStateCheck(
155+
/// Asserts an action was received from an effect. Does not assert state changes.
156+
public func receiveWithoutStateCheck(
130157
_ action: Reducer.Action,
131158
timeout: TimeInterval = 5,
132159
file: StaticString = #file,
@@ -153,9 +180,7 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
153180
break
154181
}
155182

156-
if let firstIndex = untestedActions.firstIndex(where: { $0.action == .action(action) }),
157-
let stateTransition = untestedActions[safe: firstIndex]
158-
{
183+
if let stateTransition = untestedActions.first(where: { $0.action == .action(action) }) {
159184
testedActions.append(stateTransition)
160185
break
161186
}
@@ -164,86 +189,134 @@ public final class TestStore<Reducer: ReducerProtocol> where Reducer.Action: Equ
164189
}
165190
}
166191

192+
/// Sends an action to the store and asserts when state changes.
193+
///
194+
/// - Parameters:
195+
/// - action: An action.
196+
/// - assert: A closure that asserts state changed by sending the action to
197+
/// the store. The mutable state sent to this closure must be modified to match the state of
198+
/// the store after processing the given action. Do not provide a closure if no change is
199+
/// expected.
200+
/// - Returns: A ``SendTask`` that represents the lifecycle of the effect executed when
201+
/// sending the action.
202+
@discardableResult
167203
@MainActor
168204
public func send(
169205
_ action: Reducer.Action,
170206
assert expected: ((StateContainer<Reducer.Target>) -> Void)? = nil
171-
) async {
207+
) async -> SendTask {
172208
let expectedContainer = target.store.setContainerIfNeeded(for: target, states: states)
173209
runningContainer = expectedContainer
174210
let actualContainer = expectedContainer.copy()
175211

176-
target.store.sendIfNeeded(action)
212+
let sendTask = target.store.sendIfNeeded(action)
177213

178214
expected?(actualContainer)
179215

180216
assertStatesNoDifference(expected: expectedContainer, actual: actualContainer)
181217
assertReducerNoDifference(expected: expectedContainer, actual: actualContainer)
182218

183219
await Task.megaYield()
220+
return sendTask
184221
}
185222

223+
/// Sends an action to the store and asserts when state changes.
224+
///
225+
/// - Parameters:
226+
/// - action: An action.
227+
/// - assert: A closure that asserts state changed by sending the action to
228+
/// the store. The mutable state sent to this closure must be modified to match the state of
229+
/// the store after processing the given action. Do not provide a closure if no change is
230+
/// expected.
231+
/// - Returns: A ``SendTask`` that represents the lifecycle of the effect executed when
232+
/// sending the action.
233+
@discardableResult
186234
@MainActor
187235
public func send(
188236
_ action: Reducer.Action,
189237
assert expected: ((StateContainer<Reducer.Target>) -> Void)? = nil
190-
) async where Reducer.Target.States: Equatable {
238+
) async -> SendTask where Reducer.Target.States: Equatable {
191239
let expectedContainer = target.store.setContainerIfNeeded(for: target, states: states)
192240
runningContainer = expectedContainer
193241
let actualContainer = expectedContainer.copy()
194242

195-
target.store.sendIfNeeded(action)
243+
let sendTask = target.store.sendIfNeeded(action)
196244

197245
expected?(actualContainer)
198246

199247
assertStatesNoDifference(expected: expectedContainer, actual: actualContainer)
200248
assertReducerNoDifference(expected: expectedContainer, actual: actualContainer)
201249

202250
await Task.megaYield()
251+
252+
return sendTask
203253
}
204254

255+
/// Sends an action to the store and asserts when state changes.
256+
///
257+
/// - Parameters:
258+
/// - action: An action.
259+
/// - assert: A closure that asserts state changed by sending the action to
260+
/// the store. The mutable state sent to this closure must be modified to match the state of
261+
/// the store after processing the given action. Do not provide a closure if no change is
262+
/// expected.
263+
/// - Returns: A ``SendTask`` that represents the lifecycle of the effect executed when
264+
/// sending the action.
265+
@discardableResult
205266
@MainActor
206267
public func send(
207268
_ action: Reducer.Action,
208269
assert expected: ((StateContainer<Reducer.Target>) -> Void)? = nil
209-
) async where Reducer.ReducerState: Equatable {
270+
) async -> SendTask where Reducer.ReducerState: Equatable {
210271
let expectedContainer = target.store.setContainerIfNeeded(for: target, states: states)
211272
runningContainer = expectedContainer
212273
let actualContainer = expectedContainer.copy()
213274

214-
target.store.sendIfNeeded(action)
275+
let sendTask = target.store.sendIfNeeded(action)
215276

216277
expected?(actualContainer)
217278

218279
assertStatesNoDifference(expected: expectedContainer, actual: actualContainer)
219280
assertReducerNoDifference(expected: expectedContainer, actual: actualContainer)
220281

221282
await Task.megaYield()
283+
return sendTask
222284
}
223285

286+
/// Sends an action to the store and asserts when state changes.
287+
///
288+
/// - Parameters:
289+
/// - action: An action.
290+
/// - assert: A closure that asserts state changed by sending the action to
291+
/// the store. The mutable state sent to this closure must be modified to match the state of
292+
/// the store after processing the given action. Do not provide a closure if no change is
293+
/// expected.
294+
/// - Returns: A ``SendTask`` that represents the lifecycle of the effect executed when
295+
/// sending the action.
296+
@discardableResult
224297
@MainActor
225298
public func send(
226299
_ action: Reducer.Action,
227300
assert expected: ((StateContainer<Reducer.Target>) -> Void)? = nil
228-
) async where Reducer.ReducerState: Equatable, Reducer.Target.States: Equatable {
301+
) async -> SendTask where Reducer.ReducerState: Equatable, Reducer.Target.States: Equatable {
229302
let expectedContainer = target.store.setContainerIfNeeded(for: target, states: states)
230303
runningContainer = expectedContainer
231304
let actualContainer = expectedContainer.copy()
232305

233-
target.store.sendIfNeeded(action)
306+
let sendTask = target.store.sendIfNeeded(action)
234307

235308
expected?(actualContainer)
236309

237310
assertStatesNoDifference(expected: actualContainer, actual: actualContainer)
238311
assertReducerNoDifference(expected: actualContainer, actual: actualContainer)
239312

240313
await Task.megaYield()
314+
315+
return sendTask
241316
}
242317
}
243318

244-
// MARK: - +Assert
245-
246-
private extension TestStore {
319+
extension TestStore {
247320
private func assertStatesNoDifference(
248321
expected expectedContainer: StateContainer<Reducer.Target>,
249322
actual actualContainer: StateContainer<Reducer.Target>
@@ -294,6 +367,10 @@ private extension TestStore {
294367
}
295368

296369
public extension ActionSendable where Reducer.Action: Equatable, Reducer.ReducerAction: Equatable {
370+
/// Creates and returns a new test store.
371+
///
372+
/// - Parameter states: The initial states for testing.
373+
/// - Returns: A new TestStore instance.
297374
func testStore(states: States) -> TestStore<Reducer> {
298375
TestStore(target: self, states: states)
299376
}

0 commit comments

Comments
 (0)