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}
33
34impl fmt::Display for StepEvent {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            StepEvent::Started => f.write_str("started"),
38            StepEvent::Succeeded => f.write_str("succeeded"),
39            StepEvent::Failed => f.write_str("failed"),
40            StepEvent::Skipped => f.write_str("skipped"),
41        }
42    }
43}
44
45/// Finite state machine for a workflow step.
46///
47/// # Transition table
48///
49/// | From | Event | To |
50/// |------|-------|----|
51/// | Pending | Started | Running |
52/// | Pending | Skipped | Skipped |
53/// | Running | Succeeded | Completed |
54/// | Running | Failed | Failed |
55///
56/// # Examples
57///
58/// ```
59/// use ironflow_engine::fsm::{StepFsm, StepEvent};
60/// use ironflow_store::entities::StepStatus;
61///
62/// let mut fsm = StepFsm::new();
63/// fsm.apply(StepEvent::Started).unwrap();
64/// fsm.apply(StepEvent::Succeeded).unwrap();
65/// assert_eq!(fsm.state(), StepStatus::Completed);
66/// ```
67#[derive(Debug, Clone)]
68pub struct StepFsm {
69    state: StepStatus,
70    history: Vec<Transition<StepStatus, StepEvent>>,
71}
72
73impl StepFsm {
74    /// Create a new FSM in `Pending` state.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use ironflow_engine::fsm::StepFsm;
80    /// use ironflow_store::entities::StepStatus;
81    ///
82    /// let fsm = StepFsm::new();
83    /// assert_eq!(fsm.state(), StepStatus::Pending);
84    /// ```
85    pub fn new() -> Self {
86        Self {
87            state: StepStatus::Pending,
88            history: Vec::new(),
89        }
90    }
91
92    /// Create a FSM from an existing state.
93    pub fn from_state(state: StepStatus) -> Self {
94        Self {
95            state,
96            history: Vec::new(),
97        }
98    }
99
100    /// Returns the current state.
101    pub fn state(&self) -> StepStatus {
102        self.state
103    }
104
105    /// Returns the full transition history.
106    pub fn history(&self) -> &[Transition<StepStatus, StepEvent>] {
107        &self.history
108    }
109
110    /// Returns `true` if the FSM is in a terminal state.
111    pub fn is_terminal(&self) -> bool {
112        self.state.is_terminal()
113    }
114
115    /// Apply an event, transitioning to a new state if valid.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`TransitionError`] if the event is not allowed in the current state.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use ironflow_engine::fsm::{StepFsm, StepEvent};
125    ///
126    /// let mut fsm = StepFsm::new();
127    /// assert!(fsm.apply(StepEvent::Started).is_ok());
128    /// assert!(fsm.apply(StepEvent::Started).is_err()); // can't start twice
129    /// ```
130    pub fn apply(
131        &mut self,
132        event: StepEvent,
133    ) -> Result<StepStatus, TransitionError<StepStatus, StepEvent>> {
134        let next = next_state(self.state, event).ok_or(TransitionError {
135            from: self.state,
136            event,
137        })?;
138
139        let transition = Transition {
140            from: self.state,
141            to: next,
142            event,
143            at: Utc::now(),
144        };
145
146        self.history.push(transition);
147        self.state = next;
148        Ok(next)
149    }
150
151    /// Check if an event would be accepted without applying it.
152    pub fn can_apply(&self, event: StepEvent) -> bool {
153        next_state(self.state, event).is_some()
154    }
155}
156
157impl Default for StepFsm {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163fn next_state(from: StepStatus, event: StepEvent) -> Option<StepStatus> {
164    match (from, event) {
165        (StepStatus::Pending, StepEvent::Started) => Some(StepStatus::Running),
166        (StepStatus::Pending, StepEvent::Skipped) => Some(StepStatus::Skipped),
167        (StepStatus::Running, StepEvent::Succeeded) => Some(StepStatus::Completed),
168        (StepStatus::Running, StepEvent::Failed) => Some(StepStatus::Failed),
169        _ => None,
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn success_path() {
179        let mut fsm = StepFsm::new();
180        fsm.apply(StepEvent::Started).unwrap();
181        fsm.apply(StepEvent::Succeeded).unwrap();
182        assert_eq!(fsm.state(), StepStatus::Completed);
183        assert!(fsm.is_terminal());
184        assert_eq!(fsm.history().len(), 2);
185    }
186
187    #[test]
188    fn failure_path() {
189        let mut fsm = StepFsm::new();
190        fsm.apply(StepEvent::Started).unwrap();
191        fsm.apply(StepEvent::Failed).unwrap();
192        assert_eq!(fsm.state(), StepStatus::Failed);
193        assert!(fsm.is_terminal());
194    }
195
196    #[test]
197    fn skip_path() {
198        let mut fsm = StepFsm::new();
199        fsm.apply(StepEvent::Skipped).unwrap();
200        assert_eq!(fsm.state(), StepStatus::Skipped);
201        assert!(fsm.is_terminal());
202    }
203
204    #[test]
205    fn cannot_start_twice() {
206        let mut fsm = StepFsm::new();
207        fsm.apply(StepEvent::Started).unwrap();
208        assert!(fsm.apply(StepEvent::Started).is_err());
209    }
210
211    #[test]
212    fn cannot_succeed_from_pending() {
213        let mut fsm = StepFsm::new();
214        assert!(fsm.apply(StepEvent::Succeeded).is_err());
215    }
216
217    #[test]
218    fn cannot_transition_from_terminal() {
219        let mut fsm = StepFsm::new();
220        fsm.apply(StepEvent::Started).unwrap();
221        fsm.apply(StepEvent::Succeeded).unwrap();
222        assert!(fsm.apply(StepEvent::Started).is_err());
223        assert!(fsm.apply(StepEvent::Failed).is_err());
224    }
225
226    #[test]
227    fn can_apply_without_mutation() {
228        let fsm = StepFsm::new();
229        assert!(fsm.can_apply(StepEvent::Started));
230        assert!(fsm.can_apply(StepEvent::Skipped));
231        assert!(!fsm.can_apply(StepEvent::Succeeded));
232        assert!(!fsm.can_apply(StepEvent::Failed));
233    }
234
235    #[test]
236    fn from_state_resumes() {
237        let mut fsm = StepFsm::from_state(StepStatus::Running);
238        assert!(fsm.history().is_empty());
239        fsm.apply(StepEvent::Failed).unwrap();
240        assert_eq!(fsm.state(), StepStatus::Failed);
241    }
242
243    #[test]
244    fn history_records_all_transitions() {
245        let mut fsm = StepFsm::new();
246        fsm.apply(StepEvent::Started).unwrap();
247        fsm.apply(StepEvent::Succeeded).unwrap();
248
249        let h = fsm.history();
250        assert_eq!(h[0].from, StepStatus::Pending);
251        assert_eq!(h[0].to, StepStatus::Running);
252        assert_eq!(h[0].event, StepEvent::Started);
253        assert_eq!(h[1].from, StepStatus::Running);
254        assert_eq!(h[1].to, StepStatus::Completed);
255        assert_eq!(h[1].event, StepEvent::Succeeded);
256    }
257}