Expand description
Compile-time verified typestate surfaces for Rust.
Statum is for values whose phase should change what methods are legally
available on that value. It helps keep invalid, undesirable, or not-yet-
validated states out of ordinary code.
In the same spirit as Option and Result, it uses the type system to
make absence, failure, and workflow legality explicit instead of leaving
them in status fields and guard code. It generates typed state markers,
typed machines, transition helpers, and typed rehydration from stored data.
§Mental Model
Use Statum when pressing . before and after a phase change should show a
meaningfully different method surface.
Durable workflows and protocols are one strong fit. Staged validation, resolution, and build surfaces are another. The current macro surface is machine-shaped:
statedefines the legal phases.machinedefines the durable context carried across phases.transitiondefines the legal edges between phases.validatorsrebuilds typed machines from persisted data.
§Quick Start
use statum::{machine, state, transition};
#[state]
enum CheckoutState {
EmptyCart,
ReadyToPay(OrderDraft),
Paid,
}
#[derive(Clone)]
struct OrderDraft {
total_cents: u64,
}
#[machine]
struct Checkout<CheckoutState> {
id: String,
}
#[transition]
impl Checkout<EmptyCart> {
fn review(self, total_cents: u64) -> Checkout<ReadyToPay> {
self.transition_with(OrderDraft { total_cents })
}
}
#[transition]
impl Checkout<ReadyToPay> {
fn pay(self) -> Checkout<Paid> {
self.transition()
}
}
fn main() {
let cart = Checkout::<EmptyCart>::builder()
.id("order-1".to_owned())
.build();
let ready = cart.review(4200);
assert_eq!(ready.state_data.total_cents, 4200);
let _paid = ready.pay();
}§Typed Rehydration
#[validators] lets you rebuild persisted rows back into typed machine
states:
use statum::{machine, state, validators, Error};
#[state]
enum TaskState {
Draft,
InReview(String),
Published,
}
#[machine]
struct Task<TaskState> {
id: u64,
}
struct TaskRow {
id: u64,
status: &'static str,
reviewer: Option<String>,
}
#[validators(Task)]
impl TaskRow {
fn is_draft(&self) -> statum::Result<()> {
if self.status == "draft" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
fn is_in_review(&self) -> statum::Result<String> {
if self.status == "in_review" {
self.reviewer.clone().ok_or(Error::InvalidState)
} else {
Err(Error::InvalidState)
}
}
fn is_published(&self) -> statum::Result<()> {
if self.status == "published" {
Ok(())
} else {
Err(Error::InvalidState)
}
}
}
fn main() -> statum::Result<()> {
let row = TaskRow {
id: 7,
status: "in_review",
reviewer: Some("alice".to_owned()),
};
let row_id = row.id;
let machine = Task::rebuild(&row).id(row_id).build()?;
match machine {
task::SomeState::InReview(task) => assert_eq!(task.state_data, "alice"),
_ => panic!("expected in-review task"),
}
Ok(())
}If you want explainable rebuild traces, validators can also return
Validation. Then .build_report() and .build_reports() populate RebuildAttempt::reason_key and
RebuildAttempt::message for failed matches while keeping the normal
.into_result() surface.
§Compile-Time Gating
Methods only exist on states where you define them.
use statum::{machine, state};
#[state]
enum LightState {
Off,
On,
}
#[machine]
struct Light<LightState> {}
let light = Light::<Off>::builder().build();
let _ = light.switch_off(); // no such method on Light<Off>§Machine Introspection
With the introspection feature enabled, Statum can also expose the static
machine structure as typed metadata.
This is useful when the same machine definition should drive:
- CLI explainers
- generated docs
- graph exports
- exact transition assertions in tests
- runtime replay or debug tooling
The default feature set does not emit the generated StateId,
TransitionId, GRAPH, PRESENTATION, or linkme inventory surface.
Enable introspection for that generated metadata surface. Enable
strict-introspection when you also want stricter transition return-shape
rejection; strict-introspection implies introspection.
With strict-introspection, the graph is exact at the transition-site
level for the supported observation point. A consumer can ask for the legal
targets of one specific method on one specific source state and treat that
metadata as the authoritative static graph surface for macro-validated
inputs.
The observation point is the macro-validated semantic model: locally readable
#[state] and #[machine] items, locally readable #[transition] method
signatures, plus any explicit #[introspect(return = ...)] escape hatches.
Supported return shapes are direct machine returns plus canonical
wrapper paths around machine types:
::core::option::Option<Machine<NextState>>,
::core::result::Result<Machine<NextState>, E>, and
::statum::Branch<Machine<Left>, Machine<Right>>.
Unsupported custom decision enums, wrapper aliases, and differently-qualified
machine paths are rejected instead of approximated. In the default feature
set, Statum still follows some source-backed aliases for ergonomics, but
that mode should be treated as convenient metadata rather than the strongest
exactness guarantee. Whole-item #[cfg] gates are supported, but nested
#[cfg] or #[cfg_attr] on #[state] variants, variant payload fields,
or #[machine] fields are rejected because they would otherwise drift the
generated metadata from the active build.
For small amounts of human-facing metadata, Statum can also generate a
machine::PRESENTATION constant from #[present(...)] attributes. Add
#[presentation_types(...)] on the machine when those attributes should
carry typed metadata = ... payloads instead of just labels and
descriptions. The example below requires the introspection feature.
use statum::{
machine, state, transition, MachineIntrospection, MachineTransitionRecorder,
};
#[state]
enum FlowState {
Fetched,
Accepted,
Rejected,
}
#[machine]
struct Flow<FlowState> {}
#[transition]
impl Flow<Fetched> {
fn validate(
self,
accept: bool,
) -> ::core::result::Result<Flow<Accepted>, Flow<Rejected>> {
if accept {
Ok(self.accept())
} else {
Err(self.reject())
}
}
fn accept(self) -> Flow<Accepted> {
self.transition()
}
fn reject(self) -> Flow<Rejected> {
self.transition()
}
}
fn main() {
let graph = <Flow<Fetched> as MachineIntrospection>::GRAPH;
let validate = graph
.transition_from_method(flow::StateId::Fetched, "validate")
.unwrap();
assert_eq!(
graph.legal_targets(validate.id).unwrap(),
&[flow::StateId::Accepted, flow::StateId::Rejected]
);
let event = <Flow<Fetched> as MachineTransitionRecorder>::try_record_transition_to::<
Flow<Accepted>,
>(Flow::<Fetched>::VALIDATE)
.unwrap();
assert_eq!(event.chosen, flow::StateId::Accepted);
}Transition ids are exact and typed, but they are exposed as generated
associated consts on the source-state machine type, such as
Flow::<Fetched>::VALIDATE.
§Where To Look Next
- Start with
state,machine, andtransition. - For stored rows and database rebuilds, read
validators. - For append-only event logs, use
projectionbefore validator rebuilds. - With the
introspectionfeature enabled, machine introspection, presentation, and runtime recording types are re-exported at the crate root (MachineGraph,RecordedTransition,MachinePresentation, and related descriptors) so applications can inspect generated transition metadata without depending onstatum-coredirectly. - The repository README and
docs/directory contain longer guides and showcase applications.
Modules§
- projection
- Event-stream projection helpers for Statum rebuild flows.
Structs§
- Rebuild
Attempt - One validator evaluation recorded during typed rehydration.
- Rebuild
Input - Describes the persisted input that a rebuild report evaluated.
- Rebuild
Report - A typed rehydration result plus the validator attempts that produced it.
- Rejection
- A structured validator rejection captured during typed rehydration.
Enums§
- Branch
- A first-class two-way branching transition result.
- Error
- Errors returned by Statum runtime helpers.
- Rebuild
Ambiguity - Ambiguity status for a rebuild report.
Traits§
- CanTransition
Map - A machine that can transition by mapping its current state data into
Next. - CanTransition
To - A machine that can transition directly to
Next. - CanTransition
With - A machine that can transition using
Data. - Data
State - A generated state marker that carries payload data.
- State
Marker - A generated state marker type.
- Unit
State - A generated state marker with no payload.
Type Aliases§
- Result
- Convenience result alias used by Statum APIs.
- Validation
- An opt-in validator result that carries structured rejection details.
Attribute Macros§
- machine
- Define a typed machine that carries durable context across states.
- state
- Define the legal lifecycle phases for a machine.
- transition
- Validate and generate legal transitions for one source state.
- validators
- Rebuild typed machines from persisted data.