Skip to content

Commit c271c42

Browse files
authored
Merge pull request #177 from yumemi-inc/codex/setup-wrapper-docs
Introduce Setup alias for Store DSL
2 parents 0189780 + 646b6e5 commit c271c42

File tree

2 files changed

+87
-9
lines changed

2 files changed

+87
-9
lines changed

README.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ It keeps surrounding helper layers intentionally small, so dependencies and feat
6969
- [Middleware](#middleware)
7070
- [Logging](#logging)
7171
- [Message](#message)
72+
- [Project-specific AppStore Wrapper](#project-specific-appstore-wrapper)
7273
- [Testing Store](#testing-store)
7374

7475
## Installation
@@ -207,7 +208,7 @@ class CounterStore(
207208
counterRepository: CounterRepository,
208209
): Store<CounterState, CounterAction, CounterEvent> by Store(
209210
initialState = CounterState(count = 0),
210-
configure = {
211+
setup = {
211212
state<CounterState> {
212213
// ...
213214
}
@@ -886,6 +887,78 @@ val mainStore: Store<MainState, MainAction, MainEvent> = Store {
886887
```
887888
</details>
888889
890+
## Project-specific AppStore Wrapper
891+
892+
In larger projects, it can be useful to wrap `Store(...)` in a project-specific `AppStore(...)`.
893+
894+
This allows you to centralize shared behavior such as common middleware, exception handling, and state persistence.
895+
It also gives you a place to prepare an extra setup hook for testing and debugging.
896+
897+
```kt
898+
fun <S : State, A : Action, E : Event> AppStore(
899+
initialState: S,
900+
extraSetup: Setup<S, A, E> = {},
901+
setup: Setup<S, A, E>,
902+
): Store<S, A, E> = Store(initialState) {
903+
middleware(AppLoggingMiddleware())
904+
exceptionHandler(AppExceptionHandler)
905+
906+
setup()
907+
extraSetup()
908+
}
909+
```
910+
911+
A feature Store can then focus on its own state transitions and actions:
912+
913+
```kt
914+
fun CounterStore(
915+
counterRepository: CounterRepository,
916+
extraSetup: Setup<CounterState, CounterAction, CounterEvent> = {},
917+
): Store<CounterState, CounterAction, CounterEvent> = AppStore(
918+
initialState = CounterState(count = 0),
919+
extraSetup = extraSetup,
920+
) {
921+
state<CounterState> {
922+
action<CounterAction.Increment> {
923+
val count = state.count + 1
924+
counterRepository.set(count)
925+
nextState(state.copy(count = count))
926+
}
927+
}
928+
}
929+
```
930+
931+
For tests or debug builds, you can inject only the additional behavior you need:
932+
933+
```kt
934+
val recordedEvents = mutableListOf<CounterEvent>()
935+
936+
val store = CounterStore(
937+
counterRepository = repository,
938+
extraSetup = {
939+
coroutineContext(testDispatcher)
940+
middleware(
941+
Middleware(
942+
afterEventEmit = { _, event ->
943+
recordedEvents += event
944+
},
945+
),
946+
)
947+
},
948+
)
949+
```
950+
951+
This pattern is useful for:
952+
953+
- applying project-wide middleware and exception handling
954+
- injecting test- or debug-only middleware
955+
- overriding `coroutineContext` in tests
956+
- keeping feature Store definitions focused on business logic
957+
958+
Avoid using `extraSetup` to redefine `state {}` or `anyState {}` handlers, because handler selection depends on registration order.
959+
960+
Also note that middleware execution order should not be relied on.
961+
889962
## Testing Store
890963
891964
Tart's architecture makes writing unit tests for your *Store* straightforward.

tart-core/src/commonMain/kotlin/io/yumemi/tart/core/StoreBuilder.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,32 +363,37 @@ class StoreBuilder<S : State, A : Action, E : Event> internal constructor() {
363363
}
364364

365365
/**
366-
* Creates a Store instance with the specified initial state and optional configuration.
366+
* Store DSL setup block.
367+
*/
368+
typealias Setup<S, A, E> = StoreBuilder<S, A, E>.() -> Unit
369+
370+
/**
371+
* Creates a Store instance with the specified initial state and optional setup.
367372
*
368373
* @param initialState The initial state of the store
369-
* @param configure Optional configuration block to customize the store
374+
* @param setup Optional setup block to customize the store
370375
* @return A configured Store instance
371376
*/
372377
fun <S : State, A : Action, E : Event> Store(
373378
initialState: S,
374-
configure: StoreBuilder<S, A, E>.() -> Unit,
379+
setup: Setup<S, A, E>,
375380
): Store<S, A, E> {
376381
return StoreBuilder<S, A, E>().apply {
377382
initialState(initialState)
378-
configure()
383+
setup()
379384
}.build()
380385
}
381386

382387
/**
383-
* Creates a Store instance with configuration provided in the block.
388+
* Creates a Store instance with setup provided in the block.
384389
* The initial state must be set within the block using initialState().
385390
*
386-
* @param configure Configuration block to customize the store
391+
* @param setup Setup block to customize the store
387392
* @return A configured Store instance
388393
* @throws IllegalArgumentException if the initial state is not set in the block
389394
*/
390395
fun <S : State, A : Action, E : Event> Store(
391-
configure: StoreBuilder<S, A, E>.() -> Unit,
396+
setup: Setup<S, A, E>,
392397
): Store<S, A, E> {
393-
return StoreBuilder<S, A, E>().apply(configure).build()
398+
return StoreBuilder<S, A, E>().apply(setup).build()
394399
}

0 commit comments

Comments
 (0)