Skip to main content

ironflow_engine/fsm/
step_fsm.rs

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