3 unstable releases
0.2.0 | Apr 13, 2023 |
---|---|
0.1.1 | Apr 12, 2023 |
0.1.0 | Apr 12, 2023 |
#2537 in Algorithms
9KB
90 lines
Finite state machine
Create finite state machines in rust with macros. The macro generates a struct, event enums and traits and also a decider trait. The decider trait is used to decide which state to transition to. Each state has a function which is called automatically when the state is entered. An invalid state and an end state are automatically added. You also need to implement a function for each transition. The macro automatically generates traits for everything you need to implement so the validity is checked on compile time.
Usage
use finite_state_machine::state_machine;
// Debug is only needed if the verbose feature is enabled
#[derive(Debug, Default)]
struct Data {
...
}
state_machine!(
// The name of the state machine and the type of the data, you can also use livetimes here
CircuitBreaker(Data);
// the first state will automatically become the start state, no matter the name
Closed {
Ok => Closed, // on Ok event go to Closed state
AmperageTooHigh => Open // on AmperageTooHigh event go to open state
},
Open {
AttemptReset => HalfOpen,
Wait => Open
},
HalfOpen {
Success => Closed,
AmperageTooHigh => Open,
MaxAttemps => End
}
);
use circuit_breaker::*;
// now you need to implement the decider trait which emits events which decide which state to transition to
impl Deciders for CircuitBreaker {
fn closed(&self) -> circuit_breaker::ClosedEvents {
if self.data.current_amperage > self.data.max_amperage {
circuit_breaker::ClosedEvents::AmperageTooHigh
} else {
circuit_breaker::ClosedEvents::Ok
}
}
...
}
// now we need to implement the transition trait for each state
impl ClosedTransitions for CircuitBreaker {
fn amperage_too_high(&mut self) -> Result<(), &'static str> {
self.data.tripped_at = Some(SystemTime::now());
Ok(())
}
fn ok(&mut self) -> Result<(), &'static str> {
self.data.current_amperage += 1;
std::thread::sleep(Duration::from_millis(500));
Ok(())
}
fn illegal(&mut self) {}
}
For more details, check the examples folder.
How it works
- The macro will create a module with the name of the state machine which is transformed to snake case. In the examples case:
CircuitBreaker
->circuit_breaker
. - The macro will generate a deciders trait. This trait has a function for each state. You need to implement this trait for your struct. In our case
impl Deciders for CircuitBreaker
. The decider functions only get a non mutable reference&self
and must return an enum with a variant for each transition. This enum is generated by the macro. In our casecircuit_breaker::ClosedEvents
, etc. There is also anIllegal
variant which is used when you encounter a state of the struct which is illegal/impossible. This will transition to theInvalid
state. In an isolated state machine this should never happen. But if you use the state machine in a larger system, this can happen. - For each state you create, a
<StateName>Transitions
trait will be generated which you need to implement for your struct. In our caseimpl ClosedTransitions for CircuitBreaker
. These functions get a mutable reference to self&mut self
and must return aResult<(), &'static str>
. The&'static str
is used to return an error message. If you return anErr
the state machine will transition to the invalid state which is automatically added. When entering this state the machine stops and returns to you the error message.
Debugging
You can enable the feature verbose
to get more information about the state machine. This will print the current state, the transition and the current data.