Skip to main content

Crate statecraft

Crate statecraft 

Source
Expand description

§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:

  1. Run-time checks can incur a performance penalty
  2. 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:

§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.

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>> {
        // Always fail for test sake
        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 }
    }
}
fn main() {
    let alpha = Machine { state: Alpha };
    let beta: Machine<Beta> = alpha.transition();

    // Try the transition from Beta->Gamma and expect an error
    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 {
        // We can do some async stuff here
        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>> {
        // We can do some fallible async stuff here
        Ok(Machine { state: Gamma })
    }
}

impl Transitionable<Alpha> for Machine<Gamma> {
    type NextStateful = Machine<Alpha>;

    fn transition(self) -> Self::NextStateful {
        // No async stuff here because we are using Transitionable
        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.

Macros§

return_recover
Return the required Recovered with a provided error message
return_recover_async
Async variant of return_recover. This must be instantiated within an async block.
transition
Explicitly transition a Transitionable Stateful to an optionally provided state. This is useful when there are more than one valid transitions for a Stateful.
transition_async
Async variant of transition. This must be instantiated within an async block.
try_recover
Try some operation and return the required Recovered if the operation fails
try_recover_async
Async variant of try_recover. This must be instantiated within an async block.
try_transition
Explicitly try to transition a TryTransitionable Stateful to an optionally provided state with an optionally provided recovery state. This is useful when there are more than one valid transitions for a Stateful.
try_transition_async
Async variant of try_transition. This must be instantiated within an async block.
try_transition_continue
Try some operation and on failure, assign a given existing binding to the recovered Stateful, then continue
try_transition_continue_async
Async variant of try_transition_continue. This must be instantiated within an async block.
try_transition_inner_recover
Attempt to transition a TryTransitionable within another TryTransitionable machine, recovering the outer state machine if the inner transition fails
try_transition_inner_recover_async
Async variant of try_transition_inner_recover. This must be instantiated within an async block.

Structs§

Recovered
The error value of a failed TryTransitionable
TransitionError
Message-only error produced by the return_recover! macro family

Traits§

AsyncTransitionable
Async variant of Transitionable
AsyncTryTransitionable
Async variant of TryTransitionable
Bailable
Convenience trait to convert Result<S, Recovered<F, E>> into the error E
SelfTransitionable
Statefuls which can transition back to themselves
State
A state which a Stateful can be in
Stateful
Types which can be in one of any given number of States at a given time
Transitionable
Statefuls which can transition between different States
TryTransitionable
Statefuls which can transition between different States, but may fail when attempting to do so

Derive Macros§

SelfTransitionable
Derive for SelfTransitionable
State
Derive for State trait
Stateful
Derive for Stateful trait