Finitomata for Rust — Documentation
A Rust port of the Finitomata Elixir library, providing compile-time validated finite state machines with rich lifecycle callbacks, persistence, and actor-based supervision via joerl.
Table of Contents
- Overview
- Architecture
- Quick Start
- The
#[finitomata]Proc Macro - The
FinitomataTrait - Supervisor API
- Transition Graph
- Event Kinds
- Persistence
- Listeners
- Timers
- Supervised FSMs (Fault Tolerance)
- State Cache
- Error Handling
- Mermaid Syntax
- PlantUML Syntax
- API Reference
Overview
Finitomata provides:
- Compile-time validation — FSM definitions are parsed and validated by a proc macro. Invalid graphs (unreachable states, missing initial/final states) produce compile errors.
- Rich lifecycle callbacks —
on_start,on_enter,on_exit,on_transition,on_failure,on_timer,on_terminate. - Actor-based execution — Each FSM instance runs as a joerl actor with its own mailbox, enabling concurrent independent operation.
- Fault-tolerant supervision — FSMs can be spawned under a joerl supervisor with automatic restart and state recovery from persistence.
- Persistence — Trait-based pluggable storage. Ships with an in-memory backend; implement
Persistencyfor databases, Redis, etc. - Timers — Configurable recurring timers that invoke
on_timerfor periodic state-driven logic. - Listeners — Observer pattern for external telemetry/logging of transitions.
Architecture
finitomata.rust/
├── finitomata/ # Main library crate
│ ├── src/
│ │ ├── lib.rs # Public re-exports
│ │ ├── callbacks.rs # Finitomata trait (user implements)
│ │ ├── engine.rs # Transition orchestration
│ │ ├── supervisor.rs # FinitomataSupervisor (joerl-backed)
│ │ ├── transition.rs # TransitionGraph, validation, pathfinding
│ │ ├── state.rs # FsmState, Lifecycle, BoundedHistory
│ │ ├── error.rs # Error types
│ │ ├── cache.rs # DashMap-backed state cache
│ │ ├── timer.rs # FsmTimer (tokio interval)
│ │ ├── listener.rs # Listener trait + implementations
│ │ ├── persistency/ # Persistency trait + InMemoryPersistency
│ │ ├── parser/ # Mermaid + PlantUML parsers
│ │ └── examples/ # Runnable examples
└── finitomata_macro/ # Proc macro crate (#[finitomata])
Quick Start
use async_trait;
use ;
use Duration;
;
async
The #[finitomata] Proc Macro
The #[finitomata] attribute macro parses an FSM definition at compile time, validates the graph, and generates:
- A State enum (e.g.,
MyFsmState) with variants for each state - An Event enum (e.g.,
MyFsmEvent) with variants for each event - A
build_graph()associated function that returns aTransitionGraph
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
fsm |
string (required) | — | The FSM definition in Mermaid or PlantUML syntax |
syntax |
"mermaid" or "plantuml" |
"mermaid" |
Which parser to use |
auto_terminate |
bool |
false |
Auto-terminate on reaching a final state |
timer |
integer (ms) | — | Timer interval in milliseconds |
Compile-Time Validation
The macro rejects:
- Definitions with no initial state (
[*] --> ...) - Definitions with no final state (
... --> [*]) - Unreachable states
- Empty definitions
The Finitomata Trait
The core trait that users implement to define FSM behavior.
TransitionResult
Callback Sequence
For every transition, the engine executes this sequence:
- Validate: does current state respond to event? (soft events skip silently)
on_exit(current_state, payload)on_transition(from, event, event_payload, payload)→ target state- Validate: is target in the graph's allowed set?
- Persist (if configured)
- Update state + history
- Notify listener
on_enter(new_state, payload)- If
auto_terminateand target is final →on_terminateand stop - If event is Hard → immediately fire next hard event
Supervisor API
FinitomataSupervisor<F> is the high-level API for managing FSM instances.
Construction (Builder Pattern)
let supervisor = new
.with_persistency // Enable persistence
.with_listener // Log transitions
.with_auto_terminate // Stop on final state
.with_timer; // Recurring timer
Methods
| Method | Description |
|---|---|
start_fsm(name, fsm, payload) |
Spawn an unsupervised FSM actor |
spawn_supervised(name, fsm, payload, strategy) |
Spawn under a fault-tolerant supervisor |
spawn_supervised_with_intensity(name, fsm, payload, strategy, intensity) |
Same with custom restart limits |
transition(name, event, payload) |
Send an event to trigger a transition |
state(name) |
Get full state snapshot (from cache) |
current_state(name) |
Get just the current state enum value |
alive(name) |
Check if FSM is in Running lifecycle |
all() |
List all managed FSMs |
shutdown(name) |
Graceful shutdown (calls on_terminate) |
system() |
Access the underlying joerl ActorSystem |
id() |
Get the supervisor's ID |
Transition Graph
TransitionGraph<S, E> holds the validated FSM structure.
Key Methods
graph.initial_state // → &S
graph.final_states // → &BTreeSet<S>
graph.is_final // → bool
graph.responds // → bool
graph.allowed // → Option<(&[S], EventKind)>
graph.events_for // → Vec<(&E, EventKind)>
graph.all_states // → BTreeSet<S>
graph.all_events // → BTreeSet<&E>
graph.shortest_path // → Option<Vec<(E, S)>>
graph.validate // → Result<(), Vec<ValidationError>>
Event Kinds
Events can have special suffixes that modify their behavior:
| Suffix | Kind | Behavior |
|---|---|---|
| (none) | Normal |
Standard event — must be explicitly triggered |
! |
Hard |
Auto-fires immediately when entering the source state |
? |
Soft |
Silently ignored if the current state doesn't respond |
Hard Events
[*] --> init
init --> |boot!| ready ← fires automatically when entering init
ready --> |go| active
After entering init, the boot! event fires immediately without any external trigger. This is equivalent to Elixir Finitomata's "determined transitions."
Soft Events
running --> |tick?| running ← no error if sent while in idle
Sending tick? to an FSM in idle state silently succeeds (no-op) instead of returning an error.
Persistence
Trait
In-Memory Backend
use InMemoryPersistency;
let persist = new;
let supervisor = new
.with_persistency;
Recovery
When start_fsm or spawn_supervised is called with persistence configured:
- The system checks
persist.load(name)for existing state - If found, the FSM starts from the persisted state (not the graph's initial state)
- Every successful transition calls
persist.store(name, state, payload)
Listeners
Listeners observe transitions for telemetry, logging, or event propagation.
Built-in Listeners
NoopListener— discards all notificationsTracingListener— emitstracing::info!events
Custom Listener Example
;
Timers
Configure a recurring timer to invoke on_timer:
let supervisor = new
.with_timer;
The on_timer callback can return Some((event, payload)) to trigger a transition, or None to do nothing:
async
For supervised FSMs, timers use joerl's send_after mechanism (self-rescheduling on each tick). For unsupervised FSMs, the standalone FsmTimer is available.
Supervised FSMs (Fault Tolerance)
spawn_supervised wraps an FSM under a joerl supervisor that automatically restarts it on crash.
use RestartStrategy;
supervisor
.spawn_supervised
.await
.unwrap;
Restart Strategies
| Strategy | Behavior |
|---|---|
OneForOne |
Only the crashed FSM is restarted |
OneForAll |
All children of the supervisor are restarted |
RestForOne |
The crashed FSM and all started after it are restarted |
Recovery Flow
- FSM crashes (panic, unrecoverable error)
- joerl supervisor detects the exit
- Supervisor calls the factory to create a fresh FSM actor
- New actor's
started()hook callsPersistency::load(name) - If state is found, the FSM resumes from the last persisted state
on_startandon_enterare called on the recovered state- The actor re-registers its PID in the joerl registry
- Subsequent
transition()calls route to the new PID
Custom Restart Intensity
use RestartIntensity;
supervisor
.spawn_supervised_with_intensity
.await
.unwrap;
If restarts exceed the limit, the supervisor itself terminates.
State Cache
The StateCache stores the latest FSM state in a concurrent DashMap, updated after every transition. This enables supervisor.state(name) to return immediately without an actor round-trip.
// These are instant (cache reads):
let state = supervisor.state;
let current = supervisor.current_state;
let is_alive = supervisor.alive;
let all = supervisor.all;
Error Handling
FinitomataError
The primary error type with a stage field indicating where the error occurred:
| Stage | When |
|---|---|
NotResponds |
Event sent to a state that doesn't handle it |
NotAllowed |
on_transition returned a target not in the graph |
OnTransition |
User code returned TransitionResult::Error |
OnEnter |
Error during entry callback |
OnExit |
Error during exit callback |
Persistency |
Storage backend failed |
Validation |
General validation/operational error |
ValidationError
Compile-time or runtime graph validation errors:
NoInitialStateNoFinalStateUnreachableState(name)MultipleInitialStates(names)OrphanState(name)
Mermaid Syntax
Two flavors are supported:
Flowchart Style
[*] --> idle
idle --> |start| running
running --> |pause| paused
running --> |stop| idle
paused --> |resume| running
idle --> |shutdown| [*]
State Diagram Style
[*] --> idle
idle --> running : start
running --> paused : pause
running --> idle : stop
paused --> running : resume
idle --> [*] : shutdown
[*]as source = initial state (first occurrence defines the initial)[*]as target = final state (the source state becomes final)- Event suffixes:
start!(hard),check?(soft)
PlantUML Syntax
[*] --> idle
idle --> running : start
running --> idle : stop
idle --> [*] : shutdown
Same semantics as Mermaid state diagram syntax. State declarations (state "label" as name) are also supported.
API Reference
Re-exported Types
From finitomata:
Finitomata— core traitTransitionResult— return type for on_transitionFinitomataSupervisor— high-level supervisorTransitionGraph,Transition,EventKindFsmState,BoundedHistory,LifecycleFinitomataError,ValidationError,PersistencyErrorPersistency,Listener,NoopListener,TracingListenerFsmTimer,FsmParser,ParsedFsm,ParsedTransition,ParseError
From joerl:
ActorSystem,PidRestartStrategy,RestartIntensity,SupervisorSpec,ChildSpec
Crate Features
The crate uses these workspace dependencies:
tokio— async runtimeasync-trait— async methods in traitsdashmap— concurrent maps (cache, in-memory persistence)joerl— actor system and supervisionserde— serialization support for state typestracing— structured loggingthiserror— error derivation
Examples
Run the included examples: