This guide provides step-by-step tutorials and practical examples for using the Rust statechart library.
- Getting Started
- Basic Concepts
- Your First State Machine
- Working with Context
- Guards and Actions
- Hierarchical States
- Parallel States
- History States
- Validation and Debugging
- Visualization
- Best Practices
- Common Patterns
Add the statechart library to your Cargo.toml:
[dependencies]
statechart = "0.1.0"use statechart::prelude::*;This imports all the essential types and traits you'll need.
Every state machine has three core components:
- States - The different conditions your system can be in
- Events - Things that happen that might cause state changes
- Context - Shared data that persists across state changes
// Define your states
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum LightState {
Off,
On,
Dimmed,
}
// Define your events
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum LightEvent {
TurnOn,
TurnOff,
Dim,
Brighten,
}
// Define your context (shared data)
#[derive(Debug)]
struct LightContext {
brightness: u8,
power_usage: f32,
}State machines are created using a builder pattern:
let definition = StateMachineBuilder::<LightState, LightEvent, LightContext>::new()
.initial_state(LightState::Off)
// Add states and transitions...
.build()?;Let's create a simple on/off switch:
use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum SwitchState { Off, On }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum SwitchEvent { Toggle }
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build the state machine definition
let definition = StateMachineBuilder::<SwitchState, SwitchEvent, ()>::new()
.initial_state(SwitchState::Off)
.add_simple_state(SwitchState::Off)
.add_simple_state(SwitchState::On)
.add_transition(
SwitchState::Off, // from
SwitchState::On, // to
SwitchEvent::Toggle, // on event
None, // no guard
vec![], // no actions
)
.add_transition(
SwitchState::On,
SwitchState::Off,
SwitchEvent::Toggle,
None,
vec![],
)
.build()?;
// Create the runtime machine
let mut machine = StateMachine::new(definition, ())?;
// Test it out
println!("Initial state: {:?}", machine.current_state());
machine.process_event(Event::new(SwitchEvent::Toggle))?;
println!("After toggle: {:?}", machine.current_state());
machine.process_event(Event::new(SwitchEvent::Toggle))?;
println!("After second toggle: {:?}", machine.current_state());
Ok(())
}Context allows you to store data that persists across state changes:
use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum CounterState { Idle, Counting }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum CounterEvent { Start, Stop, Increment }
#[derive(Debug)]
struct CounterContext {
count: u32,
max_count: u32,
}
fn create_counter_machine() -> Result<StateMachine<CounterState, CounterEvent, CounterContext>, Box<dyn std::error::Error>> {
let definition = StateMachineBuilder::new()
.initial_state(CounterState::Idle)
.add_simple_state(CounterState::Idle)
.add_simple_state(CounterState::Counting)
.add_transition(
CounterState::Idle,
CounterState::Counting,
CounterEvent::Start,
None,
vec![Box::new(|ctx| {
println!("Starting counter at {}", ctx.count);
Ok(())
})],
)
.add_transition(
CounterState::Counting,
CounterState::Idle,
CounterEvent::Stop,
None,
vec![Box::new(|ctx| {
println!("Stopping counter at {}", ctx.count);
Ok(())
})],
)
.build()?;
let context = CounterContext {
count: 0,
max_count: 10,
};
Ok(StateMachine::new(definition, context)?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut machine = create_counter_machine()?;
machine.process_event(Event::new(CounterEvent::Start))?;
println!("Count: {}", machine.context().count);
machine.process_event(Event::new(CounterEvent::Stop))?;
Ok(())
}Guards control when transitions can occur, and actions execute code during transitions.
Guards are functions that return bool to allow or prevent transitions:
use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum DoorState { Closed, Open }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum DoorEvent { OpenRequest, Close }
#[derive(Debug)]
struct DoorContext {
is_locked: bool,
access_code: Option<String>,
}
fn create_door_machine() -> Result<StateMachine<DoorState, DoorEvent, DoorContext>, Box<dyn std::error::Error>> {
let definition = StateMachineBuilder::new()
.initial_state(DoorState::Closed)
.add_simple_state(DoorState::Closed)
.add_simple_state(DoorState::Open)
.add_transition(
DoorState::Closed,
DoorState::Open,
DoorEvent::OpenRequest,
// Guard: only open if not locked
Some(Box::new(|ctx: &DoorContext| !ctx.is_locked)),
vec![Box::new(|ctx| {
println!("Door opened!");
Ok(())
})],
)
.add_transition(
DoorState::Open,
DoorState::Closed,
DoorEvent::Close,
None,
vec![Box::new(|ctx| {
println!("Door closed!");
Ok(())
})],
)
.build()?;
let context = DoorContext {
is_locked: false,
access_code: None,
};
Ok(StateMachine::new(definition, context)?)
}You can create complex guard logic:
use statechart::model::{AndGuard, OrGuard, NotGuard};
// Multiple conditions must be true
let and_guard = AndGuard::new(vec![
Box::new(|ctx: &DoorContext| !ctx.is_locked),
Box::new(|ctx: &DoorContext| ctx.access_code.is_some()),
]);
// Any condition can be true
let or_guard = OrGuard::new(vec![
Box::new(|ctx: &DoorContext| !ctx.is_locked),
Box::new(|ctx: &DoorContext| ctx.access_code == Some("admin".to_string())),
]);
// Negate a condition
let not_guard = NotGuard::new(
Box::new(|ctx: &DoorContext| ctx.is_locked)
);Hierarchical states allow you to create nested state machines:
use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MediaState {
Stopped,
Playing,
PlayingNormal,
PlayingFastForward,
Paused,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum MediaEvent {
Play,
Pause,
Stop,
FastForward,
Normal,
}
fn create_media_player() -> Result<StateMachine<MediaState, MediaEvent, ()>, Box<dyn std::error::Error>> {
// Create the composite "Playing" state
let playing_state = StateBuilder::<MediaState, MediaEvent, ()>::new(MediaState::Playing)
.composite()
.initial(MediaState::PlayingNormal) // Default substate
.on_entry(vec![Box::new(|_| {
println!("Started playing");
Ok(())
})])
.on_exit(vec![Box::new(|_| {
println!("Stopped playing");
Ok(())
})])
.build();
let definition = StateMachineBuilder::new()
.initial_state(MediaState::Stopped)
.add_simple_state(MediaState::Stopped)
.add_state(playing_state) // Add the composite state
.add_simple_state(MediaState::PlayingNormal)
.add_simple_state(MediaState::PlayingFastForward)
.add_simple_state(MediaState::Paused)
// Transitions between top-level states
.add_transition(MediaState::Stopped, MediaState::Playing, MediaEvent::Play, None, vec![])
.add_transition(MediaState::Playing, MediaState::Stopped, MediaEvent::Stop, None, vec![])
.add_transition(MediaState::Playing, MediaState::Paused, MediaEvent::Pause, None, vec![])
.add_transition(MediaState::Paused, MediaState::Playing, MediaEvent::Play, None, vec![])
// Transitions within the Playing state
.add_transition(MediaState::PlayingNormal, MediaState::PlayingFastForward, MediaEvent::FastForward, None, vec![])
.add_transition(MediaState::PlayingFastForward, MediaState::PlayingNormal, MediaEvent::Normal, None, vec![])
.build()?;
Ok(StateMachine::new(definition, ())?)
}Parallel states allow multiple state machines to run simultaneously:
use statechart::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum SystemState {
System,
// Network region
NetworkDisconnected,
NetworkConnected,
// Audio region
AudioMuted,
AudioUnmuted,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum SystemEvent {
Connect,
Disconnect,
Mute,
Unmute,
}
fn create_parallel_system() -> Result<StateMachine<SystemState, SystemEvent, ()>, Box<dyn std::error::Error>> {
// Create parallel state with two regions
let system_state = StateBuilder::<SystemState, SystemEvent, ()>::new(SystemState::System)
.parallel()
.build();
let definition = StateMachineBuilder::new()
.initial_state(SystemState::System)
.add_state(system_state)
// Network region states
.add_simple_state(SystemState::NetworkDisconnected)
.add_simple_state(SystemState::NetworkConnected)
// Audio region states
.add_simple_state(SystemState::AudioMuted)
.add_simple_state(SystemState::AudioUnmuted)
// Network transitions
.add_transition(SystemState::NetworkDisconnected, SystemState::NetworkConnected, SystemEvent::Connect, None, vec![])
.add_transition(SystemState::NetworkConnected, SystemState::NetworkDisconnected, SystemEvent::Disconnect, None, vec![])
// Audio transitions
.add_transition(SystemState::AudioUnmuted, SystemState::AudioMuted, SystemEvent::Mute, None, vec![])
.add_transition(SystemState::AudioMuted, SystemState::AudioUnmuted, SystemEvent::Unmute, None, vec![])
.build()?;
Ok(StateMachine::new(definition, ())?)
}History states remember the last active substate when exiting a composite state:
use statechart::prelude::*;
use statechart::model::{StateKind, HistoryType};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AppState {
Inactive,
Active,
ActiveEditing,
ActiveViewing,
HistoryShallow,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AppEvent {
Activate,
Deactivate,
Edit,
View,
}
fn create_app_with_history() -> Result<StateMachine<AppState, AppEvent, ()>, Box<dyn std::error::Error>> {
// Create history state
let history_state = StateBuilder::<AppState, AppEvent, ()>::new(AppState::HistoryShallow)
.history(HistoryType::Shallow)
.build();
// Create composite active state
let active_state = StateBuilder::<AppState, AppEvent, ()>::new(AppState::Active)
.composite()
.initial(AppState::HistoryShallow) // Start with history
.build();
let definition = StateMachineBuilder::new()
.initial_state(AppState::Inactive)
.add_simple_state(AppState::Inactive)
.add_state(active_state)
.add_state(history_state)
.add_simple_state(AppState::ActiveEditing)
.add_simple_state(AppState::ActiveViewing)
.add_transition(AppState::Inactive, AppState::Active, AppEvent::Activate, None, vec![])
.add_transition(AppState::Active, AppState::Inactive, AppEvent::Deactivate, None, vec![])
.add_transition(AppState::ActiveViewing, AppState::ActiveEditing, AppEvent::Edit, None, vec![])
.add_transition(AppState::ActiveEditing, AppState::ActiveViewing, AppEvent::View, None, vec![])
.build()?;
Ok(StateMachine::new(definition, ())?)
}The library provides comprehensive validation to catch errors early:
use statechart::validation::{validate, Validator};
fn validate_machine() -> Result<(), Box<dyn std::error::Error>> {
let definition = StateMachineBuilder::<MyState, MyEvent, ()>::new()
.initial_state(MyState::Start)
// ... build definition
.build()?;
// Quick validation
let report = validate(&definition);
if !report.is_valid() {
println!("Validation failed:");
for issue in report.issues() {
println!(" {}: {}", issue.severity(), issue.message());
}
return Err("Validation failed".into());
}
println!("State machine is valid!");
Ok(())
}
// Custom validation
fn custom_validation() -> Result<(), Box<dyn std::error::Error>> {
let mut validator = Validator::new();
// Add custom validation rules if needed
let report = validator.validate(&definition);
// Check specific validation aspects
if report.has_errors() {
println!("Found {} errors", report.error_count());
}
if report.has_warnings() {
println!("Found {} warnings", report.warning_count());
}
Ok(())
}Generate visual diagrams of your state machines:
use statechart::visualization::{to_dot, to_dot_with_options, DotOptions, StateStyle, TransitionStyle};
fn visualize_machine() -> Result<(), Box<dyn std::error::Error>> {
let definition = /* ... your state machine definition ... */;
// Basic DOT output
let dot = to_dot(&definition);
println!("{}", dot);
// Save to file
std::fs::write("state_machine.dot", dot)?;
// Customized visualization
let options = DotOptions::new()
.with_title("My State Machine")
.with_state_style(StateStyle::Rounded)
.with_transition_style(TransitionStyle::Curved)
.with_show_guards(true)
.with_show_actions(true);
let custom_dot = to_dot_with_options(&definition, &options);
std::fs::write("custom_state_machine.dot", custom_dot)?;
println!("DOT files generated! Use Graphviz to render:");
println!(" dot -Tpng state_machine.dot -o state_machine.png");
Ok(())
}Each state should represent a single, well-defined condition:
// Good: Clear, single-purpose states
enum PlayerState {
Stopped,
Playing,
Paused,
Buffering,
}
// Avoid: Compound states that mix concerns
enum BadPlayerState {
StoppedAndMuted,
PlayingButBuffering,
PausedWithError,
}Events should clearly describe what happened:
// Good: Action-oriented event names
enum UserEvent {
LoginRequested,
LogoutClicked,
PasswordChanged,
SessionExpired,
}
// Avoid: Vague or technical names
enum BadEvent {
Event1,
Update,
Change,
Process,
}Only store data that's truly shared across states:
// Good: Focused context
struct AuthContext {
user_id: Option<String>,
session_token: Option<String>,
login_attempts: u32,
}
// Avoid: Kitchen sink context
struct BadContext {
user_id: Option<String>,
ui_theme: String,
last_click_position: (i32, i32),
random_data: Vec<u8>,
}Put your business rules in guards, not in state transitions:
// Good: Business logic in guards
.add_transition(
State::LoggedOut,
State::LoggedIn,
Event::LoginAttempt,
Some(Box::new(|ctx| {
ctx.login_attempts < 3 && ctx.credentials_valid()
})),
vec![],
)
// Better: Named guards for complex logic
let login_allowed = NamedGuard::new(
"login_allowed",
Box::new(|ctx| {
ctx.login_attempts < 3 &&
ctx.credentials_valid() &&
!ctx.account_locked()
})
);Always handle action errors appropriately:
.add_transition(
State::Idle,
State::Processing,
Event::StartWork,
None,
vec![Box::new(|ctx| {
match ctx.start_background_task() {
Ok(_) => {
ctx.task_started = true;
Ok(())
}
Err(e) => {
log::error!("Failed to start task: {}", e);
Err(ActionError::new(format!("Task start failed: {}", e)))
}
}
})],
)Handle timeouts using event queuing:
use std::time::{Duration, Instant};
#[derive(Debug)]
struct TimerContext {
timeout_start: Option<Instant>,
timeout_duration: Duration,
}
impl TimerContext {
fn start_timeout(&mut self) {
self.timeout_start = Some(Instant::now());
}
fn is_timeout(&self) -> bool {
if let Some(start) = self.timeout_start {
start.elapsed() > self.timeout_duration
} else {
false
}
}
}
// In your main loop:
fn run_with_timeout(machine: &mut StateMachine<State, Event, TimerContext>) {
loop {
// Check for timeout
if machine.context().is_timeout() {
machine.process_event(Event::new(Event::Timeout)).unwrap();
}
// Process other events...
machine.process_queued_events().unwrap();
std::thread::sleep(Duration::from_millis(10));
}
}Combine multiple state machines:
struct CompositeSystem {
network_machine: StateMachine<NetworkState, NetworkEvent, NetworkContext>,
ui_machine: StateMachine<UiState, UiEvent, UiContext>,
}
impl CompositeSystem {
fn process_network_event(&mut self, event: NetworkEvent) -> Result<(), Box<dyn std::error::Error>> {
let transitioned = self.network_machine.process_event(Event::new(event))?;
// React to network state changes in UI
if transitioned {
match self.network_machine.current_state() {
NetworkState::Connected => {
self.ui_machine.process_event(Event::new(UiEvent::ShowConnected))?;
}
NetworkState::Disconnected => {
self.ui_machine.process_event(Event::new(UiEvent::ShowDisconnected))?;
}
}
}
Ok(())
}
}Store events for replay and debugging:
#[derive(Debug)]
struct EventSourcingContext {
event_log: Vec<(Instant, String)>,
}
impl EventSourcingContext {
fn log_event(&mut self, event_name: &str) {
self.event_log.push((Instant::now(), event_name.to_string()));
}
fn replay_events(&self) {
for (timestamp, event) in &self.event_log {
println!("{:?}: {}", timestamp, event);
}
}
}
// In your actions:
vec![Box::new(|ctx| {
ctx.log_event("user_login");
// ... rest of action
Ok(())
})]This user guide provides comprehensive coverage of the statechart library's features with practical examples and best practices. For detailed API documentation, see the API Reference.