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