5 releases

0.1.4 Oct 31, 2024
0.1.3 Oct 29, 2024
0.1.2 Oct 27, 2024
0.1.1 Oct 26, 2024
0.1.0 Oct 26, 2024

#1456 in Algorithms

MIT license

40KB
390 lines

Statement - An Event-Driven State Machine

Statement is an event-driven state machine implementation library. Statement is easy to use, and provides a great deal of flexibility around how state machines are defined.

How do I use it?

Statement is organized around the idea that you typically want a state machine per instance for a potentially large number of business entities of the same type. These might be TCP connections, web sessions, hotel reservations, orders, or anything else that goes through a predictable set of states when events happen.

Much more information is available in the docs: https://docs.rs/statement/latest/statement/

Example

use anyhow::{anyhow};
use statement::{StateMachineFactory, StateMachineError};

fn test_double_transition<'a>() -> anyhow::Result<()> {
    #[derive(Eq, PartialEq)]
    enum StateMachineMessage {
        GoToTwo
    }

    // State here is just an integer
    let factory = StateMachineFactory::new()
        // Evaluate all transitions in a loop
        // until no transition occurs
        .cycle(true)
        // When we receive a GoToTwo event
        // while in state 1, go to state 2
        .with_event_transition(
            &StateMachineMessage::GoToTwo,
            1,
            2
        )
        // When we transition to state 2,
        // immediately transition to state 3
        .with_auto_transition(
            2,
            3
        )
        // Lock the factory object so that
        // we can build a state machine
        .lock();

    // Build the state machine, with an empty () as data
    // (we don't care about data for this example)
    let mut sm = factory.build(1, ());

    // The StateMachine starts in state 1
    assert_eq!(1, sm.state);

    // Handling an event tells us what state we end up in
    match sm.handle_event(StateMachineMessage::GoToTwo) {
        Ok(state) => {
            assert_eq!(3, *state);
        }
        Err(StateMachineError::EffectError(from, to, e)) => {
            return Err(anyhow!("error changing state from {} to {}: {}", from, to, e));
        }
    };

    // Because of the two transitions that we defined,
    // we end up in state 3
    assert_eq!(3, sm.state);
    Ok(())
}

This is a longer example, showing use of state machine data and more complex transitions:

use std::sync::atomic::Ordering::SeqCst;
use anyhow::anyhow;
use atomic_float::AtomicF64;
use statement::FromState::{Any, AnyOf};
use statement::{StateMachineFactory, StateTransitionEffectData};
use statement::ToState::Same;

struct CalcData {
    pub input_value: AtomicF64,
    pub stored_value: AtomicF64,
}

#[test]
fn calculator_test() -> anyhow::Result<()> {
    #[derive(Copy, Clone, Eq, PartialEq, Debug)]
    enum States {
        Idle,
        Adding,
        Subtracting,
        Multiplying,
        Dividing
    }

    #[derive(Copy, Clone, Eq, PartialEq, Debug)]
    enum Events {
        Clear,
        Digit { digit: u8 },
        Add,
        Subtract,
        Multiply,
        Divide,
        Equals
    }

    impl Events {
        fn is_digit(&self) -> bool {
            if let Events::Digit { digit: _ } = self { true } else { false }
        }
    }

    let mut init_data = CalcData {
        input_value: AtomicF64::new(0f64),
        stored_value: AtomicF64::new(0f64)
    };

    let mut sm = StateMachineFactory::<Events, States, &CalcData>::new()
        // This is an example of a logger that runs before any other transition, but doesn't
        // do anything in terms of state transitions itself.
        .with_transition_effect(
            Any,
            Same,
            |d| {
                print!("user sent {:?} event", d.event);
                Ok(())
            })
        .with_predicated_transition_effect(
            Any,
            Same,
            |d| d.event.is_digit(),
            |d| {
                if let Events::Digit { digit } = d.event {
                    append_digit(d.data, digit.clone());
                }
                Ok(())
            })
        .with_predicated_transition_effect(
            AnyOf(vec![States::Adding, States::Subtracting, States::Multiplying, States::Dividing]),
            States::Idle,
            |d| {
                match d.event {
                    Events::Add | Events::Subtract | Events::Multiply | Events::Divide | Events::Equals => true,
                    _ => false
                }
            },
            |d| {
                apply_function(d);
                Ok(())
            })
        .with_event_transition_effect(&Events::Add, States::Idle, States::Adding, |d| {
            swap(d.data);
            Ok(())
        })
        .with_event_transition_effect(&Events::Subtract, States::Idle, States::Subtracting, |d| {
            swap(d.data);
            Ok(())
        })
        .with_event_transition_effect(&Events::Multiply, States::Idle, States::Multiplying, |d| {
            swap(d.data);
            Ok(())
        })
        .with_event_transition_effect(&Events::Divide, States::Idle, States::Dividing, |d| {
            swap(d.data);
            Ok(())
        })
        // This is an example of a logger that runs after any other transition, but doesn't
        // do anything in terms of state transitions itself. It continues the log lines from
        // the earlier logger
        .with_transition_effect(
            Any,
            Same,
            |d| {
                println!(", input value is {}, stored value is {}", d.data.input_value.load(SeqCst), d.data.stored_value.load(SeqCst));
                Ok(())
            })
        .lock().build(States::Idle, &mut init_data);

    let error_mapper = |_| { anyhow!("error transitioning") };
    sm.handle_event(Events::Digit {digit: 2}).map_err(error_mapper)?;
    sm.handle_event(Events::Add).map_err(error_mapper)?;
    sm.handle_event(Events::Digit {digit: 0}).map_err(error_mapper)?;
    sm.handle_event(Events::Subtract).map_err(error_mapper)?;
    sm.handle_event(Events::Digit {digit: 1}).map_err(error_mapper)?;
    sm.handle_event(Events::Multiply).map_err(error_mapper)?;
    sm.handle_event(Events::Digit {digit: 1}).map_err(error_mapper)?;
    sm.handle_event(Events::Digit {digit: 2}).map_err(error_mapper)?;
    sm.handle_event(Events::Digit {digit: 6}).map_err(error_mapper)?;
    sm.handle_event(Events::Divide).map_err(error_mapper)?;
    sm.handle_event(Events::Digit {digit: 3}).map_err(error_mapper)?;
    sm.handle_event(Events::Equals).map_err(error_mapper)?;

    assert_eq!(42f64, sm.data.input_value.load(SeqCst));

    return Ok(());

    fn append_digit(d: &CalcData, b: u8) {
        let input_value_current = d.input_value.load(SeqCst);
        d.input_value.store(input_value_current * 10f64 + b as f64, SeqCst);
    }
    fn swap(d: &CalcData) {
        let old_input_value = d.input_value.load(SeqCst);
        d.stored_value.store(old_input_value, SeqCst);
        d.input_value.store(0f64, SeqCst);
    }
    fn apply_function(arg: StateTransitionEffectData<Events, States, &CalcData>) {
        let old_stored_value = arg.data.stored_value.load(SeqCst);
        let old_input_value = arg.data.input_value.load(SeqCst);
        match arg.from {
            States::Adding => {
                arg.data.input_value.store(old_stored_value + old_input_value, SeqCst);
            }
            States::Subtracting => {
                arg.data.input_value.store(old_stored_value - old_input_value, SeqCst);
            }
            States::Multiplying => {
                arg.data.input_value.store(old_stored_value * old_input_value, SeqCst);
            }
            States::Dividing => {
                arg.data.input_value.store(old_stored_value / old_input_value, SeqCst);
            }
            States::Idle => {}
        }
    }
}

Dependencies

~240–700KB
~16K SLoC