The reflection DSL provides a declarative way to define state machines using Go struct tags. This approach offers a more compact, readable syntax for machine definitions.
package main
import (
"fmt"
"go.klarlabs.de/statekit"
)
type Context struct{}
// Define machine using struct tags
type TrafficLight struct {
statekit.MachineDef `id:"traffic_light" initial:"green"`
Green statekit.StateNode `on:"TIMER->yellow"`
Yellow statekit.StateNode `on:"TIMER->red"`
Red statekit.StateNode `on:"TIMER->green"`
}
func main() {
registry := statekit.NewActionRegistry[Context]()
machine, err := statekit.FromStruct[TrafficLight, Context](registry)
if err != nil {
panic(err)
}
interp := statekit.NewInterpreter(machine)
interp.Start()
fmt.Println(interp.State().Value) // "green"
}Embedded in the machine struct to define machine-level configuration:
type MyMachine struct {
statekit.MachineDef `id:"machine_id" initial:"first_state"`
// states...
}Tags:
id:"..."- Required. Machine identifier.initial:"..."- Required. Initial state name (snake_case).
Defines an atomic (simple) state:
Idle statekit.StateNode `on:"START->running" entry:"logIdle" exit:"cleanup"`Tags:
on:"..."- Transition definitionsentry:"..."- Entry actions (comma-separated)exit:"..."- Exit actions (comma-separated)
Defines a compound (parent) state with children:
type ActiveState struct {
statekit.CompoundNode `initial:"idle" on:"RESET->done"`
Idle statekit.StateNode `on:"START->working"`
Working statekit.StateNode `on:"STOP->idle"`
}Tags:
initial:"..."- Required. Initial child state.on:"..."- Parent-level transitionsentry:"..."- Parent entry actionsexit:"..."- Parent exit actions
Defines a final (terminal) state:
Done statekit.FinalNodeFinal states typically have no transitions.
Basic format: on:"EVENT->target"
`on:"SUBMIT->processing"`With guard: on:"EVENT->target:guardName"
`on:"SUBMIT->processing:hasItems"`With action: on:"EVENT->target/actionName"
`on:"SUBMIT->processing/validateForm"`With multiple actions: on:"EVENT->target/action1;action2"
`on:"SUBMIT->processing/validate;log"`With action and guard: on:"EVENT->target/action:guard"
`on:"SUBMIT->processing/validate:hasItems"`Separate with commas:
`on:"START->running,CANCEL->cancelled,SKIP->done"`Comma-separated action names:
`entry:"logEntry,startTimer"`
`exit:"cleanup,stopTimer"`Register action and guard implementations:
type OrderContext struct {
Items int
Total float64
}
registry := statekit.NewActionRegistry[OrderContext]().
WithAction("logOrder", func(ctx *OrderContext, e statekit.Event) {
fmt.Printf("Order: %d items, $%.2f\n", ctx.Items, ctx.Total)
}).
WithAction("addItem", func(ctx *OrderContext, e statekit.Event) {
ctx.Items++
ctx.Total += 10.00
}).
WithGuard("hasItems", func(ctx OrderContext, e statekit.Event) bool {
return ctx.Items > 0
}).
WithGuard("canCheckout", func(ctx OrderContext, e statekit.Event) bool {
return ctx.Total >= 25.00
})machine, err := statekit.FromStruct[MachineType, ContextType](registry)Provide an initial context value:
initialCtx := OrderContext{Items: 0, Total: 0.0}
machine, err := statekit.FromStructWithContext[OrderMachine, OrderContext](
registry,
initialCtx,
)Define nested states using embedded structs:
// Child states as struct types
type IdleState struct {
statekit.StateNode `on:"START->working"`
}
type WorkingState struct {
statekit.StateNode `on:"PAUSE->paused,COMPLETE->done"`
}
type PausedState struct {
statekit.StateNode `on:"RESUME->working"`
}
// Parent state embedding CompoundNode
type ActiveState struct {
statekit.CompoundNode `initial:"idle" on:"CANCEL->cancelled"`
Idle IdleState
Working WorkingState
Paused PausedState
}
// Machine definition
type WorkflowMachine struct {
statekit.MachineDef `id:"workflow" initial:"active"`
Active ActiveState
Done statekit.FinalNode
Cancelled statekit.FinalNode
}Field names are converted to snake_case for state IDs. Acronyms are handled intelligently:
| Field Name | State ID |
|---|---|
Idle |
idle |
DontWalk |
dont_walk |
PaymentError |
payment_error |
HTTPServer |
http_server |
APIGateway |
api_gateway |
XMLParser |
xml_parser |
package main
import (
"fmt"
"go.klarlabs.de/statekit"
)
type OrderContext struct {
Items int
Total float64
}
type CartState struct {
statekit.StateNode `on:"ADD_ITEM->cart/addItem,CHECKOUT->payment:hasItems" entry:"logCart"`
}
type PaymentState struct {
statekit.StateNode `on:"PAY->processing/processPayment:canPay"`
}
type ProcessingState struct {
statekit.StateNode `on:"SUCCESS->completed,FAILURE->payment"`
}
type OrderMachine struct {
statekit.MachineDef `id:"order" initial:"cart"`
Cart CartState
Payment PaymentState
Processing ProcessingState
Completed statekit.FinalNode
}
func main() {
registry := statekit.NewActionRegistry[OrderContext]().
WithAction("addItem", func(ctx *OrderContext, e statekit.Event) {
ctx.Items++
ctx.Total += 10.00
}).
WithAction("logCart", func(ctx *OrderContext, e statekit.Event) {
fmt.Printf("Cart: %d items\n", ctx.Items)
}).
WithAction("processPayment", func(ctx *OrderContext, e statekit.Event) {
fmt.Printf("Processing payment: $%.2f\n", ctx.Total)
}).
WithGuard("hasItems", func(ctx OrderContext, e statekit.Event) bool {
return ctx.Items > 0
}).
WithGuard("canPay", func(ctx OrderContext, e statekit.Event) bool {
return ctx.Total > 0
})
machine, err := statekit.FromStruct[OrderMachine, OrderContext](registry)
if err != nil {
panic(err)
}
interp := statekit.NewInterpreter(machine)
interp.Start()
interp.Send(statekit.Event{Type: "ADD_ITEM"})
interp.Send(statekit.Event{Type: "ADD_ITEM"})
interp.Send(statekit.Event{Type: "CHECKOUT"})
interp.Send(statekit.Event{Type: "PAY"})
interp.Send(statekit.Event{Type: "SUCCESS"})
fmt.Println("Final state:", interp.State().Value)
fmt.Println("Done:", interp.Done())
}Machines are validated at build time:
type InvalidMachine struct {
statekit.MachineDef `id:"invalid" initial:"nonexistent"`
Idle statekit.StateNode
}
_, err := statekit.FromStruct[InvalidMachine, Context](registry)
// Error: initial state 'nonexistent' does not existCommon validation errors:
- Missing required
idorinitialtags - Initial state doesn't exist
- Transition target doesn't exist
- Referenced action not in registry
- Referenced guard not in registry
- Compound state missing initial child
| Fluent Builder | Reflection DSL |
|---|---|
| More verbose | More compact |
| IDE autocomplete | Tag syntax |
| Inline actions | Named actions |
| Build-time type safety | Struct-level type safety |
| Good for dynamic machines | Good for static definitions |
Both approaches produce identical MachineConfig and can be used interchangeably.
-
Use descriptive field names - They become state IDs.
-
Keep registries organized - Group related actions and guards.
-
Validate early - Check errors from
FromStructat startup. -
Use type aliases for reuse - Define state types once, reuse in multiple machines.
-
Document tag syntax - Add comments explaining complex transitions.