stateforward-sml 1.2.0

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

pub struct OpenClose;
pub struct CdDetected;
pub struct Play;
pub struct Pause;
pub struct EndPause;
pub struct Stop;

sml! {
    Player {
         "open"_s <= *"empty"_s + event<OpenClose> / open_drawer,
         "open"_s + event<OpenClose> / close_drawer = "empty"_s,
         "empty"_s + event<CdDetected> / store_cd_info = "stopped"_s,
         "stopped"_s + event<Play> / start_playback = "playing"_s,
         "playing"_s + event<Pause> / pause_playback = "paused"_s,
         "paused"_s + event<EndPause> / resume_playback = "playing"_s,
         "playing"_s + event<Stop> / stop_playback = "stopped"_s,
         "paused"_s + event<Stop> / stop_playback = "stopped"_s,
         "stopped"_s + event<Stop> / stopped_again = "stopped"_s,
         "stopped"_s + event<OpenClose> / open_drawer = "open"_s,
         "paused"_s + event<OpenClose> / stop_and_open = "open"_s,
         "playing"_s + event<OpenClose> / stop_and_open = "open"_s,
         "open"_s + unexpected_event<Play> / invalid_play = X,
         "open"_s + on_entry<_> / entered_open,
         "open"_s + on_exit<_> / exited_open,
    }
}

#[derive(Default)]
struct Context {
    actions: usize,
}

macro_rules! action {
    ($name:ident, $event:ty) => {
        fn $name(&mut self, _event: &$event) -> Result<(), ()> {
            self.actions += 1;
            Ok(())
        }
    };
}

impl PlayerStateMachineContext for Context {
    action!(open_drawer, OpenClose);
    action!(close_drawer, OpenClose);
    action!(store_cd_info, CdDetected);
    action!(start_playback, Play);
    action!(pause_playback, Pause);
    action!(resume_playback, EndPause);
    action!(stop_playback, Stop);
    action!(stopped_again, Stop);
    action!(stop_and_open, OpenClose);
    action!(invalid_play, Play);

    fn entered_open(&mut self) -> Result<(), ()> {
        self.actions += 1;
        Ok(())
    }

    fn exited_open(&mut self) -> Result<(), ()> {
        self.actions += 1;
        Ok(())
    }
}

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

    sm.process_event(OpenClose).unwrap();
    assert!(matches!(sm.state(), PlayerStates::Open));
    sm.process_event(Play).unwrap();
    assert!(sm.is_terminated());
    assert_eq!(sm.context().actions, 4);
}

pub struct Ping;
pub struct Reset;

sml! {
    TransitionKinds {
        *"idle"_s + on_entry<_> / entered,
         "idle"_s + on_exit<_> / exited,
         "idle"_s + event<Ping> / internal_action,
         "idle"_s + event<Reset> / self_action = "idle"_s,
    }
}

#[derive(Default)]
struct TransitionContext {
    entries: usize,
    exits: usize,
    actions: usize,
}

impl TransitionKindsStateMachineContext for TransitionContext {
    fn entered(&mut self) -> Result<(), ()> {
        self.entries += 1;
        Ok(())
    }

    fn exited(&mut self) -> Result<(), ()> {
        self.exits += 1;
        Ok(())
    }

    fn internal_action(&mut self, _event: &Ping) -> Result<(), ()> {
        self.actions += 1;
        Ok(())
    }

    fn self_action(&mut self, _event: &Reset) -> Result<(), ()> {
        self.actions += 1;
        Ok(())
    }
}

#[test]
fn cpp_internal_and_self_transition_lifecycle_differs() {
    let mut sm = TransitionKindsStateMachine::new(TransitionContext::default());
    sm.initialize().unwrap();

    sm.process_event(Ping).unwrap();
    assert_eq!(sm.context().entries, 1);
    assert_eq!(sm.context().exits, 0);

    sm.process_event(Reset).unwrap();
    assert_eq!(sm.context().entries, 2);
    assert_eq!(sm.context().exits, 1);
    assert_eq!(sm.context().actions, 2);
}

