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}