Macro rustfsm_procmacro::fsm[][src]

fsm!() { /* proc-macro */ }

Parses a DSL for defining finite state machines, and produces code implementing the StateMachine trait.

An example state machine definition of a card reader for unlocking a door:

use rustfsm_procmacro::fsm;
use std::convert::Infallible;
use rustfsm_trait::{StateMachine, TransitionResult};

fsm! {
    name CardReader; command Commands; error Infallible; shared_state SharedState;

    Locked --(CardReadable(CardData), shared on_card_readable) --> ReadingCard;
    Locked --(CardReadable(CardData), shared on_card_readable) --> Locked;
    ReadingCard --(CardAccepted, on_card_accepted) --> DoorOpen;
    ReadingCard --(CardRejected, on_card_rejected) --> Locked;
    DoorOpen --(DoorClosed, on_door_closed) --> Locked;
}

#[derive(Clone)]
pub struct SharedState {
    last_id: Option<String>
}

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Commands {
    StartBlinkingLight,
    StopBlinkingLight,
    ProcessData(CardData),
}

type CardData = String;

/// Door is locked / idle / we are ready to read
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct Locked {}

/// Actively reading the card
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ReadingCard {
    card_data: CardData,
}

/// The door is open, we shouldn't be accepting cards and should be blinking the light
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct DoorOpen {}
impl DoorOpen {
    fn on_door_closed(&self) -> CardReaderTransition<Locked> {
        TransitionResult::ok(vec![], Locked {})
    }
}

impl Locked {
    fn on_card_readable(&self, shared_dat: SharedState, data: CardData)
      -> CardReaderTransition<ReadingCardOrLocked> {
        match shared_dat.last_id {
            // Arbitrarily deny the same person entering twice in a row
            Some(d) if d == data => TransitionResult::ok(vec![], Locked {}.into()),
            _ => {
                // Otherwise issue a processing command. This illustrates using the same handler
                // for different destinations
                TransitionResult::ok_shared(
                    vec![
                        Commands::ProcessData(data.clone()),
                        Commands::StartBlinkingLight,
                    ],
                    ReadingCard { card_data: data.clone() }.into(),
                    SharedState { last_id: Some(data) }
                )
            }   
        }
    }
}

impl ReadingCard {
    fn on_card_accepted(&self) -> CardReaderTransition<DoorOpen> {
        TransitionResult::ok(vec![Commands::StopBlinkingLight], DoorOpen {})
    }
    fn on_card_rejected(&self) -> CardReaderTransition<Locked> {
        TransitionResult::ok(vec![Commands::StopBlinkingLight], Locked {})
    }
}

let crs = CardReaderState::Locked(Locked {});
let mut cr = CardReader { state: crs, shared_state: SharedState { last_id: None } };
let cmds = cr.on_event_mut(CardReaderEvents::CardReadable("badguy".to_string()))?;
assert_eq!(cmds[0], Commands::ProcessData("badguy".to_string()));
assert_eq!(cmds[1], Commands::StartBlinkingLight);

let cmds = cr.on_event_mut(CardReaderEvents::CardRejected)?;
assert_eq!(cmds[0], Commands::StopBlinkingLight);

let cmds = cr.on_event_mut(CardReaderEvents::CardReadable("goodguy".to_string()))?;
assert_eq!(cmds[0], Commands::ProcessData("goodguy".to_string()));
assert_eq!(cmds[1], Commands::StartBlinkingLight);

let cmds = cr.on_event_mut(CardReaderEvents::CardAccepted)?;
assert_eq!(cmds[0], Commands::StopBlinkingLight);

In the above example the first word is the name of the state machine, then after the comma the type (which you must define separately) of commands produced by the machine.

then each line represents a transition, where the first word is the initial state, the tuple inside the arrow is (eventtype[, event handler]), and the word after the arrow is the destination state. here eventtype is an enum variant , and event_handler is a function you must define outside the enum whose form depends on the event variant. the only variant types allowed are unit and one-item tuple variants. For unit variants, the function takes no parameters. For the tuple variants, the function takes the variant data as its parameter. In either case the function is expected to return a TransitionResult to the appropriate state.

The first transition can be interpreted as “If the machine is in the locked state, when a CardReadable event is seen, call on_card_readable (pasing in CardData) and transition to the ReadingCard state.

The macro will generate a few things:

  • A struct for the overall state machine, named with the provided name. Here:

    struct CardMachine {
        state: CardMachineState,
        shared_state: CardId,
    }
  • An enum with a variant for each state, named with the provided name + “State”.

    enum CardMachineState {
        Locked(Locked),
        ReadingCard(ReadingCard),
        Unlocked(Unlocked),
    }

    You are expected to define a type for each state, to contain that state’s data. If there is no data, you can simply: type StateName = ()

  • For any instance of transitions with the same event/handler which transition to different destination states (dynamic destinations), an enum named like DestAOrDestBOrDestC is generated. This enum must be used as the destination “state” from those handlers.

  • An enum with a variant for each event. You are expected to define the type (if any) contained in the event variant.

    enum CardMachineEvents {
      CardReadable(CardData)
    }
  • An implementation of the StateMachine trait for the generated state machine enum (in this case, CardMachine)

  • A type alias for a TransitionResult with the appropriate generic parameters set for your machine. It is named as your machine with Transition appended. In this case, CardMachineTransition.