Hey Rustaceans! I've created a library called *Statum* for building type-safe state machines in Rust. It ensures invalid state transitions are caught at *compile time*, giving you extra safety with minimal boilerplate.
### How it works
- *`#[state]`* transforms your enum variants into separate structs while generating a trait to represent the states.
- *`#[machine]`* injects some extra fields to track which state your machine is in, at the type level.
---
## States with Data
Need states to carry extra information?
```rust
#[state]
pub enum DocumentState {
Draft,
Review(ReviewData), // State with associated data
Published,
}
Many Rust state machine libraries need:
- Complex trait impls
- Manual checks for transitions
- Runtime guards for state data
*Statum* does all of this at compile time with a straightforward API.
---
## Gotchas: Attribute Ordering
- *`#[machine]`* must come before any user `#[derive(...)]` on the same struct, or you may see:
```
error[E0063]: missing fields `marker` and `state_data`
```
This is because the derive macros expand before Statum can inject its hidden fields.
---
## Feedback Welcome!
I'm actively developing Statum and would love community input:
- Which patterns do you commonly use for state machines?
- Features you’d like to see added?
- Ideas to make the API smoother?
One of the issue with type states if you aren’t aware is the state of each type needs to then be a type parameter of the parent type. Meaning creating a dynamic number of these state machines would be frustrating.
Do you have some thoughts or samples of say a Vec of Lights as an extension of the sample tou have?
Great question! If i understood you correctly, you’re asking about creating a `Vec` of state machines and operating on them. With the type-state pattern, this can be tricky because each state is a distinct type (`Light<Off>` and `Light<On>`), and `Vec` requires all elements to have the same type.
To directly manage a `Vec` of state machines, you’d need dynamic dispatch (e.g., `Box<dyn ...>`), but as you mentioned, dynamic dispatch can be a giant pain in the ass.
Instead of directly managing a `Vec` of state machines (`Vec<Light<Box<dyn ...>>>`), my approach is to work with a dynamic collection of raw data (e.g., `Vec<DbData>`) and reconstruct state machines on the fly using the `to_machine` method generated by `#[validators]`.
Why?
1. You don’t need to store state machines directly in a collection.
2. The `to_machine` method dynamically reconstructs the appropriate state machine from raw data at runtime.
3.You still retain `Statum`’s compile-time guarantees for state transitions once the machine is reconstructed.
Here’s what i mean:
use statum::{machine, state, validators};
#[state]
pub enum LightState {
Off,
On,
}
#[machine]
pub struct LightMachine<S: LightState> {
name: String,
}
impl LightMachine<Off> {
pub fn switch_on(self) -> LightMachine<On> {
self.transition()
}
}
impl LightMachine<On> {
pub fn switch_off(self) -> LightMachine<Off> {
self.transition()
}
}
// Represents dynamic data
#[derive(Clone)]
pub struct DbData {
id: String,
state: String,
}
#[validators(state = LightState, machine = LightMachine)]
impl DbData {
fn is_off(&self) -> Result<(), statum::Error> {
if self.state == "off" {
Ok(())
} else {
Err(statum::Error::InvalidState)
}
}
fn is_on(&self) -> Result<(), statum::Error> {
if self.state == "on" {
Ok(())
} else {
Err(statum::Error::InvalidState)
}
}
}
fn main() {
// raw data collection
let db_data = vec![
DbData {
id: "1".to_owned(),
state: "off".to_owned(),
},
DbData {
id: "2".to_owned(),
state: "on".to_owned(),
},
];
// do stuff with vec
// dynamically reconstruct and operate on state machines
for data in db_data {
let machine = data
.to_machine("Light".to_owned()) // Pass shared data here
.unwrap(); // Handle errors if needed
match machine {
LightMachineState::Off(light) => handle_off_light(light),
LightMachineState::On(light) => handle_on_light(light),
}
}
}
fn handle_off_light(light: LightMachine<Off>) {
println!("Turning light on: {}", light.name);
}
fn handle_on_light(light: LightMachine<On>) {
println!("Turning light offf: {}", light.name);
}
This approach is kind of indirect but it solves the underlying need: operating on many state machines dynamically without sacrificing `Statum`’s type safety.
Did this address your question? If so, I’ll add it to the README—this is a great point worth documenting!
I was thinking more along the lines of dynamic sets of client state machines or or device drivers. But right I get type states make this an adventure. Embedded Rust kind of has this problem actually, because the type states mean the state of say all the device drivers. This can easily mean some type parameter explosion of a parent structure that say has a set of peripheral driver structs as members
4 comments
[ 4.3 ms ] story [ 17.2 ms ] thread---
## Quick Example (Minimal)
```rust use statum::{state, machine};
#[state] pub enum TaskState { New, InProgress, Complete, }
#[machine] struct Task<S: TaskState> { id: String, name: String, }
impl Task<New> { fn start(self) -> Task<InProgress> { self.transition() } }
impl Task<InProgress> { fn complete(self) -> Task<Complete> { self.transition() } }
fn main() { let task = Task::new("task-1".to_owned(), "Important Task".to_owned()) .start() .complete(); } ```
### How it works - *`#[state]`* transforms your enum variants into separate structs while generating a trait to represent the states. - *`#[machine]`* injects some extra fields to track which state your machine is in, at the type level.
---
## States with Data
Need states to carry extra information?
```rust #[state] pub enum DocumentState { Draft, Review(ReviewData), // State with associated data Published, }
struct ReviewData { reviewer: String, comments: Vec<String>, }
impl Document<Draft> { fn submit_for_review(self, reviewer: String) -> Document<Review> { // Transition to `Review` with data: self.transition_with(ReviewData { reviewer, comments: vec![], }) } } ```
### Accessing State Data
When your state has associated data, use `.get_state_data()` or `.get_state_data_mut()`:
```rust impl Document<Review> { fn add_comment(&mut self, comment: String) { if let Some(review_data) = self.get_state_data_mut() { review_data.comments.push(comment); } }
} ```---
## Why Statum?
Many Rust state machine libraries need: - Complex trait impls - Manual checks for transitions - Runtime guards for state data
*Statum* does all of this at compile time with a straightforward API.
---
## Gotchas: Attribute Ordering - *`#[machine]`* must come before any user `#[derive(...)]` on the same struct, or you may see: ``` error[E0063]: missing fields `marker` and `state_data` ``` This is because the derive macros expand before Statum can inject its hidden fields.
---
## Feedback Welcome!
I'm actively developing Statum and would love community input: - Which patterns do you commonly use for state machines? - Features you’d like to see added? - Ideas to make the API smoother?
---
## Installation
```bash cargo add statum ```
Check it out on [GitHub](https://github.com/eboody/statum) or [Crates.io](https://crates.io/crates/statum). Let me know what you think!
Do you have some thoughts or samples of say a Vec of Lights as an extension of the sample tou have?
To directly manage a `Vec` of state machines, you’d need dynamic dispatch (e.g., `Box<dyn ...>`), but as you mentioned, dynamic dispatch can be a giant pain in the ass.
Instead of directly managing a `Vec` of state machines (`Vec<Light<Box<dyn ...>>>`), my approach is to work with a dynamic collection of raw data (e.g., `Vec<DbData>`) and reconstruct state machines on the fly using the `to_machine` method generated by `#[validators]`.
Why?
1. You don’t need to store state machines directly in a collection.
2. The `to_machine` method dynamically reconstructs the appropriate state machine from raw data at runtime.
3.You still retain `Statum`’s compile-time guarantees for state transitions once the machine is reconstructed.
Here’s what i mean:
This approach is kind of indirect but it solves the underlying need: operating on many state machines dynamically without sacrificing `Statum`’s type safety.Did this address your question? If so, I’ll add it to the README—this is a great point worth documenting!