squads_rustfsm_trait/
lib.rs

1use std::{
2    error::Error,
3    fmt::{Debug, Display, Formatter},
4};
5
6/// This trait defines a state machine (more formally, a [finite state
7/// transducer](https://en.wikipedia.org/wiki/Finite-state_transducer)) which accepts events (the
8/// input alphabet), uses them to mutate itself, and (may) output some commands (the output
9/// alphabet) as a result.
10pub trait StateMachine: Sized {
11    /// The error type produced by this state machine when handling events
12    type Error: Error;
13    /// The type used to represent different machine states. Should be an enum.
14    type State;
15    /// The type used to represent state that common among all states. Should be a struct.
16    type SharedState;
17    /// The type used to represent events the machine handles. Should be an enum.
18    type Event;
19    /// The type used to represent commands the machine issues upon transitions.
20    type Command;
21
22    /// Handle an incoming event, returning any new commands or an error. Implementations may
23    /// mutate current state, possibly moving to a new state.
24    fn on_event(
25        &mut self,
26        event: Self::Event,
27    ) -> Result<Vec<Self::Command>, MachineError<Self::Error>>;
28
29    fn name(&self) -> &str;
30
31    /// Returns the current state of the machine
32    fn state(&self) -> &Self::State;
33    fn set_state(&mut self, new_state: Self::State);
34
35    /// Returns the current shared state of the machine
36    fn shared_state(&self) -> &Self::SharedState;
37
38    /// Returns true if the machine's current state is a final one
39    fn has_reached_final_state(&self) -> bool;
40
41    /// Given the shared data and new state, create a new instance.
42    fn from_parts(state: Self::State, shared: Self::SharedState) -> Self;
43
44    /// Return a PlantUML definition of the fsm that can be used to visualize it
45    fn visualizer() -> &'static str;
46}
47
48/// The error returned by [StateMachine]s when handling events
49#[derive(Debug)]
50pub enum MachineError<E: Error> {
51    /// An undefined transition was attempted
52    InvalidTransition,
53    /// Some error occurred while processing the transition
54    Underlying(E),
55}
56
57impl<E: Error> From<E> for MachineError<E> {
58    fn from(e: E) -> Self {
59        Self::Underlying(e)
60    }
61}
62
63impl<E: Error> Display for MachineError<E> {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        match self {
66            MachineError::InvalidTransition => f.write_str("Invalid transition"),
67            MachineError::Underlying(e) => Display::fmt(&e, f),
68        }
69    }
70}
71impl<E: Error> Error for MachineError<E> {}
72
73pub enum MachineUpdate<Machine>
74where
75    Machine: StateMachine,
76{
77    InvalidTransition,
78    Ok { commands: Vec<Machine::Command> },
79}
80
81impl<M> MachineUpdate<M>
82where
83    M: StateMachine,
84{
85    /// Unwraps the machine update, panicking if the transition was invalid.
86    pub fn unwrap(self) -> Vec<M::Command> {
87        match self {
88            Self::Ok { commands } => commands,
89            Self::InvalidTransition => panic!("Transition was not successful!"),
90        }
91    }
92}
93
94/// A transition result is emitted every time the [StateMachine] handles an event.
95pub enum TransitionResult<Machine, DestinationState>
96where
97    Machine: StateMachine,
98    DestinationState: Into<Machine::State>,
99{
100    /// This state does not define a transition for this event from this state. All other errors
101    /// should use the [Err](enum.TransitionResult.html#variant.Err) variant.
102    InvalidTransition,
103    /// The transition was successful
104    Ok {
105        commands: Vec<Machine::Command>,
106        new_state: DestinationState,
107    },
108    /// There was some error performing the transition
109    Err(Machine::Error),
110}
111
112impl<Sm, Ds> TransitionResult<Sm, Ds>
113where
114    Sm: StateMachine,
115    Ds: Into<Sm::State>,
116{
117    /// Produce a transition with the provided commands to the provided state. No changes to shared
118    /// state if it exists
119    pub fn ok<CI>(commands: CI, new_state: Ds) -> Self
120    where
121        CI: IntoIterator<Item = Sm::Command>,
122    {
123        Self::Ok {
124            commands: commands.into_iter().collect(),
125            new_state,
126        }
127    }
128
129    /// Uses `Into` to produce a transition with no commands from the provided current state to
130    /// the provided (by type parameter) destination state.
131    pub fn from<CurrentState>(current_state: CurrentState) -> Self
132    where
133        CurrentState: Into<Ds>,
134    {
135        let as_dest: Ds = current_state.into();
136        Self::Ok {
137            commands: vec![],
138            new_state: as_dest,
139        }
140    }
141}
142
143impl<Sm, Ds> TransitionResult<Sm, Ds>
144where
145    Sm: StateMachine,
146    Ds: Into<Sm::State> + Default,
147{
148    /// Produce a transition with commands relying on [Default] for the destination state's value
149    pub fn commands<CI>(commands: CI) -> Self
150    where
151        CI: IntoIterator<Item = Sm::Command>,
152    {
153        Self::Ok {
154            commands: commands.into_iter().collect(),
155            new_state: Ds::default(),
156        }
157    }
158}
159
160impl<Sm, Ds> Default for TransitionResult<Sm, Ds>
161where
162    Sm: StateMachine,
163    Ds: Into<Sm::State> + Default,
164{
165    fn default() -> Self {
166        Self::Ok {
167            commands: vec![],
168            new_state: Ds::default(),
169        }
170    }
171}
172
173impl<Sm, Ds> TransitionResult<Sm, Ds>
174where
175    Sm: StateMachine,
176    Ds: Into<Sm::State>,
177{
178    /// Turns more-specific (struct) transition result into more-general (enum) transition result
179    pub fn into_general(self) -> TransitionResult<Sm, Sm::State> {
180        match self {
181            TransitionResult::InvalidTransition => TransitionResult::InvalidTransition,
182            TransitionResult::Ok {
183                commands,
184                new_state,
185            } => TransitionResult::Ok {
186                commands,
187                new_state: new_state.into(),
188            },
189            TransitionResult::Err(e) => TransitionResult::Err(e),
190        }
191    }
192
193    /// Transforms the transition result into a machine-ready outcome with commands and new state,
194    /// or a [MachineError]
195    #[allow(clippy::type_complexity)]
196    pub fn into_cmd_result(self) -> Result<(Vec<Sm::Command>, Sm::State), MachineError<Sm::Error>> {
197        let general = self.into_general();
198        match general {
199            TransitionResult::Ok {
200                new_state,
201                commands,
202            } => Ok((commands, new_state)),
203            TransitionResult::InvalidTransition => Err(MachineError::InvalidTransition),
204            TransitionResult::Err(e) => Err(MachineError::Underlying(e)),
205        }
206    }
207}