Skip to main content

Crate statum

Crate statum 

Source
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:

  • state defines the legal phases.
  • machine defines the durable context carried across phases.
  • transition defines the legal edges between phases.
  • validators rebuilds 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, and transition.
  • For stored rows and database rebuilds, read validators.
  • For append-only event logs, use projection before validator rebuilds.
  • With the introspection feature 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 on statum-core directly.
  • The repository README and docs/ directory contain longer guides and showcase applications.

Modules§

projection
Event-stream projection helpers for Statum rebuild flows.

Structs§

RebuildAttempt
One validator evaluation recorded during typed rehydration.
RebuildInput
Describes the persisted input that a rebuild report evaluated.
RebuildReport
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.
RebuildAmbiguity
Ambiguity status for a rebuild report.

Traits§

CanTransitionMap
A machine that can transition by mapping its current state data into Next.
CanTransitionTo
A machine that can transition directly to Next.
CanTransitionWith
A machine that can transition using Data.
DataState
A generated state marker that carries payload data.
StateMarker
A generated state marker type.
UnitState
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.