Statum
Statum is a zero-boilerplate library for finite-state machines in Rust, with compile-time state transition validation. To start, 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.
There is one more super useful macro, but read on to find out more!
Quick Start (Minimal Example)
Here’s 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.
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, put #[machine] above the derive(s).
// ❌ This will cause an error
// ↩ note the position of the derive
// ✅ This will work
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:
3. Complex Transitions & Data-Bearing States
Defining State Data
States can hold data. For example:
// ...
// ...
We use self.transition_with(data) instead of self.transition() to transition to a state that carries data.
Accessing State Data
Use .get_state_data() or .get_state_data_mut() to interact with the state-specific data:
4. Reconstructing State Machines from Persistent Data
State machines in real-world applications often need to persist their state—saving to and loading from external storage like databases. Reconstructing a state machine from this data must be both robust and type-safe. Statum's #[validators] macro simplifies this process, ensuring seamless integration between your persistent data and state machine logic.
Using #[validators] to Reconstruct State Machines
Here's a quick example to illustrate how #[validators] helps reconstruct state machines from persistent data:
use Serialize;
use ;
// the struct that represents our persistent data
// Define validators for each state
// Note: the validator method names are the same as the state variants but begin with is_*
In this example, the #[validators] macro ensures that:
- Fields of the machine (
client,name,priority) are automatically available inside validator methods. db_data.to_machine()calls the macro-generatedto_machinemethod to determine the appropriate state and reconstruct the state machine.- Using
matchonTaskMachineWrapper, the reconstructed machine's state determines the behavior, ensuring type-safe and intuitive handling
Why #[validators]?
The #[validators] macro exists to solve a key problem: connecting persistent data to state machines in a type-safe, ergonomic, and flexible way.
-
Defining State Conditions for Persistent Data:
When data is stored persistently (e.g., in a database), it typically includes information about the current state of an entity. To accurately reconstruct the state machine from this data, we must clearly define what it means for the data to be in each possible state of the machine.
-
Handling Complex Validation Logic:
Determining the state based on persistent data can be intricate. Various fields, relationships, or external factors might influence the state determination. Statum provides the flexibility for developers to implement custom validation logic tailored to their specific requirements.
-
Organized Validation via impl Blocks:
By defining validation methods within an impl block on the persistent data struct (e.g., DbData),
statumensures that there is a dedicated method for each state variant. This organization:- Enforces Completeness: Guarantees that every state has an associated validator.
- Enhances Readability: Centralizes state-related validation logic, making the codebase easier to understand and maintain.
- Leverages Rust’s Type System: Ensures that validations are type-safe and integrated seamlessly with the rest of the Rust code.
-
Constructing State-Specific Data Within Validators:
For states that carry additional data (e.g., InProgress(DraftData)), the validator methods are responsible for constructing the necessary state-specific data. This design choice ensures that:
- Data Integrity: The state machine is instantiated with all required data, maintaining consistency and preventing runtime errors.
- Encapsulation: The logic for creating state-specific data is encapsulated within the validator, keeping the reconstruction process clean and modular.
- Flexibility: Developers can define exactly how state-specific data is derived from persistent data, accommodating diverse and complex scenarios.
Macro-Generated Reconstruction
The #[validators] macro also generates a to_machine method that automates the process of:
- Validating the state using the corresponding methods.
- It does this by generated try_from implementations for each state.
- Constructing the state machine with the correct state and any state-specific data.
Tip: If any of your validators are async, ensure you call .to_machine() with .await to avoid compilation errors.
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're using the nightly toolchain and you see warnings like:
= note: no expected values for `feature`
= help: consider adding `serde` as a feature in `Cargo.toml`
it means you have the unexpected_cfgs lint enabled but you haven’t told your crate “feature = serde” 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.