Macro rustfsm::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
.