Statum
[!WARNING] Nightly Rust is required. This workspace uses
proc_macro_spanand is pinned tonightlyviarust-toolchain.toml. Stable toolchains are not supported.
Statum is a zero-boilerplate library for compile-time typestate builders in Rust. It models state-machine flows while validating transitions at compile time.
Why Use Statum?
- Compile-Time Typestate Safety: State transitions are validated at compile time, ensuring no invalid transitions.
- Ergonomic Typestate Builders: Define states and transitions 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 defining a typestate-enabled struct whose state is encoded in the type 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 typestate builder flow where invalid transitions are impossible. You can model state-machine behavior with it, but the primary guarantee is type-level transition safety.
Core model vs boundary model:
- Keep business logic on concrete machine types such as
LightSwitch<Off>andLightSwitch<On>. - Use the generated superstate enum only when a boundary needs mixed states (for example collections, channels, storage, or transport).
- After crossing a boundary,
matchquickly to recover concrete types and continue with compile-time-checked transitions.
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
Typestate flows often need to persist their current stage. When reconstructing from storage, the stage is runtime data, so Statum returns a superstate boundary wrapper that you match to recover concrete typestate.
The key pieces are:
#[validators]macro on your data type impl block.into_machine()generated on the data type to reconstruct the machine (withmachine_builder()kept for compatibility).
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:
During reconstruction, the concrete state is runtime data (from a row, payload, or message), so the builder returns this superstate alias. Treat it as a boundary type: match and move back to concrete machine types for domain logic.
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).
Tip: Keep the decision enum variant names aligned with your #[state] variants. It makes matches read the same way while still carrying typed machines.
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). Example: statum-examples/src/examples/14-transition-map.rs.
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;
Boundary interop with superstate enums
Use *SuperState when a boundary must carry mixed concrete states (for example collections, channels, storage, or transport). This is intentional type erasure for interoperability; match as soon as possible to recover concrete typestate precision.
Tested in statum-examples/tests/patterns.rs (type-erased storage).
use mpsc;
let = ;
tx.send.await?;
tx.send.await?;
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 can use
transition_with(data)ortransition_map(|current| next_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
{Machine}SuperStateenum and machine-scoped module alias are generated by#[machine], and validators return that boundary type for reconstruction when runtime data determines the current state.
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 typestate-enabled struct and injects fields for type-level 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) |
.transition_map(f) |
Transitions by mapping owned current state data into next state data without cloning. | `self.transition_map( |
.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 |
Boundary/interoperability wrapper enum for mixed states; match to recover concrete machine types. | match machine { task_machine::State::Draft(m) => ... } |
into_machine() |
Builder generated on the data type to reconstruct a machine from stored data. | row.into_machine().client(c).build() |
Type Aliases
statum::Result<T> is a convenience alias for Result<T, statum::Error>.