step_machine/
lib.rs

1//! Run your CLI programs as state machines with persistence and recovery abilities. When such a
2//! program breaks you'll have opportunity to change the external world (create a missing folder,
3//! change a file permissions or something) and continue the program from the step it was
4//! interrupted on.
5//!
6//! # Usage
7//!
8//! Let's toss two coins and make sure they both landed on the same side. We express the behaviour
9//! as two states of our machine. Step logic is implemented in `State::next()` methods which
10//! return the next state or `None` for the last step (the full code is in `examples/coin.rs`).
11//! ```rust
12//! #[derive(Debug, Serialize, Deserialize, From)]
13//! enum Machine {
14//!     FirstToss(FirstToss),
15//!     SecondToss(SecondToss),
16//! }
17//!
18//! #[derive(Debug, Serialize, Deserialize)]
19//! struct FirstToss;
20//! impl FirstToss {
21//!     fn next(self) -> StepResult {
22//!         let first_coin = Coin::toss();
23//!         println!("First coin: {:?}", first_coin);
24//!         Ok(Some(SecondToss { first_coin }.into()))
25//!     }
26//! }
27//!
28//! #[derive(Debug, Serialize, Deserialize)]
29//! struct SecondToss {
30//!     first_coin: Coin,
31//! }
32//! impl SecondToss {
33//!     fn next(self) -> StepResult {
34//!         let second_coin = Coin::toss();
35//!         println!("Second coin: {:?}", second_coin);
36//!         ensure!(second_coin == self.first_coin, "Coins landed differently");
37//!         println!("Coins match");
38//!         Ok(None)
39//!     }
40//! }
41//! ```
42//!
43//! Then we start our machine like this:
44//! ```rust
45//! let init_state = FirstToss.into();
46//! let mut engine = Engine::<Machine>::new(init_state)?.restore()?;
47//! engine.drop_error()?;
48//! engine.run()?;
49//! ```
50//! We initialize the `Engine` with the first step. Then we restore the previous state if the
51//! process was interrupted (e.g. by an error). Then we drop a possible error and run all the steps
52//! to completion.
53//!
54//! Let's run it now:
55//! ```sh
56//! $ cargo run --example coin
57//! First coin: Heads
58//! Second coin: Tails
59//! Error: Coins landed differently
60//! ```
61//!
62//! We weren't lucky this time and the program resulted in an error. Let's run it again:
63//! ```sh
64//! $ cargo run --example coin
65//! Second coin: Heads
66//! Coins match
67//! ```
68//!
69//! Notice that, thanks to the `restore()`, our machine run from the step it was interrupted,
70//! knowing about the first coin landed on heads.
71use serde::{de::DeserializeOwned, Deserialize, Serialize};
72use std::fmt;
73use std::io;
74use std::path::PathBuf;
75use store::Store;
76
77mod store;
78
79type StdResult<T, E> = std::result::Result<T, E>;
80
81#[derive(thiserror::Error, Debug)]
82pub enum Error {
83    #[error("IO error: {0}")]
84    IO(#[from] io::Error),
85    #[error("Serde error: {0}")]
86    Serde(#[from] serde_json::Error),
87    #[error("{0}")]
88    Step(String),
89}
90
91pub type Result<T> = StdResult<T, Error>;
92
93/// Represents state of a state machine M
94pub trait State<M: State<M>> {
95    type Error: fmt::Debug;
96
97    /// Runs the current step and returns the next machine state or `None` if everything is done
98    fn next(self) -> StdResult<Option<M>, Self::Error>;
99}
100
101/// Machine state with metadata to store
102#[derive(Debug, Serialize, Deserialize, Default)]
103pub struct Step<M> {
104    /// Current state of the machine
105    pub state: M,
106    /// An error if any
107    pub error: Option<String>,
108}
109
110impl<M> Step<M> {
111    fn new(state: M) -> Self {
112        Self { state, error: None }
113    }
114}
115
116#[derive(Debug)]
117pub struct Engine<M> {
118    store: Store,
119    step: Step<M>,
120}
121
122impl<M> Engine<M>
123where
124    M: fmt::Debug + Serialize + DeserializeOwned + State<M>,
125{
126    /// Creates an Engine using initial state
127    pub fn new(state: M) -> Result<Self> {
128        let store: Store = Store::new()?;
129        let step = Step::new(state);
130        Ok(Self { store, step })
131    }
132
133    /// Use another store path
134    pub fn with_store_path(mut self, path: impl Into<PathBuf>) -> Self {
135        let path = path.into();
136        self.store.path = path;
137        self
138    }
139
140    /// Restores an Engine from the previous run
141    pub fn restore(mut self) -> Result<Self> {
142        if let Some(step) = self.store.load()? {
143            self.step = step;
144        }
145        Ok(self)
146    }
147
148    /// Runs all steps to completion
149    pub fn run(mut self) -> Result<()> {
150        if let Some(e) = self.step.error.as_ref() {
151            return Err(crate::Error::Step(format!(
152                "Previous run resulted in an error: {} on step: {:?}",
153                e, self.step.state
154            )));
155        }
156
157        loop {
158            log::info!("Running step: {:?}", &self.step.state);
159            let state_backup = serde_json::to_string(&self.step.state)?;
160            match self.step.state.next() {
161                Ok(state) => {
162                    if let Some(state) = state {
163                        self.step = Step::new(state); // TODO
164                        self.save()?;
165                    } else {
166                        log::info!("Finished successfully");
167                        self.store.clean()?;
168                        break;
169                    }
170                }
171                Err(e) => {
172                    self.step.state = serde_json::from_str(&state_backup)?;
173                    let err_str = format!("{:?}", e);
174                    self.step.error = Some(err_str.clone());
175                    self.save()?;
176                    return Err(crate::Error::Step(err_str));
177                }
178            }
179        }
180        Ok(())
181    }
182
183    /// Drops the previous error
184    pub fn drop_error(&mut self) -> Result<()> {
185        self.step.error = None;
186        self.save()
187    }
188
189    fn save(&self) -> Result<()> {
190        self.store.save(&self.step)?;
191        Ok(())
192    }
193}