Skip to main content

ironflow_engine/fsm/
step_fsm.rs

1//! [`StepFsm`] — Finite state machine for the step lifecycle.
2
3use std::fmt;
4
5use chrono::Utc;
6use ironflow_store::entities::StepStatus;
7use serde::{Deserialize, Serialize};
8
9use super::{Transition, TransitionError};
10
11/// Events that drive [`StepFsm`] transitions.
12///
13/// # Examples
14///
15/// ```
16/// use ironflow_engine::fsm::StepEvent;
17///
18/// let event = StepEvent::Started;
19/// assert_eq!(event.to_string(), "started");
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum StepEvent {
24    /// Execution started.
25    Started,
26    /// Execution completed successfully.
27    Succeeded,
28    /// Execution failed.
29    Failed,
30    /// Step was skipped (prior step failed).
31    Skipped,
32    /// Step is suspended, waiting for human approval.
33    Suspended,
34    /// Approval received, step resumes.
35    Resumed,
36    /// Human rejected the approval.
37    Rejected,
38}
39
40impl fmt::Display for StepEvent {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            StepEvent::Started => f.write_str("started"),
44            StepEvent::Succeeded => f.write_str("succeeded"),
45            StepEvent::Failed => f.write_str("failed"),
46            StepEvent::Skipped => f.write_str("skipped"),
47            StepEvent::Suspended => f.write_str("suspended"),
48            StepEvent::Resumed => f.write_str("resumed"),
49            StepEvent::Rejected => f.write_str("rejected"),
50        }
51    }
52}
53
54/// Finite state machine for a workflow step.
55///
56/// # Transition table
57///
58/// | From | Event | To |
59/// |------|-------|----|
60/// | Pending | Started | Running |
61/// | Pending | Skipped | Skipped |
62/// | Running | Succeeded | Completed |
63/// | Running | Failed | Failed |
64/// | Running | Suspended | AwaitingApproval |
65/// | AwaitingApproval | Resumed | Running |
66/// | AwaitingApproval | Rejected | Rejected |
67/// | AwaitingApproval | Failed | Failed |
68///
69/// # Examples
70///
71/// ```
72/// use ironflow_engine::fsm::{StepFsm, StepEvent};
73/// use ironflow_store::entities::StepStatus;
74///
75/// let mut fsm = StepFsm::new();
76/// fsm.apply(StepEvent::Started).unwrap();
77/// fsm.apply(StepEvent::Succeeded).unwrap();
78/// assert_eq!(fsm.state(), StepStatus::Completed);
79/// ```
80#[derive(Debug, Clone)]
81pub struct StepFsm {
82    state: StepStatus,
83    history: Vec<Transition<StepStatus, StepEvent>>,
84}
85
86impl StepFsm {
87    /// Create a new FSM in `Pending` state.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use ironflow_engine::fsm::StepFsm;
93    /// use ironflow_store::entities::StepStatus;
94    ///
95    /// let fsm = StepFsm::new();
96    /// assert_eq!(fsm.state(), StepStatus::Pending);
97    /// ```
98    pub fn new() -> Self {
99        Self {
100            state: StepStatus::Pending,
101            history: Vec::new(),
102        }
103    }
104
105    /// Create a FSM from an existing state.
106    pub fn from_state(state: StepStatus) -> Self {
107        Self {
108            state,
109            history: Vec::new(),
110        }
111    }
112
113    /// Returns the current state.
114    pub fn state(&self) -> StepStatus {
115        self.state
116    }
117
118    /// Returns the full transition history.
119    pub fn history(&self) -> &[Transition<StepStatus, StepEvent>] {
120        &self.history
121    }
122
123    /// Returns `true` if the FSM is in a terminal state.
124    pub fn is_terminal(&self) -> bool {
125        self.state.is_terminal()
126    }
127
128    /// Apply an event, transitioning to a new state if valid.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`TransitionError`] if the event is not allowed in the current state.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use ironflow_engine::fsm::{StepFsm, StepEvent};
138    ///
139    /// let mut fsm = StepFsm::new();
140    /// assert!(fsm.apply(StepEvent::Started).is_ok());
141    /// assert!(fsm.apply(StepEvent::Started).is_err()); // can't start twice
142    /// ```
143    pub fn apply(
144        &mut self,
145        event: StepEvent,
146    ) -> Result<StepStatus, TransitionError<StepStatus, StepEvent>> {
147        let next = next_state(self.state, event).ok_or(TransitionError {
148            from: self.state,
149            event,
150        })?;
151
152        let transition = Transition {
153            from: self.state,
154            to: next,
155            event,
156            at: Utc::now(),
157        };
158
159        self.history.push(transition);
160        self.state = next;
161        Ok(next)
162    }
163
164    /// Check if an event would be accepted without applying it.
165    pub fn can_apply(&self, event: StepEvent) -> bool {
166        next_state(self.state, event).is_some()
167    }
168}
169
170impl Default for StepFsm {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176fn next_state(from: StepStatus, event: StepEvent) -> Option<StepStatus> {
177    match (from, event) {
178        (StepStatus::Pending, StepEvent::Started) => Some(StepStatus::Running),
179        (StepStatus::Pending, StepEvent::Skipped) => Some(StepStatus::Skipped),
180        (StepStatus::Running, StepEvent::Succeeded) => Some(StepStatus::Completed),
181        (StepStatus::Running, StepEvent::Failed) => Some(StepStatus::Failed),
182        (StepStatus::Running, StepEvent::Suspended) => Some(StepStatus::AwaitingApproval),
183        (StepStatus::AwaitingApproval, StepEvent::Resumed) => Some(StepStatus::Running),
184        (StepStatus::AwaitingApproval, StepEvent::Rejected) => Some(StepStatus::Rejected),
185        (StepStatus::AwaitingApproval, StepEvent::Failed) => Some(StepStatus::Failed),
186        _ => None,
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn success_path() {
196        let mut fsm = StepFsm::new();
197        fsm.apply(StepEvent::Started).unwrap();
198        fsm.apply(StepEvent::Succeeded).unwrap();
199        assert_eq!(fsm.state(), StepStatus::Completed);
200        assert!(fsm.is_terminal());
201        assert_eq!(fsm.history().len(), 2);
202    }
203
204    #[test]
205    fn failure_path() {
206        let mut fsm = StepFsm::new();
207        fsm.apply(StepEvent::Started).unwrap();
208        fsm.apply(StepEvent::Failed).unwrap();
209        assert_eq!(fsm.state(), StepStatus::Failed);
210        assert!(fsm.is_terminal());
211    }
212
213    #[test]
214    fn skip_path() {
215        let mut fsm = StepFsm::new();
216        fsm.apply(StepEvent::Skipped).unwrap();
217        assert_eq!(fsm.state(), StepStatus::Skipped);
218        assert!(fsm.is_terminal());
219    }
220
221    #[test]
222    fn cannot_start_twice() {
223        let mut fsm = StepFsm::new();
224        fsm.apply(StepEvent::Started).unwrap();
225        assert!(fsm.apply(StepEvent::Started).is_err());
226    }
227
228    #[test]
229    fn cannot_succeed_from_pending() {
230        let mut fsm = StepFsm::new();
231        assert!(fsm.apply(StepEvent::Succeeded).is_err());
232    }
233
234    #[test]
235    fn cannot_transition_from_terminal() {
236        let mut fsm = StepFsm::new();
237        fsm.apply(StepEvent::Started).unwrap();
238        fsm.apply(StepEvent::Succeeded).unwrap();
239        assert!(fsm.apply(StepEvent::Started).is_err());
240        assert!(fsm.apply(StepEvent::Failed).is_err());
241    }
242
243    #[test]
244    fn can_apply_without_mutation() {
245        let fsm = StepFsm::new();
246        assert!(fsm.can_apply(StepEvent::Started));
247        assert!(fsm.can_apply(StepEvent::Skipped));
248        assert!(!fsm.can_apply(StepEvent::Succeeded));
249        assert!(!fsm.can_apply(StepEvent::Failed));
250    }
251
252    #[test]
253    fn from_state_resumes() {
254        let mut fsm = StepFsm::from_state(StepStatus::Running);
255        assert!(fsm.history().is_empty());
256        fsm.apply(StepEvent::Failed).unwrap();
257        assert_eq!(fsm.state(), StepStatus::Failed);
258    }
259
260    #[test]
261    fn history_records_all_transitions() {
262        let mut fsm = StepFsm::new();
263        fsm.apply(StepEvent::Started).unwrap();
264        fsm.apply(StepEvent::Succeeded).unwrap();
265
266        let h = fsm.history();
267        assert_eq!(h[0].from, StepStatus::Pending);
268        assert_eq!(h[0].to, StepStatus::Running);
269        assert_eq!(h[0].event, StepEvent::Started);
270        assert_eq!(h[1].from, StepStatus::Running);
271        assert_eq!(h[1].to, StepStatus::Completed);
272        assert_eq!(h[1].event, StepEvent::Succeeded);
273    }
274
275    #[test]
276    fn approval_suspend_and_resume_path() {
277        let mut fsm = StepFsm::new();
278        fsm.apply(StepEvent::Started).unwrap();
279        fsm.apply(StepEvent::Suspended).unwrap();
280        assert_eq!(fsm.state(), StepStatus::AwaitingApproval);
281        assert!(!fsm.is_terminal());
282
283        fsm.apply(StepEvent::Resumed).unwrap();
284        assert_eq!(fsm.state(), StepStatus::Running);
285
286        fsm.apply(StepEvent::Succeeded).unwrap();
287        assert_eq!(fsm.state(), StepStatus::Completed);
288        assert!(fsm.is_terminal());
289    }
290
291    #[test]
292    fn approval_reject_path() {
293        let mut fsm = StepFsm::new();
294        fsm.apply(StepEvent::Started).unwrap();
295        fsm.apply(StepEvent::Suspended).unwrap();
296        assert_eq!(fsm.state(), StepStatus::AwaitingApproval);
297
298        fsm.apply(StepEvent::Rejected).unwrap();
299        assert_eq!(fsm.state(), StepStatus::Rejected);
300        assert!(fsm.is_terminal());
301    }
302
303    #[test]
304    fn cannot_suspend_from_pending() {
305        let mut fsm = StepFsm::new();
306        assert!(fsm.apply(StepEvent::Suspended).is_err());
307    }
308
309    #[test]
310    fn cannot_resume_from_running() {
311        let mut fsm = StepFsm::new();
312        fsm.apply(StepEvent::Started).unwrap();
313        assert!(fsm.apply(StepEvent::Resumed).is_err());
314    }
315}