tvl-depot/users/tazjin/finito/finito-core/src/lib.rs
Vincent Ambo 0d0b43ed88 fix(users/tazjin): rustfmt code with non-default settings
rustfmt only sometimes detects path-based nested config
files (probably some kind of race?), so my users folder uses a
separate formatting check for rustfmt to avoid flaky CI. Enough flakes
around already ...

Change-Id: Ifd862f9974f071b3a256643dd8e56c019116156a
Reviewed-on: https://cl.tvl.fyi/c/depot/+/5242
Reviewed-by: tazjin <tazjin@tvl.su>
Autosubmit: tazjin <tazjin@tvl.su>
Tested-by: BuildkiteCI
2022-02-07 16:58:59 +00:00

248 lines
9 KiB
Rust

//! 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::de::DeserializeOwned;
use serde::Serialize;
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(action: Self::Action, state: &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;
}