feat(tazjin/finito): Check in my old Rust state-machine library
I dug through my archives for this and found a version that, while unfortunately not the latest implementation, is close enough to the real thing to show off what Finito did. This is a Postgres-backed state-machine library for complex application logic. I wrote this originally for a work purpose in a previous life, but have always wanted to apply it elsewhere, too. git-subtree-dir: users/tazjin/finito git-subtree-mainline:0380841eb1git-subtree-split:b748117225Change-Id: I0de02d6258568447a14870f1a533812a67127763
This commit is contained in:
commit
9e7b81391d
13 changed files with 1279 additions and 0 deletions
243
users/tazjin/finito/finito-core/src/lib.rs
Normal file
243
users/tazjin/finito/finito-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
//! Finito's core finite-state machine abstraction.
|
||||
//!
|
||||
//! # What & why?
|
||||
//!
|
||||
//! Most processes that occur in software applications can be modeled
|
||||
//! as finite-state machines (FSMs), however the actual states, the
|
||||
//! transitions between them and the model's interaction with the
|
||||
//! external world is often implicit.
|
||||
//!
|
||||
//! Making the states of a process explicit using a simple language
|
||||
//! that works for both software developers and other people who may
|
||||
//! have opinions on processes makes it easier to synchronise thoughts,
|
||||
//! extend software and keep a good level of control over what is going
|
||||
//! on.
|
||||
//!
|
||||
//! This library aims to provide functionality for implementing
|
||||
//! finite-state machines in a way that balances expressivity and
|
||||
//! safety.
|
||||
//!
|
||||
//! Finito does not aim to prevent every possible incorrect
|
||||
//! transition, but aims for somewhere "safe-enough" (please don't
|
||||
//! lynch me) that is still easily understood.
|
||||
//!
|
||||
//! # Conceptual overview
|
||||
//!
|
||||
//! The core idea behind Finito can be expressed in a single line and
|
||||
//! will potentially look familiar if you have used Erlang in a
|
||||
//! previous life. The syntax used here is the type-signature notation
|
||||
//! of Haskell.
|
||||
//!
|
||||
//! ```text
|
||||
//! advance :: state -> event -> (state, [action])
|
||||
//! ```
|
||||
//!
|
||||
//! In short, every FSM is made up of three distinct types:
|
||||
//!
|
||||
//! * a state type representing all possible states of the machine
|
||||
//!
|
||||
//! * an event type representing all possible events in the machine
|
||||
//!
|
||||
//! * an action type representing a description of all possible
|
||||
//! side-effects of the machine
|
||||
//!
|
||||
//! Using the definition above we can now say that a transition in a
|
||||
//! state-machine, involving these three types, takes an initial state
|
||||
//! and an event to apply it to and returns a new state and a list of
|
||||
//! actions to execute.
|
||||
//!
|
||||
//! With this definition most processes can already be modeled quite
|
||||
//! well. Two additional functions are required to make it all work:
|
||||
//!
|
||||
//! ```text
|
||||
//! -- | The ability to cause additional side-effects after entering
|
||||
//! -- a new state.
|
||||
//! > enter :: state -> [action]
|
||||
//! ```
|
||||
//!
|
||||
//! as well as
|
||||
//!
|
||||
//! ```text
|
||||
//! -- | An interpreter for side-effects
|
||||
//! act :: action -> m [event]
|
||||
//! ```
|
||||
//!
|
||||
//! **Note**: This library is based on an original Haskell library. In
|
||||
//! Haskell, side-effects can be controlled via the type system which
|
||||
//! is impossible in Rust.
|
||||
//!
|
||||
//! Some parts of Finito make assumptions about the programmer not
|
||||
//! making certain kinds of mistakes, which are pointed out in the
|
||||
//! documentation. Unfortunately those assumptions are not
|
||||
//! automatically verifiable in Rust.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! Please consult `finito-door` for an example representing a simple,
|
||||
//! lockable door as a finite-state machine. This gives an overview
|
||||
//! over Finito's primary features.
|
||||
//!
|
||||
//! If you happen to be the kind of person who likes to learn about
|
||||
//! libraries by reading code, you should familiarise yourself with the
|
||||
//! door as it shows up as the example in other finito-related
|
||||
//! libraries, too.
|
||||
//!
|
||||
//! # Persistence, side-effects and mud
|
||||
//!
|
||||
//! These three things are inescapable in the fateful realm of
|
||||
//! computers, but Finito separates them out into separate libraries
|
||||
//! that you can drag in as you need them.
|
||||
//!
|
||||
//! Currently, those libraries include:
|
||||
//!
|
||||
//! * `finito`: Core components and classes of Finito
|
||||
//!
|
||||
//! * `finito-in-mem`: In-memory implementation of state machines
|
||||
//! that do not need to live longer than an application using
|
||||
//! standard library concurrency primitives.
|
||||
//!
|
||||
//! * `finito-postgres`: Postgres-backed, persistent implementation
|
||||
//! of state machines that, well, do need to live longer. Uses
|
||||
//! Postgres for concurrency synchronisation, so keep that in
|
||||
//! mind.
|
||||
//!
|
||||
//! Which should cover most use-cases. Okay, enough prose, lets dive
|
||||
//! in.
|
||||
//!
|
||||
//! # Does Finito make you want to scream?
|
||||
//!
|
||||
//! Please reach out! I want to know why!
|
||||
|
||||
extern crate serde;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt::Debug;
|
||||
use std::mem;
|
||||
|
||||
/// Primary trait that needs to be implemented for every state type
|
||||
/// representing the states of an FSM.
|
||||
///
|
||||
/// This trait is used to implement transition logic and to "tie the
|
||||
/// room together", with the room being our triplet of types.
|
||||
pub trait FSM where Self: Sized {
|
||||
/// A human-readable string uniquely describing what this FSM
|
||||
/// models. This is used in log messages, database tables and
|
||||
/// various other things throughout Finito.
|
||||
const FSM_NAME: &'static str;
|
||||
|
||||
/// The associated event type of an FSM represents all possible
|
||||
/// events that can occur in the state-machine.
|
||||
type Event;
|
||||
|
||||
/// The associated action type of an FSM represents all possible
|
||||
/// actions that can occur in the state-machine.
|
||||
type Action;
|
||||
|
||||
/// The associated error type of an FSM represents failures that
|
||||
/// can occur during action processing.
|
||||
type Error: Debug;
|
||||
|
||||
/// The associated state type of an FSM describes the state that
|
||||
/// is made available to the implementation of action
|
||||
/// interpretations.
|
||||
type State;
|
||||
|
||||
/// `handle` deals with any incoming events to cause state
|
||||
/// transitions and emit actions. This function is the core logic
|
||||
/// of any state machine.
|
||||
///
|
||||
/// Implementations of this function **must not** cause any
|
||||
/// side-effects to avoid breaking the guarantees of Finitos
|
||||
/// conceptual model.
|
||||
fn handle(self, event: Self::Event) -> (Self, Vec<Self::Action>);
|
||||
|
||||
/// `enter` is called when a new state is entered, allowing a
|
||||
/// state to produce additional side-effects.
|
||||
///
|
||||
/// This is useful for side-effects that event handlers do not
|
||||
/// need to know about and for resting assured that a certain
|
||||
/// action has been caused when a state is entered.
|
||||
///
|
||||
/// FSM state types are expected to be enum (i.e. sum) types. A
|
||||
/// state is considered "new" and enter calls are run if is of a
|
||||
/// different enum variant.
|
||||
fn enter(&self) -> Vec<Self::Action>;
|
||||
|
||||
/// `act` interprets and executes FSM actions. This is the only
|
||||
/// part of an FSM in which side-effects are allowed.
|
||||
fn act(Self::Action, &Self::State) -> Result<Vec<Self::Event>, Self::Error>;
|
||||
}
|
||||
|
||||
/// This function is the primary function used to advance a state
|
||||
/// machine. It takes care of both running the event handler as well
|
||||
/// as possible state-enter calls and returning the result.
|
||||
///
|
||||
/// Users of Finito should basically always use this function when
|
||||
/// advancing state-machines manually, and never call FSM-trait
|
||||
/// methods directly.
|
||||
pub fn advance<S: FSM>(state: S, event: S::Event) -> (S, Vec<S::Action>) {
|
||||
// Determine the enum variant of the initial state (used to
|
||||
// trigger enter calls).
|
||||
let old_discriminant = mem::discriminant(&state);
|
||||
|
||||
let (new_state, mut actions) = state.handle(event);
|
||||
|
||||
// Compare the enum variant of the resulting state to the old one
|
||||
// and run `enter` if they differ.
|
||||
let new_discriminant = mem::discriminant(&new_state);
|
||||
let mut enter_actions = if old_discriminant != new_discriminant {
|
||||
new_state.enter()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
actions.append(&mut enter_actions);
|
||||
|
||||
(new_state, actions)
|
||||
}
|
||||
|
||||
/// This trait is implemented by Finito backends. Backends are
|
||||
/// expected to be able to keep track of the current state of an FSM
|
||||
/// and retrieve it / apply updates transactionally.
|
||||
///
|
||||
/// See the `finito-postgres` and `finito-in-mem` crates for example
|
||||
/// implementations of this trait.
|
||||
///
|
||||
/// Backends must be parameterised over an additional (user-supplied)
|
||||
/// state type which can be used to track application state that must
|
||||
/// be made available to action handlers, for example to pass along
|
||||
/// database connections.
|
||||
pub trait FSMBackend<S: 'static> {
|
||||
/// Key type used to identify individual state machines in this
|
||||
/// backend.
|
||||
///
|
||||
/// TODO: Should be parameterised over FSM type after rustc
|
||||
/// #44265.
|
||||
type Key;
|
||||
|
||||
/// Error type for all potential failures that can occur when
|
||||
/// interacting with this backend.
|
||||
type Error: Debug;
|
||||
|
||||
/// Insert a new state-machine into the backend's storage and
|
||||
/// return its newly allocated key.
|
||||
fn insert_machine<F>(&self, initial: F) -> Result<Self::Key, Self::Error>
|
||||
where F: FSM + Serialize + DeserializeOwned;
|
||||
|
||||
/// Retrieve the current state of an FSM by its key.
|
||||
fn get_machine<F: FSM>(&self, key: Self::Key) -> Result<F, Self::Error>
|
||||
where F: FSM + Serialize + DeserializeOwned;
|
||||
|
||||
/// Advance a state machine by applying an event and persisting it
|
||||
/// as well as any resulting actions.
|
||||
///
|
||||
/// **Note**: Whether actions are automatically executed depends
|
||||
/// on the backend used. Please consult the backend's
|
||||
/// documentation for details.
|
||||
fn advance<'a, F: FSM>(&'a self, key: Self::Key, event: F::Event) -> Result<F, Self::Error>
|
||||
where F: FSM + Serialize + DeserializeOwned,
|
||||
F::State: From<&'a S>,
|
||||
F::Event: Serialize + DeserializeOwned,
|
||||
F::Action: Serialize + DeserializeOwned;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue