Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ val store: Store<CounterState, CounterAction, CounterEvent> = Store {
)

// add multiple Middlewares
middlewares(..., ...)
middleware(..., ...)
}
```

Expand Down Expand Up @@ -898,19 +898,21 @@ val mainStore: Store<MainState, MainAction, MainEvent> = Store {
In larger projects, it can be useful to wrap `Store(...)` in a project-specific `AppStore(...)`.

This allows you to centralize shared behavior such as common middleware, exception handling, and state persistence.
It also gives you a place to prepare an extra setup hook for testing and debugging.
It also gives you a place to apply non-state overrides for testing and debugging.

```kt
fun <S : State, A : Action, E : Event> AppStore(
initialState: S,
extraSetup: Setup<S, A, E> = {},
overrides: Overrides<S, A, E> = {},
setup: Setup<S, A, E>,
): Store<S, A, E> = Store(initialState) {
): Store<S, A, E> = Store(
initialState = initialState,
overrides = overrides,
) {
middleware(AppLoggingMiddleware())
exceptionHandler(AppExceptionHandler)

setup()
extraSetup()
}
```

Expand All @@ -919,10 +921,10 @@ A feature Store can then focus on its own state transitions and actions:
```kt
fun CounterStore(
counterRepository: CounterRepository,
extraSetup: Setup<CounterState, CounterAction, CounterEvent> = {},
overrides: Overrides<CounterState, CounterAction, CounterEvent> = {},
): Store<CounterState, CounterAction, CounterEvent> = AppStore(
initialState = CounterState(count = 0),
extraSetup = extraSetup,
overrides = overrides,
) {
state<CounterState> {
action<CounterAction.Increment> {
Expand All @@ -941,8 +943,9 @@ val recordedEvents = mutableListOf<CounterEvent>()

val store = CounterStore(
counterRepository = repository,
extraSetup = {
overrides = {
coroutineContext(testDispatcher)
clearMiddlewares()
middleware(
Middleware(
afterEventEmit = { _, event ->
Expand All @@ -954,15 +957,18 @@ val store = CounterStore(
)
```

Use `replaceMiddlewares(...)` inside `overrides {}` when you want to replace the default middleware set.
Use `clearMiddlewares()` when you want to remove all configured middleware.

This pattern is useful for:

- applying project-wide middleware and exception handling
- injecting test- or debug-only middleware
- overriding `stateSaver` or `exceptionHandler` for tests
- overriding `coroutineContext` in tests
- clearing or replacing default middleware via `clearMiddlewares()` / `replaceMiddlewares(...)`
- keeping feature Store definitions focused on business logic

Avoid using `extraSetup` to redefine `state {}` or `anyState {}` handlers, because handler selection depends on registration order.

Also note that middleware execution order should not be relied on.

## Testing Store
Expand Down
124 changes: 105 additions & 19 deletions tart-core/src/commonMain/kotlin/io/yumemi/tart/core/StoreBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,24 @@ class StoreBuilder<S : State, A : Action, E : Event> internal constructor() {
}

/**
* Adds a single middleware instance to the store.
* Adds one or more middleware instances to the store.
*
* @param middleware The middleware instance to add
* @param first The first middleware instance to add
* @param rest Additional middleware instances to add
*/
fun middleware(middleware: Middleware<S, A, E>) {
storeMiddlewares.add(middleware)
fun middleware(first: Middleware<S, A, E>, vararg rest: Middleware<S, A, E>) {
storeMiddlewares.add(first)
storeMiddlewares.addAll(rest)
}

/**
* Adds multiple middleware instances to the store.
*
* @param middleware Array of middleware instances to add
*/
fun middlewares(vararg middleware: Middleware<S, A, E>) {
storeMiddlewares.addAll(middleware)
internal fun clearMiddlewares() {
storeMiddlewares.clear()
}

internal fun replaceMiddlewares(first: Middleware<S, A, E>, vararg rest: Middleware<S, A, E>) {
clearMiddlewares()
storeMiddlewares.add(first)
storeMiddlewares.addAll(rest)
}

class StateHandler<P, SC : StoreScope>(
Expand Down Expand Up @@ -368,32 +371,115 @@ class StoreBuilder<S : State, A : Action, E : Event> internal constructor() {
typealias Setup<S, A, E> = StoreBuilder<S, A, E>.() -> Unit

/**
* Creates a Store instance with the specified initial state and optional setup.
* Store overrides block applied after the main Store setup.
* This block is limited to non-state configuration such as coroutine context,
* persistence, exception handling, and middleware.
*/
typealias Overrides<S, A, E> = StoreOverridesBuilder<S, A, E>.() -> Unit

/**
* DSL for overriding Store configuration after the main setup has been applied.
* This DSL intentionally does not expose state/action handler APIs.
*/
@Suppress("unused")
@TartStoreDsl
class StoreOverridesBuilder<S : State, A : Action, E : Event> internal constructor() {
private val operations = mutableListOf<StoreBuilder<S, A, E>.() -> Unit>()

/**
* Overrides the coroutine context for store operations.
*/
fun coroutineContext(coroutineContext: CoroutineContext) {
operations.add { coroutineContext(coroutineContext) }
}

/**
* Overrides the state saver used by the store.
*/
fun stateSaver(stateSaver: StateSaver<S>) {
operations.add { stateSaver(stateSaver) }
}

/**
* Overrides the exception handler used by the store.
*/
fun exceptionHandler(exceptionHandler: ExceptionHandler) {
operations.add { exceptionHandler(exceptionHandler) }
}

/**
* Adds one or more middleware instances after the main Store setup.
*/
fun middleware(first: Middleware<S, A, E>, vararg rest: Middleware<S, A, E>) {
val restValues = rest.copyOf()
operations.add { middleware(first, *restValues) }
}

/**
* Replaces all middleware instances configured so far.
* This can be used to remove default middleware in tests or debug setups.
*/
fun replaceMiddlewares(first: Middleware<S, A, E>, vararg rest: Middleware<S, A, E>) {
val restValues = rest.copyOf()
operations.add { replaceMiddlewares(first, *restValues) }
}

/**
* Clears all middleware instances configured so far.
* This can be used to remove default middleware in tests or debug setups.
*/
fun clearMiddlewares() {
operations.add { clearMiddlewares() }
}

internal fun applyTo(builder: StoreBuilder<S, A, E>) {
operations.forEach { operation -> operation(builder) }
}
}

private fun <S : State, A : Action, E : Event> buildStore(
initialState: S? = null,
overrides: Overrides<S, A, E>? = null,
setup: Setup<S, A, E>,
): Store<S, A, E> {
val builder = StoreBuilder<S, A, E>()
initialState?.let(builder::initialState)
builder.setup()
overrides?.let {
StoreOverridesBuilder<S, A, E>().apply(it).applyTo(builder)
}
return builder.build()
}

/**
* Creates a Store instance with the specified initial state and optional overrides.
* Overrides are applied after the main setup block.
*
* @param initialState The initial state of the store
* @param setup Optional setup block to customize the store
* @param overrides Overrides block for non-state Store configuration
* @param setup Setup block to customize the store
* @return A configured Store instance
*/
fun <S : State, A : Action, E : Event> Store(
initialState: S,
overrides: Overrides<S, A, E> = {},
setup: Setup<S, A, E>,
): Store<S, A, E> {
return StoreBuilder<S, A, E>().apply {
initialState(initialState)
setup()
}.build()
return buildStore(initialState = initialState, overrides = overrides, setup = setup)
}

/**
* Creates a Store instance with setup provided in the block.
* Creates a Store instance with setup provided in the block and optional overrides.
* The initial state must be set within the block using initialState().
*
* @param overrides Overrides block for non-state Store configuration
* @param setup Setup block to customize the store
* @return A configured Store instance
* @throws IllegalArgumentException if the initial state is not set in the block
*/
fun <S : State, A : Action, E : Event> Store(
overrides: Overrides<S, A, E> = {},
setup: Setup<S, A, E>,
): Store<S, A, E> {
return StoreBuilder<S, A, E>().apply(setup).build()
return buildStore(overrides = overrides, setup = setup)
}
Loading
Loading