13 releases
new 0.1.12 | Jan 8, 2025 |
---|---|
0.1.11 | Jan 8, 2025 |
#684 in Development tools
46 downloads per month
15KB
194 lines
statum
A zero-boilerplate library for finite-state machines in Rust, with compile-time state transition validation.
Overview
The typestate pattern lets you encode state machines at the type level, making invalid state transitions impossible at compile time. This crate makes implementing typestates effortless through two attributes:
#[state]
- Define your states#[machine]
- Create your state machine
Installation
Run the following Cargo command in your project directory:
cargo add statum
Quick Start
Here's a simple example of a task processor:
use statum::{state, machine};
#[state]
pub enum TaskState {
New,
InProgress,
Complete,
}
#[machine]
pub struct Task<S: TaskState> {
id: String,
name: String,
}
impl Task<New> {
fn start(self) -> Task<InProgress> {
// Use transition() for simple state transitions
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(),
);
let task = task.start();
let task = task.complete();
}
Features
debug
(enabled by default) - Implements Debug for state machines and statesserde
- Adds serialization support via serde
Enable features in your Cargo.toml:
[dependencies]
statum = { version = "...", features = ["serde"] }
Advanced Features
States with Data
States can carry state-specific data:
#[state]
pub enum DocumentState {
Draft, // Simple state
Review(ReviewData), // State with data
Published,
}
pub struct ReviewData {
reviewer: String,
comments: Vec<String>,
}
#[machine]
pub struct Document<S: DocumentState> {
id: String,
content: String,
}
impl Document<Draft> {
fn submit_for_review(self, reviewer: String) -> Document<Review> {
// Use transition_with() for states with data
self.transition_with(ReviewData {
reviewer,
comments: vec![],
})
}
}
Accessing State Data
When a state has associated data, you can access it safely:
impl Document<Review> {
fn add_comment(&mut self, comment: String) {
// Safely modify state data
if let Some(review_data) = self.get_state_data_mut() {
review_data.comments.push(comment);
}
}
fn get_reviewer(&self) -> Option<&str> {
// Safely read state data
self.get_state_data().map(|data| data.reviewer.as_str())
}
fn approve(self) -> Document<Published> {
// Transition to a state without data
self.transition()
}
}
Database Integration
Here's how to integrate with external data sources:
#[derive(Debug)]
pub enum Error {
InvalidState,
}
#[derive(Clone)]
pub struct DbRecord {
id: String,
state: String,
}
// Convert from database record to state machine
impl TryFrom<&DbRecord> for Document<Draft> {
type Error = Error;
fn try_from(record: &DbRecord) -> Result<Self, Error> {
if record.state != "draft" {
return Err(Error::InvalidState);
}
Ok(Document::new(
record.id.clone(),
String::new(),
))
}
}
// Or use methods for more complex conversions with data
impl DbRecord {
fn try_to_review(&self, reviewer: String) -> Result<Document<Review>, Error> {
if self.state != "review" {
return Err(Error::InvalidState);
}
let doc = Document::new(
self.id.clone(),
String::new(),
);
Ok(doc.transition_with(ReviewData {
reviewer,
comments: vec![],
}))
}
}
Rich Context
Your state machine can maintain any context it needs:
#[machine]
pub struct DocumentProcessor<S: DocumentState> {
id: Uuid,
created_at: DateTime<Utc>,
metadata: HashMap<String, String>,
config: Config,
}
Contributing
Contributions welcome! Feel free to submit pull requests.
License
MIT License - see LICENSE for details.
Dependencies
~215–710KB
~17K SLoC