stateforward-sml 1.2.0

sml.rs: a no-std Rust implementation of the sml.cpp transition-table DSL
Documentation
use sml::sml;

#[derive(Clone, Debug, PartialEq)]
pub struct Payload {
    value: u32,
    valid: bool,
}

sml! {
    Completion[
        states_attr: #[derive(Debug)],
        events_attr: #[derive(Debug)]
    ] {
        *Idle + Go(Payload) / origin = Step1,
        Idle + Other = Step1,
        Step1 + completion<Go>(Payload) [is_valid] / first_completion = Step2,
        Step2 + completion<Go>(Payload) / second_completion = Done,
    }
}

#[derive(Default)]
struct Context {
    calls: Vec<(&'static str, u32)>,
}

impl CompletionStateMachineContext for Context {
    fn origin(&mut self, payload: Payload) -> Result<(), ()> {
        self.calls.push(("origin", payload.value));
        Ok(())
    }

    fn is_valid(&self, payload: &Payload) -> Result<bool, ()> {
        Ok(payload.valid)
    }

    fn first_completion(&mut self, payload: Payload) -> Result<(), ()> {
        self.calls.push(("first", payload.value));
        Ok(())
    }

    fn second_completion(&mut self, payload: Payload) -> Result<(), ()> {
        self.calls.push(("second", payload.value));
        Ok(())
    }
}

#[test]
fn completion_transitions_chain_and_preserve_origin_data() {
    let mut sm = CompletionStateMachine::new(Context::default());

    let state = sm
        .process_event(CompletionEvents::Go(Payload {
            value: 42,
            valid: true,
        }))
        .unwrap();

    assert!(matches!(state, CompletionStates::Done));
    assert_eq!(
        sm.context().calls,
        [("origin", 42), ("first", 42), ("second", 42)]
    );
}

#[test]
fn completion_transition_is_specific_to_the_origin_event() {
    let mut sm = CompletionStateMachine::new(Context::default());

    let state = sm.process_event(CompletionEvents::Other).unwrap();

    assert!(matches!(state, CompletionStates::Step1));
    assert!(sm.context().calls.is_empty());
}

#[test]
fn failed_completion_guard_stops_the_chain_without_failing_origin_event() {
    let mut sm = CompletionStateMachine::new(Context::default());

    let state = sm
        .process_event(CompletionEvents::Go(Payload {
            value: 7,
            valid: false,
        }))
        .unwrap();

    assert!(matches!(state, CompletionStates::Step1));
    assert_eq!(sm.context().calls, [("origin", 7)]);
}

sml! {
    AsyncCompletion {
        *Idle + Go / async origin = Step,
        Step + completion<Go> / async complete = Done,
    }
}

#[derive(Default)]
struct AsyncContext {
    calls: Vec<&'static str>,
}

impl AsyncCompletionStateMachineContext for AsyncContext {
    async fn origin(&mut self) -> Result<(), ()> {
        self.calls.push("origin");
        Ok(())
    }

    async fn complete(&mut self) -> Result<(), ()> {
        self.calls.push("completion");
        Ok(())
    }
}

#[test]
fn completion_transitions_support_async_actions() {
    smol::block_on(async {
        let mut sm = AsyncCompletionStateMachine::new(AsyncContext::default());
        let state = sm.process_event(AsyncCompletionEvents::Go).await.unwrap();

        assert!(matches!(state, AsyncCompletionStates::Done));
        assert_eq!(sm.context().calls, ["origin", "completion"]);
    });
}

sml! {
    TemporaryCompletion[temporary_context: &mut u32] {
        *Idle + Go / origin_with_temporary = Step,
        Step + completion<Go> / completion_with_temporary = Done,
    }
}

struct TemporaryContext;

impl TemporaryCompletionStateMachineContext for TemporaryContext {
    fn origin_with_temporary(&mut self, value: &mut u32) -> Result<(), ()> {
        *value += 1;
        Ok(())
    }

    fn completion_with_temporary(&mut self, value: &mut u32) -> Result<(), ()> {
        *value += 10;
        Ok(())
    }
}

#[test]
fn completion_transitions_reborrow_mutable_temporary_context() {
    let mut sm = TemporaryCompletionStateMachine::new(TemporaryContext);
    let mut value = 0;

    let state = sm
        .process_event(&mut value, TemporaryCompletionEvents::Go)
        .unwrap();

    assert!(matches!(state, TemporaryCompletionStates::Done));
    assert_eq!(value, 11);
}

sml! {
    BorrowedCompletion {
        *Idle + Go(&'a Payload) = Step,
        Step + completion<Go>(&'a Payload) / capture_borrowed = Done,
    }
}

#[derive(Default)]
struct BorrowedContext {
    captured: Option<u32>,
}

impl BorrowedCompletionStateMachineContext for BorrowedContext {
    fn capture_borrowed(&mut self, payload: &Payload) -> Result<(), ()> {
        self.captured = Some(payload.value);
        Ok(())
    }
}

#[test]
fn completion_transitions_propagate_immutably_borrowed_origin_data() {
    let payload = Payload {
        value: 99,
        valid: true,
    };
    let mut sm = BorrowedCompletionStateMachine::new(BorrowedContext::default());

    let state = sm
        .process_event(BorrowedCompletionEvents::Go(&payload))
        .unwrap();

    assert!(matches!(state, BorrowedCompletionStates::Done));
    assert_eq!(sm.context().captured, Some(99));
}

sml! {
    AnonymousCompletion {
        *Initial + completion<_> / initialized = Ready,
        Ready + Go = Working,
        Working + completion<_> / stabilized = Done,
    }
}

#[derive(Default)]
struct AnonymousContext {
    calls: Vec<&'static str>,
}

impl AnonymousCompletionStateMachineContext for AnonymousContext {
    fn initialized(&mut self) -> Result<(), ()> {
        self.calls.push("initialized");
        Ok(())
    }

    fn stabilized(&mut self) -> Result<(), ()> {
        self.calls.push("stabilized");
        Ok(())
    }
}

#[test]
fn anonymous_completion_runs_during_initialization_and_after_events() {
    let mut sm = AnonymousCompletionStateMachine::new(AnonymousContext::default());

    assert!(matches!(
        sm.initialize().unwrap(),
        AnonymousCompletionStates::Ready
    ));
    assert!(matches!(
        sm.process_event(AnonymousCompletionEvents::Go).unwrap(),
        AnonymousCompletionStates::Done
    ));
    assert_eq!(sm.context().calls, ["initialized", "stabilized"]);
}

sml! {
    AsyncAnonymous {
        *Initial + completion<_> / async prepare = Ready,
        Ready + Go = Done,
    }
}

#[derive(Default)]
struct AsyncAnonymousContext {
    prepared: bool,
}

impl AsyncAnonymousStateMachineContext for AsyncAnonymousContext {
    async fn prepare(&mut self) -> Result<(), ()> {
        self.prepared = true;
        Ok(())
    }
}

#[test]
fn anonymous_initialization_supports_async_actions() {
    smol::block_on(async {
        let mut sm = AsyncAnonymousStateMachine::new(AsyncAnonymousContext::default());
        let state = sm.initialize().await.unwrap();

        assert!(matches!(state, AsyncAnonymousStates::Ready));
        assert!(sm.context().prepared);
    });
}