pub struct Ack {
    valid: bool,
    id: u32,
}

sml! {
    EventData {
        *"waiting"_s + event<Ack> [is_valid] / (capture, audit) = X,
    }
}

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

impl EventDataStateMachineContext for EventContext {
    fn is_valid(&self, event: &Ack) -> Result<bool, ()> {
        Ok(event.valid)
    }

    fn capture(&mut self, event: &Ack) -> Result<(), ()> {
        self.captured = Some(event.id);
        Ok(())
    }

    fn audit(&mut self, _event: &Ack) -> Result<(), ()> {
        self.audits += 1;
        Ok(())
    }
}

#[test]
fn external_cpp_style_event_types_preserve_payloads() {
    let mut sm = EventDataStateMachine::new(EventContext::default());

    assert!(sm
        .process_event(Ack {
            valid: false,
            id: 1,
        })
        .is_err());
    sm.process_event(Ack {
        valid: true,
        id: 42,
    })
    .unwrap();

    assert!(sm.is_terminated());
    assert_eq!(sm.context().captured, Some(42));
    assert_eq!(sm.context().audits, 1);
}

sml! {
    Anonymous {
        *"initial"_s / prepare = "ready"_s,
    }
}

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

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

#[test]
fn cpp_anonymous_transition_spelling_initializes() {
    let mut sm = AnonymousStateMachine::new(AnonymousDslContext::default());

    assert!(matches!(sm.initialize().unwrap(), AnonymousStates::Ready));
    assert!(sm.context().prepared);
}

#[derive(Clone)]
pub struct Release {
    id: u32,
}

sml! {
    OriginCompletion {
        *"idle"_s + event<Release> = "step1"_s,
         "step1"_s + completion<Release> / finish_release = X,
    }
}

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

impl OriginCompletionStateMachineContext for OriginCompletionContext {
    fn finish_release(&mut self, event: &Release) -> Result<(), ()> {
        self.id = Some(event.id);
        Ok(())
    }
}

#[test]
fn cpp_origin_completion_preserves_external_event() {
    let mut sm = OriginCompletionStateMachine::new(OriginCompletionContext::default());

    sm.process_event(Release { id: 7 }).unwrap();

    assert!(sm.is_terminated());
    assert_eq!(sm.context().id, Some(7));
}

pub struct Build;

sml! {
    DataActionSequence {
        *"idle"_s + event<Build> / (record_build, make_value) = "ready"_s(u32),
         "ready"_s(u32) + event<Build> = X,
    }
}

#[derive(Default)]
struct DataActionSequenceContext {
    recorded: bool,
}

impl DataActionSequenceStateMachineContext for DataActionSequenceContext {
    fn record_build(&mut self, _event: &Build) -> Result<(), ()> {
        self.recorded = true;
        Ok(())
    }

    fn make_value(&mut self, _event: &Build) -> Result<u32, ()> {
        Ok(42)
    }
}

#[test]
fn only_final_action_constructs_output_state_data() {
    let mut sm = DataActionSequenceStateMachine::new(DataActionSequenceContext::default());

    sm.process_event(Build).unwrap();

    assert!(matches!(sm.state(), DataActionSequenceStates::Ready(42)));
    assert!(sm.context().recorded);
}

sml! {
    NamedEvent {
        *"idle"_s + sml::on_entry<_> / named_entered,
         "idle"_s + "connected"_e = sml::X,
    }
}

#[derive(Default)]
struct NamedEventContext {
    entries: usize,
}
impl NamedEventStateMachineContext for NamedEventContext {
    fn named_entered(&mut self) -> Result<(), ()> {
        self.entries += 1;
        Ok(())
    }
}

#[test]
fn cpp_named_event_literal_is_accepted() {
    let mut sm = NamedEventStateMachine::new(NamedEventContext::default());
    sm.initialize().unwrap();
    assert_eq!(sm.context().entries, 1);
    assert!(sm.is(&NamedEventStates::Idle));
    sm.process_event(NamedEventEvents::Connected).unwrap();
    assert!(sm.is(&NamedEventStates::X));
    assert!(sm.is_terminated());
}