Skip to content

Latest commit

 

History

History
773 lines (612 loc) · 19.7 KB

File metadata and controls

773 lines (612 loc) · 19.7 KB

Statechart User Guide

This guide provides step-by-step tutorials and practical examples for using the Rust statechart library.

Table of Contents

  1. Getting Started
  2. Basic Concepts
  3. Your First State Machine
  4. Working with Context
  5. Guards and Actions
  6. Hierarchical States
  7. Parallel States
  8. History States
  9. Validation and Debugging
  10. Visualization
  11. Best Practices
  12. Common Patterns

Getting Started

Installation

Add the statechart library to your Cargo.toml:

[dependencies]
statechart = "0.1.0"

Basic Import

use statechart::prelude::*;

This imports all the essential types and traits you'll need.

Basic Concepts

States, Events, and Context

Every state machine has three core components:

  1. States - The different conditions your system can be in
  2. Events - Things that happen that might cause state changes
  3. 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,
}

The Builder Pattern

State machines are created using a builder pattern:

let definition = StateMachineBuilder::<LightState, LightEvent, LightContext>::new()
    .initial_state(LightState::Off)
    // Add states and transitions...
    .build()?;

Your First State Machine

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(())
}

Working with Context

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 and Actions

Guards control when transitions can occur, and actions execute code during transitions.

Guards

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)?)
}

Complex Guards

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

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

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

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, ())?)
}

Validation and Debugging

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(())
}

Visualization

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(())
}

Best Practices

1. Keep States Simple

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,
}

2. Use Meaningful Event Names

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,
}

3. Keep Context Focused

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>,
}

4. Use Guards for Business Logic

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()
    })
);

5. Handle Errors Gracefully

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)))
            }
        }
    })],
)

Common Patterns

1. Timeout Pattern

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));
    }
}

2. State Machine Composition

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(())
    }
}

3. Event Sourcing Pattern

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.