rust_fsm/lib.rs
1/*!
2[![Documentation][docs-badge]][docs-link]
3[![Latest Version][crate-badge]][crate-link]
4
5The `rust-fsm` crate provides a simple and universal framework for building
6state machines in Rust with minimum effort.
7
8The essential part of this crate is the
9[`StateMachineImpl`](trait.StateMachineImpl.html) trait. This trait allows a
10developer to provide a strict state machine definition, e.g. specify its:
11
12* An input alphabet - a set of entities that the state machine takes as
13 inputs and performs state transitions based on them.
14* Possible states - a set of states this machine could be in.
15* An output alphabet - a set of entities that the state machine may output
16 as results of its work.
17* A transition function - a function that changes the state of the state
18 machine based on its current state and the provided input.
19* An output function - a function that outputs something from the output
20 alphabet based on the current state and the provided inputs.
21* The initial state of the machine.
22
23Note that on the implementation level such abstraction allows build any type
24of state machines:
25
26* A classical state machine by providing only an input alphabet, a set of
27 states and a transition function.
28* A Mealy machine by providing all entities listed above.
29* A Moore machine by providing an output function that do not depend on the
30 provided inputs.
31
32## Feature flags
33
34### Default
35
36- `std` - implement features that require the `std` environment. See below.
37- `dsl` - re-export `rust-fsm-dsl` from `rust-fsm`. Recommended to leave this on
38 for the best development experience.
39
40### Non-default
41
42- `diagram` - generate Mermaid state diagrams in the doc strings. See below.
43
44## Usage in `no_std` environments
45
46This library has the feature named `std` which is enabled by default. You
47may want to import this library as
48`rust-fsm = { version = "0.8", default-features = false, features = ["dsl"] }`
49to use it in a `no_std` environment. This only affects error types (the `Error`
50trait is only available in `std`).
51
52The DSL implementation re-export is gated by the feature named `dsl` which is
53also enabled by default.
54
55## Use
56
57Initially this library was designed to build an easy to use DSL for defining
58state machines on top of it. Using the DSL will require to connect an
59additional crate `rust-fsm-dsl` (this is due to limitation of the procedural
60macros system).
61
62### Using the DSL for defining state machines
63
64The DSL is parsed by the `state_machine` macro. Here is a little example.
65
66```rust
67use rust_fsm::*;
68
69state_machine! {
70 #[derive(Debug)]
71 #[repr(C)]
72 /// A Circuit Breaker state machine.
73 circuit_breaker(Closed)
74
75 Closed(Unsuccessful) => Open [SetupTimer],
76 Open(TimerTriggered) => HalfOpen,
77 HalfOpen => {
78 Successful => Closed,
79 Unsuccessful => Open [SetupTimer]
80 }
81}
82```
83
84This code sample:
85
86* Defines a state machine called `circuit_breaker`;
87* Derives the `Debug` trait for it. All attributes you use here (like
88 `#[repr(C)]`) will be applied to all types generated by this macro. If you
89 want to apply attributes or a docstring to the `mod` generated by this macro,
90 just put it before the macro invocation.
91* Sets the initial state of this state machine to `Closed`;
92* Defines state transitions. For example: on receiving the `Successful`
93 input when in the `HalfOpen` state, the machine must move to the `Closed`
94 state;
95* Defines outputs. For example: on receiving `Unsuccessful` in the
96 `Closed` state, the machine must output `SetupTimer`.
97
98This state machine can be used as follows:
99
100```rust,ignore
101// Initialize the state machine. The state is `Closed` now.
102let mut machine = circuit_breaker::StateMachine::new();
103// Consume the `Successful` input. No state transition is performed.
104let _ = machine.consume(&circuit_breaker::Input::Successful);
105// Consume the `Unsuccesful` input. The machine is moved to the `Open`
106// state. The output is `SetupTimer`.
107let output = machine.consume(&circuit_breaker::Input::Unsuccessful).unwrap();
108// Check the output
109if let Some(circuit_breaker::Output::SetupTimer) = output {
110 // Set up the timer...
111}
112// Check the state
113if let circuit_breaker::State::Open = machine.state() {
114 // Do something...
115}
116```
117
118The following entities are generated:
119
120* An empty structure `circuit_breaker::Impl` that implements the
121 `StateMachineImpl` trait.
122* Enums `circuit_breaker::State`, `circuit_breaker::Input` and
123 `circuit_breaker::Output` that represent the state, the input alphabet and the
124 output alphabet respectively.
125* Type alias `circuit_breaker::StateMachine` that expands to
126 `StateMachine<circuit_breaker::Impl>`.
127
128Note that if there is no outputs in the specification, the output alphabet is an
129empty enum and due to technical limitations of many Rust attributes, no
130attributes (e.g. `derive`, `repr`) are applied to it.
131
132Within the `state_machine` macro you must define at least one state
133transition.
134
135#### Visibility
136
137You can specify visibility like this:
138
139```rust
140use rust_fsm::*;
141
142state_machine! {
143 pub CircuitBreaker(Closed)
144
145 Closed(Unsuccessful) => Open [SetupTimer],
146 Open(TimerTriggered) => HalfOpen,
147 HalfOpen => {
148 Successful => Closed,
149 Unsuccessful => Open [SetupTimer],
150 }
151}
152```
153
154The default visibility is private.
155
156#### Custom alphabet types
157
158You can supply your own types to use as input, output or state. All of them are
159optional: you can use only one of them or all of them at once if you want to.
160The current limitation is that you have to supply a fully qualified type path.
161
162```rust,ignore
163use rust_fsm::*;
164
165pub enum Input {
166 Successful,
167 Unsuccessful,
168 TimerTriggered,
169}
170
171pub enum State {
172 Closed,
173 HalfOpen,
174 Open,
175}
176
177pub enum Output {
178 SetupTimer,
179}
180
181state_machine! {
182 #[state_machine(input(crate::Input), state(crate::State), output(crate::Output))]
183 circuit_breaker(Closed)
184
185 Closed(Unsuccessful) => Open [SetupTimer],
186 Open(TimerTriggered) => HalfOpen,
187 HalfOpen => {
188 Successful => Closed,
189 Unsuccessful => Open [SetupTimer]
190 }
191}
192```
193
194#### Diagrams
195
196`state_machine` macro can document your state machines with diagrams. This is
197controlled by the `diagram` feature, which is non-default. The diagrams are
198generated in the [Mermaid][mermaid] format. This feature includes the Mermaid
199script into the documentation page.
200
201To see this in action, download the repository and run:
202
203```bash
204cargo doc -p doc-example --open
205```
206
207
208
209### Without DSL
210
211The `state_machine` macro has limited capabilities (for example, a state
212cannot carry any additional data), so in certain complex cases a user might
213want to write a more complex state machine by hand.
214
215All you need to do to build a state machine is to implement the
216`StateMachineImpl` trait and use it in conjuctions with some of the provided
217wrappers (for now there is only `StateMachine`).
218
219You can see an example of the Circuit Breaker state machine in the
220[project repository][repo].
221
222[repo]: https://github.com/eugene-babichenko/rust-fsm
223[docs-badge]: https://docs.rs/rust-fsm/badge.svg
224[docs-link]: https://docs.rs/rust-fsm
225[crate-badge]: https://img.shields.io/crates/v/rust-fsm.svg
226[crate-link]: https://crates.io/crates/rust-fsm
227[mermaid]: https://mermaid.js.org/
228*/
229
230#![cfg_attr(not(feature = "std"), no_std)]
231
232use core::fmt;
233#[cfg(feature = "std")]
234use std::error::Error;
235
236#[cfg(feature = "dsl")]
237pub use rust_fsm_dsl::state_machine;
238
239#[cfg(feature = "diagram")]
240pub use aquamarine::aquamarine;
241
242/// This trait is designed to describe any possible deterministic finite state
243/// machine/transducer. This is just a formal definition that may be
244/// inconvenient to be used in practical programming, but it is used throughout
245/// this library for more practical things.
246pub trait StateMachineImpl {
247 /// The input alphabet.
248 type Input;
249 /// The set of possible states.
250 type State;
251 /// The output alphabet.
252 type Output;
253 /// The initial state of the machine.
254 // allow since there is usually no interior mutability because states are enums
255 #[allow(clippy::declare_interior_mutable_const)]
256 const INITIAL_STATE: Self::State;
257 /// The transition fuction that outputs a new state based on the current
258 /// state and the provided input. Outputs `None` when there is no transition
259 /// for a given combination of the input and the state.
260 fn transition(state: &Self::State, input: &Self::Input) -> Option<Self::State>;
261 /// The output function that outputs some value from the output alphabet
262 /// based on the current state and the given input. Outputs `None` when
263 /// there is no output for a given combination of the input and the state.
264 fn output(state: &Self::State, input: &Self::Input) -> Option<Self::Output>;
265}
266
267/// A convenience wrapper around the `StateMachine` trait that encapsulates the
268/// state and transition and output function calls.
269#[derive(Debug, Clone)]
270pub struct StateMachine<T: StateMachineImpl> {
271 state: T::State,
272}
273
274#[derive(Debug, Clone)]
275/// An error type that represents that the state transition is impossible given
276/// the current combination of state and input.
277pub struct TransitionImpossibleError;
278
279impl<T> StateMachine<T>
280where
281 T: StateMachineImpl,
282{
283 /// Create a new instance of this wrapper which encapsulates the initial
284 /// state.
285 pub fn new() -> Self {
286 Self::from_state(T::INITIAL_STATE)
287 }
288
289 /// Create a new instance of this wrapper which encapsulates the given
290 /// state.
291 pub fn from_state(state: T::State) -> Self {
292 Self { state }
293 }
294
295 /// Consumes the provided input, gives an output and performs a state
296 /// transition. If a state transition with the current state and the
297 /// provided input is not allowed, returns an error.
298 pub fn consume(
299 &mut self,
300 input: &T::Input,
301 ) -> Result<Option<T::Output>, TransitionImpossibleError> {
302 if let Some(state) = T::transition(&self.state, input) {
303 let output = T::output(&self.state, input);
304 self.state = state;
305 Ok(output)
306 } else {
307 Err(TransitionImpossibleError)
308 }
309 }
310
311 /// Returns the current state.
312 pub fn state(&self) -> &T::State {
313 &self.state
314 }
315}
316
317impl<T> Default for StateMachine<T>
318where
319 T: StateMachineImpl,
320{
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl fmt::Display for TransitionImpossibleError {
327 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
328 write!(
329 f,
330 "cannot perform a state transition from the current state with the provided input"
331 )
332 }
333}
334
335#[cfg(feature = "std")]
336impl Error for TransitionImpossibleError {
337 fn source(&self) -> Option<&(dyn Error + 'static)> {
338 None
339 }
340}