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:
+10
@@ -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
|
||||
}
|
||||
+81
@@ -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()
|
||||
}
|
||||
+41
@@ -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>
|
||||
}
|
||||
+30
@@ -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)
|
||||
Reference in New Issue
Block a user