chore: implementiere Wizard-Framework mit State- und Flow-Logik sowie Feature-Flags für Migration

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-21 17:01:23 +02:00
parent ec124e9acd
commit 237c71e5a0
4 changed files with 162 additions and 0 deletions
@@ -0,0 +1,10 @@
package at.mocode.frontend.core.domain.config
/**
* Zentrale Feature-Flags für die Wizard-Orchestrator-Migration.
* Standard: AUS, damit bestehende Logik aktiv bleibt.
*/
object WizardFeatureFlags {
// Kann später via Settings/DI überschrieben werden
var WizardRuntimeEnabled: Boolean = false
}
@@ -0,0 +1,81 @@
package at.mocode.frontend.core.wizard.dsl
import at.mocode.frontend.core.wizard.runtime.*
class FlowBuilder<S : StepId, A>(private val start: S) {
internal val steps = mutableMapOf<S, StepDef<S, A>>()
fun step(id: S, block: StepBuilder<S, A>.() -> Unit) {
val sb = StepBuilder<S, A>()
sb.block()
steps[id] = StepDef(
onEnter = sb.onEnter,
onLeave = sb.onLeave,
transitions = sb.transitions.toList(),
otherwise = sb.otherwise
)
}
fun build(): WizardRuntime<S, A> = SimpleWizardRuntime(start, steps.toMap())
}
class StepBuilder<S : StepId, A> {
internal var onEnter: (suspend (WizardContext, WizardState<S, A>) -> Unit)? = null
internal var onLeave: (suspend (WizardContext, WizardState<S, A>) -> Unit)? = null
internal val transitions = mutableListOf<Transition<S>>()
internal var otherwise: S? = null
fun onEnter(block: suspend (WizardContext, WizardState<S, A>) -> Unit) {
onEnter = block
}
fun onLeave(block: suspend (WizardContext, WizardState<S, A>) -> Unit) {
onLeave = block
}
fun whenGuard(id: String, g: Guard<S, A>, go: S) {
transitions += Transition(id = id, target = go, guard = g)
}
fun otherwise(go: S) {
otherwise = go
}
}
internal data class StepDef<S : StepId, A>(
val onEnter: (suspend (WizardContext, WizardState<S, A>) -> Unit)? = null,
val onLeave: (suspend (WizardContext, WizardState<S, A>) -> Unit)? = null,
val transitions: List<Transition<S>> = emptyList(),
val otherwise: S? = null
)
private class SimpleWizardRuntime<S : StepId, A>(
override val start: S,
private val steps: Map<S, StepDef<S, A>>
) : WizardRuntime<S, A> {
override fun next(ctx: WizardContext, state: WizardState<S, A>): WizardState<S, A> {
val def = steps[state.current]
if (def == null) return state // unknown step, no-op
val target = def.transitions.firstOrNull { tr ->
@Suppress("UNCHECKED_CAST")
(tr.guard as? Guard<S, A>)?.invoke(ctx, state.acc) ?: false
}?.target ?: def.otherwise ?: state.current
if (target == state.current) return state
return state.copy(current = target, history = state.history + state.current)
}
override fun back(state: WizardState<S, A>): WizardState<S, A> {
val history = state.history
if (history.isEmpty()) return state
val prev = history.last()
return state.copy(current = prev, history = history.dropLast(1))
}
}
fun <S : StepId, A> flow(start: S, build: FlowBuilder<S, A>.() -> Unit): WizardRuntime<S, A> {
val fb = FlowBuilder<S, A>(start)
fb.apply(build)
return fb.build()
}
@@ -0,0 +1,41 @@
package at.mocode.frontend.core.wizard.runtime
import at.mocode.frontend.core.domain.repository.MasterdataStats
import at.mocode.frontend.core.navigation.AppScreen
// Base marker for step IDs (sealed interface implementations per flow)
interface StepId
data class WizardContext(
val origin: AppScreen,
val role: String? = null,
val isOnline: Boolean = true,
val stats: MasterdataStats? = null
)
data class WizardState<S : StepId, A>(
val current: S,
val history: List<S> = emptyList(),
val acc: A,
val errors: List<String> = emptyList()
)
typealias Guard<S, A> = (WizardContext, A) -> Boolean
data class Transition<S : StepId>(
val id: String,
val target: S,
val guard: Guard<S, *>? = null
)
interface StepEffects<S : StepId, A> {
suspend fun onEnter(ctx: WizardContext, state: WizardState<S, A>) {}
suspend fun onLeave(ctx: WizardContext, state: WizardState<S, A>) {}
suspend fun onComplete(ctx: WizardContext, state: WizardState<S, A>) {}
}
interface WizardRuntime<S : StepId, A> {
val start: S
fun next(ctx: WizardContext, state: WizardState<S, A>): WizardState<S, A>
fun back(state: WizardState<S, A>): WizardState<S, A>
}
@@ -0,0 +1,30 @@
package at.mocode.frontend.core.wizard.samples
import at.mocode.frontend.core.navigation.AppScreen
import at.mocode.frontend.core.wizard.dsl.flow
import at.mocode.frontend.core.wizard.runtime.Guard
import at.mocode.frontend.core.wizard.runtime.StepId
import at.mocode.frontend.core.wizard.runtime.WizardState
// Minimaler, selbstenthaltener Demo-Flow (2 Steps) für den Spike
sealed interface DemoEventStep : StepId {
data object ZnsCheck : DemoEventStep
data object VeranstalterSelection : DemoEventStep
}
data class DemoEventAcc(val dummy: String = "")
object DemoEventGuards {
val hasZns: Guard<DemoEventStep, DemoEventAcc> = { ctx, _ -> (ctx.stats?.vereinCount ?: 0) > 0 }
}
val DemoEventFlow = flow<DemoEventStep, DemoEventAcc>(start = DemoEventStep.ZnsCheck) {
step(DemoEventStep.ZnsCheck) {
whenGuard("hasZns", DemoEventGuards.hasZns, go = DemoEventStep.VeranstalterSelection)
otherwise(DemoEventStep.VeranstalterSelection)
}
}
// Hilfsfunktion für einfache manuelle Nutzung im Spike
fun demoStartState(origin: AppScreen, acc: DemoEventAcc = DemoEventAcc()): WizardState<DemoEventStep, DemoEventAcc> =
WizardState(current = DemoEventStep.ZnsCheck, acc = acc)