Statum is a framework for building protocol-safe, compile-time verified typestate workflows in Rust.
Statum helps you model workflows where phase order matters and invalid transitions are expensive. You describe lifecycle phases with #[state], durable context with #[machine], legal moves with #[transition], and typed rehydration from existing data with #[validators].
It is opinionated on purpose: explicit transitions, state-specific data, and compile-time method gating. If that is the shape of your problem, the API stays small and the safety payoff is high.
Statum targets stable Rust and currently supports Rust 1.93+.
[dependencies]
statum = "0.5.5"use statum::{machine, state, transition};
#[state]
enum LightState {
Off,
On,
}
#[machine]
struct LightSwitch<LightState> {
name: String,
}
#[transition]
impl LightSwitch<Off> {
fn switch_on(self) -> LightSwitch<On> {
self.transition()
}
}
#[transition]
impl LightSwitch<On> {
fn switch_off(self) -> LightSwitch<Off> {
self.transition()
}
}
fn main() {
let light = LightSwitch::<Off>::builder()
.name("desk lamp".to_owned())
.build();
let light = light.switch_on();
let _light = light.switch_off();
}Example: statum-examples/src/toy_demos/example_01_setup.rs
If you add derives, place them below #[state] and #[machine]:
#[machine]
#[derive(Debug, Clone)]
struct LightSwitch<LightState> {
name: String,
}That avoids the common missing fields marker and state_data error.
#[state] -> lifecycle phases
#[machine] -> durable machine context
#[transition] -> legal edges between phases
#[validators] -> typed rehydration from stored data
Roughly, Statum generates:
- Marker types for each state variant, such as
OffandOn. - A machine type parameterized by the current state, with hidden
markerandstate_datafields. - Builders for new machines, such as
LightSwitch::<Off>::builder(). - A machine-scoped enum like
task_machine::Statefor matching reconstructed machines. - A machine-scoped
task_machine::Fieldsstruct for batch rebuilds where each row needs different machine context. - A machine-scoped batch rehydration trait like
task_machine::IntoMachinesExt.
This is the whole model. The rest of the crate is about making those four pieces ergonomic.
Typed rehydration is the unusual part: if you already have rows, events, or persisted workflow data,
#[validators]can rebuild them into typed machines. Full example below.
#[validators] is the feature that turns stored data back into typed machines. Each is_* method checks whether the persisted value belongs to a state, returns () or state-specific data, and Statum builds the right typed output:
use statum::{machine, state, validators};
#[state]
enum TaskState {
Draft,
InReview(ReviewData),
Published,
}
struct ReviewData {
reviewer: String,
}
#[machine]
struct TaskMachine<TaskState> {
client: String,
name: String,
}
enum Status {
Draft,
InReview,
Published,
}
struct DbRow {
status: Status,
}
#[validators(TaskMachine)]
impl DbRow {
fn is_draft(&self) -> statum::Result<()> {
let _ = (&client, &name);
if matches!(self.status, Status::Draft) {
Ok(())
} else {
Err(statum::Error::InvalidState)
}
}
fn is_in_review(&self) -> statum::Result<ReviewData> {
let _ = &name;
if matches!(self.status, Status::InReview) {
Ok(ReviewData {
reviewer: format!("reviewer-for-{client}"),
})
} else {
Err(statum::Error::InvalidState)
}
}
fn is_published(&self) -> statum::Result<()> {
if matches!(self.status, Status::Published) {
Ok(())
} else {
Err(statum::Error::InvalidState)
}
}
}
fn main() -> statum::Result<()> {
let row = DbRow {
status: Status::InReview,
};
let machine = row
.into_machine()
.client("acme".to_owned())
.name("spec".to_owned())
.build()?;
match machine {
task_machine::State::Draft(_) => {}
task_machine::State::InReview(task) => {
assert_eq!(task.state_data.reviewer.as_str(), "reviewer-for-acme");
}
task_machine::State::Published(_) => {}
}
Ok(())
}Key details:
- Validator methods run against your persisted type and return either
Ok(...)for the matching state orErr(statum::Error::InvalidState). - Machine fields are available by name inside validator methods through generated bindings, so
clientandnameare usable without boilerplate parameter plumbing. Persisted-row fields still live onself. - Unit states return
statum::Result<()>; data-bearing states returnstatum::Result<StateData>. .build()returns the generated wrapper enum, which you can match astask_machine::State.- If any validator is
async, the generated builder becomesasync. - Use
.into_machines_by(|row| task_machine::Fields { ... })when batch reconstruction needs different machine fields per row. - For append-only event logs, project events into validator rows first.
statum::projection::reduce_oneandreduce_groupedare the small helper layer for that. - If no validator matches,
.build()returnsstatum::Error::InvalidState.
Examples: statum-examples/src/toy_demos/09-persistent-data.rs, statum-examples/src/toy_demos/10-persistent-data-vecs.rs, statum-examples/src/toy_demos/14-batch-machine-fields.rs, statum-examples/src/showcases/sqlite_event_log_rebuild.rs
More detail: docs/persistence-and-validators.md
#[state]
- Apply it to an enum.
- Variants must be unit variants or single-field tuple variants.
- Generics on the state enum are not supported.
#[machine]
- Apply it to a struct.
- The first generic parameter must match the
#[state]enum name. - Put
#[machine]above#[derive(...)].
#[transition]
- Apply it to
impl Machine<State>blocks that define legal transitions. - Transition methods must take
selformut self. - Return
Machine<NextState>directly, or wrap it inResult/Optionwhen the transition is conditional. - Use
transition_with(data)when the target state carries data.
#[validators]
- Use
#[validators(Machine)]on animplblock for your persisted type. - Define one
is_{state}method per state variant. - Return
statum::Result<()>for unit states orstatum::Result<StateData>for data-bearing states. - Prefer
into_machine()for single-item reconstruction. - For collections that share machine fields, call
.into_machines(). - For collections where machine fields vary per item, call
.into_machines_by(|row| machine::Fields { ... }). - From other modules, import
machine::IntoMachinesExt as _first.
Use Statum when:
- Workflow order is stable and meaningful.
- Invalid transitions are expensive.
- Available methods should change by phase.
- Some data is only valid in specific states.
Do not use Statum when:
- The workflow is highly ad hoc or user-authored.
- Branching is mostly runtime business logic.
- States are still changing faster than the API around them.
More design guidance: docs/typestate-builder-design-playbook.md
missing fields marker and state_data
Your derives expanded before #[machine]. Put #[machine] above #[derive(...)].
Transition helpers in the wrong place
Keep non-transition helpers in normal impl blocks. #[transition] is for protocol edges, not general utility methods.
State shape errors
#[state] accepts unit variants and single-field tuple variants only.
For real service-shaped examples, run one of these:
cargo run -p statum-examples --bin axum-sqlite-review
cargo run -p statum-examples --bin clap-sqlite-deploy-pipeline
cargo run -p statum-examples --bin sqlite-event-log-rebuild
cargo run -p statum-examples --bin tokio-sqlite-job-runner
cargo run -p statum-examples --bin tokio-websocket-sessionaxum-sqlite-reviewdemonstrates#[validators]rebuilding typed machines from database rows before each HTTP transition.clap-sqlite-deploy-pipelinedemonstrates repeated CLI invocations, SQLite-backed typed rehydration, and explicit apply/failure/rollback phases.sqlite-event-log-rebuilddemonstrates append-only event storage, projection-based typed rehydration, and batch.into_machines()reconstruction.tokio-sqlite-job-runnerdemonstrates retries, leases, async side effects, and typed rehydration in a background worker loop.tokio-websocket-sessiondemonstrates protocol-safe frame handling, phase-gated behavior, and a session lifecycle that is not persistence-driven.
If you use coding agents, Statum ships an adoption kit with copyable instruction templates, audit heuristics, and prompts for targeted refactors and reviews. Start with docs/agents/README.md.
- Toy demos: statum-examples/src/toy_demos/
- Showcase apps: statum-examples/src/showcases/
- Crate docs: statum, statum-core, statum-macros
- Review showcase binary: statum-examples/src/bin/axum-sqlite-review.rs
- Deploy pipeline binary: statum-examples/src/bin/clap-sqlite-deploy-pipeline.rs
- Event log binary: statum-examples/src/bin/sqlite-event-log-rebuild.rs
- Job runner binary: statum-examples/src/bin/tokio-sqlite-job-runner.rs
- Session binary: statum-examples/src/bin/tokio-websocket-session.rs
- Coding-agent kit: docs/agents/README.md
- Typed rehydration and validators: docs/persistence-and-validators.md
- Patterns and advanced usage: docs/patterns.md
- Typestate builder design playbook: docs/typestate-builder-design-playbook.md
- API docs: docs.rs/statum
- Stable Rust is the target.
- MSRV:
1.93