Statum
Statum is a zero-boilerplate library for finite-state machines in Rust, with compile-time state transition validation. It provides two attribute macros:
#[state]for defining states (as enums).#[machine]for creating a state machine struct that tracks which state you’re in at compile time.
Quick Start (Minimal Example)
Here’s the simplest usage of Statum without any extra features:
use ;
// 1. Define your states as an enum.
// 2. Create a machine struct that references one of those states.
// 3. Implement transitions for each state.
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’re in, letting you define transitions that change the state at the type level.
That’s 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`
That’s because the derive macro for Clone, Debug, etc., expands before #[machine] has injected these extra fields. To avoid this, either:
- Put
#[machine]above the derive(s), or - Remove the conflicting derive(s) from the same item.
For example, this works:
2. serde Integration
Statum can optionally propagate Serialize/Deserialize derives if you enable the "serde" feature and derive those on your #[state] enum. For example:
[]
= { = "x.y.z", = ["serde"] }
= { = "1.0", = ["derive"] }
Then, in your code:
use state;
If you enable Statum’s "serde" feature, any #[derive(Serialize)] and #[derive(Deserialize)] you put on the enum will get passed through to the expanded variant structs. If you do not enable that feature, deriving those traits will likely fail to compile.
3. Complex Transitions & Data-Bearing States
States can hold data. For example:
// ...
// ...
Accessing State Data
Use .get_state_data() or .get_state_data_mut() to interact with the state-specific data:
4. Attribute Ordering
#[state]must go on an enum.#[machine]must go on a struct.- Because
#[machine]injects extra fields, you need it above any user#[derive(...)]. If you place#[derive(...) ]first, you might see “missing fieldsmarkerandstate_datain initializer” errors.
5. Implementing the Typestate Builder Pattern with Statum
The typestate builder pattern is a powerful way to enforce correct usage of a sequence of steps at compile time. With statum, you can implement this pattern using the provided #[state] and #[machine] macros to ensure type-safe state transitions in your builders.
This guide will walk you through implementing a typestate builder for a hypothetical "User Registration" workflow.
Overview
Imagine we have a multi-step process for registering a user:
- Collect the user's name.
- Set the user's email.
- Submit the registration.
Using the typestate builder pattern, we can ensure:
- Each step must be completed before moving to the next.
- Skipping steps or submitting prematurely results in compile-time errors.
Steps to Implement
1. Define States
Each step in the builder process is represented as a state using #[state]. For example:
use ;
Here:
NameNotSet: The initial state where the name has not been set.NameSet: The state where the name is provided but the email is not.EmailSet: The final state before submission.
2. Create the Builder Machine
The builder itself is a #[machine]-decorated struct that uses the defined states.
This struct will manage transitions between states.
3. Define State Transitions
Implement methods to move from one state to the next:
Transition from NameNotSet to NameSet
Transition from NameSet to EmailSet
Transition from EmailSet to Submission
4. Example Usage
Here’s how you would use this builder:
Compile-Time Guarantees
-
You cannot set the email without first setting the name:
let builder = new; let builder = builder.set_email; // Compile-time error -
You cannot submit the builder without setting the name and email:
let builder = new; builder.submit; // Compile-time error
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
-
Feature gating
- If you’re using
#[derive(Serialize, Deserialize)]on a#[state]enum but didn’t enable theserdefeature in Statum, you’ll get compile errors about missing trait bounds.
- If you’re using
Lint Warnings (unexpected_cfgs)
If you see warnings like:
= note: no expected values for `feature`
= help: consider adding `foo` as a feature in `Cargo.toml`
it means you have the unexpected_cfgs lint enabled but you haven’t told your crate “feature = foo” is valid. This is a Rust nightly lint that ensures you only use #[cfg(feature="...")] with known feature values.
To fix it, either disable the lint or declare the allowed values in your crate’s Cargo.toml:
[]
= [
'cfg(feature, values("serde"))'
]
= "warn"
License
Statum is distributed under the terms of the MIT license. See LICENSE for details.