statecraft
A generic state machine library which aims to enforce transitions between states
at compile time. Inspired by
Hoverbear.
Overview
Much of this code is intended to
prevent run-time checks of a state machine's state for two primary reasons:
- Run-time checks can incur a performance penalty
- Run-time checks are often subject to bugs (though Rust is quite good at
preventing this with things like Exhaustive
Matching).
By using this library to implement your state machine, you will have guarantees
from the Rust compiler that your state machine cannot end up in an invalid or
unplanned-for state.
Users of this library will be most interested in the following traits:
- [
State]: A state that your machine can exist in
- [
Stateful]: A thing which has some state associated with it. This is likely
your state machine.
- [
Transitionable] and [TryTransitionable]: Two traits which must be
implemented to allow your [Stateful] to transition between [State]s.
- [
AsyncTransitionable] and [AsyncTryTransitionable]: Two async variants
of Transitionable and TryTransitionable which allow async transitions
between [State]s.
Examples
Simple Three-State Machine
The following is a three-state machine, which is driven externally and
transitions between each state always succeed. While perhaps unrealistic, it
provides a decent overview to the crate. Transitions go from
Alpha->Beta->Gamma->Alpha.
use statecraft::{State, Stateful, Transitionable};
#[derive(State)]
struct Alpha;
#[derive(State)]
struct Beta;
#[derive(State)]
struct Gamma;
#[derive(Stateful)]
#[state(S)]
struct Machine<S: State> {
state: S
}
impl Transitionable<Beta> for Machine<Alpha> {
type NextStateful = Machine<Beta>;
fn transition(self) -> Self::NextStateful {
Machine { state: Beta }
}
}
impl Transitionable<Gamma> for Machine<Beta> {
type NextStateful = Machine<Gamma>;
fn transition(self) -> Self::NextStateful {
Machine { state: Gamma }
}
}
impl Transitionable<Alpha> for Machine<Gamma> {
type NextStateful = Machine<Alpha>;
fn transition(self) -> Self::NextStateful {
Machine { state: Alpha }
}
}
let alpha = Machine { state: Alpha };
let beta: Machine<Beta> = alpha.transition();
let gamma: Machine<Gamma> = beta.transition();
let alpha_again: Machine<Alpha> = gamma.transition();
Fallible Transitions
What happens if you have a more complex state machine where the transition
between two states may succeed or may fail. That's where the
[TryTransitionable] trait comes in. This trait ensures that if there is a
transition from one state to another which can fail, the machine returns back to
a valid state. Imagine our previous example, but the transition from
Beta->Gamma can fail. In this case, we want the machine to return to the
Alpha state.
# use statecraft::{Bailable, Recovered, State, Stateful, Transitionable, TryTransitionable};
#
# #[derive(State, Debug)]
# struct Alpha;
#
# #[derive(State, Debug)]
# struct Beta;
#
# #[derive(State, Debug)]
# struct Gamma;
#
# #[derive(Stateful, Debug)]
# #[state(S)]
# struct Machine<S: State> {
# state: S
# }
#
# impl Transitionable<Beta> for Machine<Alpha> {
# type NextStateful = Machine<Beta>;
#
# fn transition(self) -> Self::NextStateful {
# Machine { state: Beta }
# }
# }
#
impl TryTransitionable<Gamma, Alpha> for Machine<Beta> {
type SuccessStateful = Machine<Gamma>;
type FailureStateful = Machine<Alpha>;
type Error = Box<dyn std::error::Error + Send + Sync>;
fn try_transition(self) -> Result<Self::SuccessStateful, Recovered<Self::FailureStateful, Self::Error>> {
Err(Recovered::new(Machine { state: Alpha }, "We failed :(".into()))
}
}
impl Transitionable<Alpha> for Machine<Beta> {
type NextStateful = Machine<Alpha>;
fn transition(self) -> Self::NextStateful {
Machine { state: Alpha }
}
}
#
# impl Transitionable<Alpha> for Machine<Gamma> {
# type NextStateful = Machine<Alpha>;
#
# fn transition(self) -> Self::NextStateful {
# Machine { state: Alpha }
# }
# }
#
fn main() {
let alpha = Machine { state: Alpha };
let beta: Machine<Beta> = alpha.transition();
let alpha_again = beta.try_transition().expect_err("We should be failing!");
}
Async Transitions
Sometimes, the transition between two states may require async logic. This is
quite common in state machines which may use async I/O. For these cases, we
can use [AsyncTransitionable] and [AsyncTryTransitionable] respectively.
Consider a slight modification to our original three-state machine.
use statecraft::*;
#[derive(State, Debug)]
struct Alpha;
#[derive(State, Debug)]
struct Beta;
#[derive(State, Debug)]
struct Gamma;
#[derive(Stateful, SelfTransitionable, Debug)]
#[state(S)]
struct Machine<S: State> {
state: S
}
impl AsyncTransitionable<Beta> for Machine<Alpha> {
type NextStateful = Machine<Beta>;
async fn transition(self) -> Self::NextStateful {
Machine { state: Beta }
}
}
impl AsyncTryTransitionable<Gamma, Beta> for Machine<Beta> {
type SuccessStateful = Machine<Gamma>;
type FailureStateful = Machine<Beta>;
type Error = Box<dyn std::error::Error + Send + Sync>;
async fn try_transition(self) -> Result<Self::SuccessStateful, Recovered<Self::FailureStateful, Self::Error>> {
Ok(Machine { state: Gamma })
}
}
impl Transitionable<Alpha> for Machine<Gamma> {
type NextStateful = Machine<Alpha>;
fn transition(self) -> Self::NextStateful {
Machine { state: Alpha }
}
}
#[tokio::main]
async fn main() {
let alpha = Machine { state: Alpha };
let beta: Machine<Beta> = transition_async!(alpha, Beta);
let gamma: Machine<Gamma> = try_transition_async!(beta, Gamma).expect("Failed transition");
let alpha_again: Machine<Alpha> = transition!(gamma, Alpha);
}
Note how we can even mix async and non-async transitions in the same state
machine.