Statum
Statum is a zero-boilerplate library for finite-state machines in Rust, with compile-time state transition validation.
Why Use Statum?
- Compile-Time Safety: State transitions are validated at compile time, ensuring no invalid transitions.
- Ergonomic Macros: Define states and state machines with minimal boilerplate.
- State-Specific Data: Add and access data tied to individual states easily.
- Persistence-Friendly: Reconstruct state machines seamlessly from external data sources.
Table of Contents
- Quick Start
- Additional Features & Examples
- Examples
- Patterns & Guidance
- API Rules (Current)
- Common Errors and Tips
- API Reference
Quick Start
To start, it provides three attribute macros:
#[state]for defining states (as enums).#[machine]for creating a state machine struct that tracks which state you are in at compile time.#[transition]for validating transition method signatures.
Here is the simplest usage of Statum without any extra features:
use ;
// 1. Define your states as an enum.
// 2. Define your machine with the #[machine] attribute.
// 3. Implement transitions for each state.
Example: statum-examples/src/examples/example_01_setup.rs.
How It Works
#[state]transforms your enum, generating one struct per variant (likeOffandOn), plus a traitLightState.#[machine]injects extra fields (marker,state_data) to track which state you are in, letting you define transitions that change the state at the type level.#[transition]validates method signatures and ties them to a concrete next state.
That is it. You now have a compile-time guaranteed state machine where invalid transitions are impossible.
Additional Features & Examples
1. Adding Debug, Clone, or Other Derives
By default, you can add normal Rust derives on your enum and struct. For example:
Important: If you place #[derive(...)] above #[machine], you may see an error like:
error[E0063]: missing fields `marker` and `state_data` in initializer of `Light<_>`
|
14 | #[derive(Debug, Clone)]
| ^ missing `marker` and `state_data`
To avoid this, put #[machine] above the derive(s).
// ❌ This will NOT work
// note the position of the derive
;
// ✅ This will work
;
Example: statum-examples/src/examples/03-derives.rs.
2. Complex Transitions & Data-Bearing States
Defining State Data
States can hold data. For example:
Note: We use
self.transition_with(data)instead ofself.transition()to transition to a state that carries data.
Accessing State Data
State data is available as self.state_data in the concrete machine type:
Examples: statum-examples/src/examples/07-state-data.rs, statum-examples/src/examples/08-transition-with-data.rs.
3. Reconstructing State Machines from Persistent Data
State machines often need to persist their state. Saving to and loading from external storage like databases should be both robust and type-safe. Statum's #[validators] macro simplifies this process, ensuring seamless integration between your persistent data and state machine logic.
The key pieces are:
#[validators]macro on your data type impl block.machine_builder()generated on the data type to reconstruct the machine.
Example
use ;
Examples: statum-examples/src/examples/09-persistent-data.rs, statum-examples/src/examples/10-persistent-data-vecs.rs.
4. Typestate Builder Ergonomics
The validators macro also generates a machine-scoped module that exposes a short alias:
You can use it to shorten matches without introducing collisions:
let row = DbData ;
match rebuild_task?
Tested in statum-examples/tests/patterns.rs.
Examples
See statum-examples/src/examples/ for the full suite of examples.
Patterns & Guidance
Conditional transitions (branching decisions)
Transition methods must return a single next state. Put branching logic in a normal method and call explicit transition methods:
Tested in statum-examples/tests/patterns.rs (event-driven transitions).
Event-driven transitions
Model events as an enum and route them to explicit transition methods:
Tested in statum-examples/tests/patterns.rs (event-driven transitions).
Guarded transitions
Keep preconditions in a guard method and return a Result before transitioning:
Tested in statum-examples/tests/patterns.rs (guarded transitions).
Hierarchical machines (state data as a nested machine)
Use a nested machine as state data to model parent and child flows:
Tested in statum-examples/tests/patterns.rs (hierarchical machines). Example: statum-examples/src/examples/11-hierarchical-machines.rs.
State snapshots (carry previous state data forward)
Capture the prior state's data inside the next state to keep history:
Tested in statum-examples/tests/patterns.rs (state snapshots).
Rollbacks / undo transitions
Model rollbacks by returning to a previous state explicitly, often with stored state data:
Tested in statum-examples/tests/patterns.rs (rollbacks). Example: statum-examples/src/examples/12-rollbacks.rs.
Async transitions (side effects before transition)
Keep side effects in async methods and call a sync transition at the end:
Tested in statum-examples/tests/patterns.rs (async side-effects). Example: statum-examples/src/examples/06-async-transitions.rs.
Rehydration with extra fetch
Use machine fields inside validators to fetch extra data for state reconstruction:
Tested in statum-examples/tests/patterns.rs (rehydration with fetch).
Persistent batches
When reconstructing many rows, use the batch builder on collections:
Tested in statum-examples/tests/patterns.rs (parallel reconstruction, batch builder). Example: statum-examples/src/examples/10-persistent-data-vecs.rs.
let results = rows
.machines_builder
.client
.build;
If you want a plain statum::Result<Vec<Machine>> without skipping invalid rows, map and collect:
let machines: Result = rows
.into_iter
.map
.collect;
Parallel reconstruction (async validators)
If validators are async, the batch builder returns results in parallel:
Tested in statum-examples/tests/patterns.rs (parallel reconstruction).
let results = rows
.machines_builder
.tenant
.build
.await;
Type-erased storage (collecting superstates)
Store *SuperState values in a collection and match later:
Tested in statum-examples/tests/patterns.rs (type-erased storage).
let items: = vec!;
for item in items
API Rules (Current)
#[state]
- Must be an enum.
- Must have at least one variant.
- Variants must be unit or single-field tuple variants.
- Generics on the enum are not supported.
#[machine]
- Must be a struct.
- First generic parameter must match the
#[state]enum name. - Derives on
#[state]are propagated to generated variant types. - Prefer
#[machine]above#[derive(..)]to avoid derive ordering surprises.
#[transition]
- Must be applied to
impl Machine<State>blocks. - Methods must take
selformut selfas the first argument. - Return type must be
Machine<NextState>orOption<Result<...>>wrappers. - Data-bearing states must use
transition_with(data).
#[validators]
- Use
#[validators(Machine)]on animplblock for your persistent data type. - Must define an
is_{state}method for every state variant (snake_case). - Each method returns
statum::Result<()>for unit states orstatum::Result<StateData>for data states. - Async validators are supported; if any validator is async, the generated builder is async.
- The macro generates a
{Machine}SuperStateenum and a machine-scoped module alias for matching on reconstructed states (typestate builder pattern).
Common Errors and Tips
-
missing fields marker and state_data- Usually means your derive macros (e.g.,
CloneorDebug) expanded before Statum could inject those fields. Move#[machine]above your derives, or remove them.
- Usually means your derive macros (e.g.,
-
cannot find type X in this scope- Ensure that you define your
#[machine]struct before you reference it inimplblocks or function calls.
- Ensure that you define your
-
Invalid transition return type- Transition methods must return
Machine<NextState>(optionally wrapped inOptionorResult).
- Transition methods must return
API Reference
Core Macros
| Macro | Description | Example Usage |
|---|---|---|
#[state] |
Defines states as an enum. Each variant becomes its own struct implementing the State trait. |
#[state] pub enum LightState { Off, On } |
#[machine] |
Defines a state machine struct and injects fields for state tracking and transitions. | #[machine] pub struct Light<LightState> { name: String } |
#[transition] |
Validates transition methods and generates the proper transition helpers. | #[transition] impl Light<Off> { fn on(self)->Light<On>{...} } |
#[validators] |
Defines validation methods to map persistent data to specific states. | #[validators(TaskMachine)] |
State Machine Methods / Fields
| Item | Description | Example Usage |
|---|---|---|
.builder() |
Builds a new machine in a specific state. | LightSwitch::<Off>::builder().name("lamp").build() |
.transition() |
Transitions to a unit state. | let light = light.switch_on(); |
.transition_with(data) |
Transitions to a state that carries data. | self.transition_with(data) |
.state_data |
Accesses the data of the current state (if available). | let notes = &self.state_data.notes; |
Validators Output
| Item | Description | Example Usage |
|---|---|---|
{Machine}SuperState |
Wrapper enum for all machine states, used for matching. | match machine { task_machine::State::Draft(m) => ... } |
machine_builder() |
Builder generated on the data type to reconstruct a machine from stored data. | row.machine_builder().client(c).build() |
Type Aliases
statum::Result<T> is a convenience alias for Result<T, statum::Error>.