about summary refs log tree commit diff
path: root/users/tazjin/finito/finito-core/src/lib.rs
blob: aaec03a77b42b17dfd75d62b8b7119b6ae7c6255 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! 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;
}