ironflow_engine/fsm/
step_fsm.rs1use chrono::Utc;
4use ironflow_store::entities::StepStatus;
5use serde::{Deserialize, Serialize};
6
7use super::{Transition, TransitionError};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum StepEvent {
22 Started,
24 Succeeded,
26 Failed,
28 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#[derive(Debug, Clone)]
66pub struct StepFsm {
67 state: StepStatus,
68 history: Vec<Transition<StepStatus, StepEvent>>,
69}
70
71impl StepFsm {
72 pub fn new() -> Self {
84 Self {
85 state: StepStatus::Pending,
86 history: Vec::new(),
87 }
88 }
89
90 pub fn from_state(state: StepStatus) -> Self {
92 Self {
93 state,
94 history: Vec::new(),
95 }
96 }
97
98 pub fn state(&self) -> StepStatus {
100 self.state
101 }
102
103 pub fn history(&self) -> &[Transition<StepStatus, StepEvent>] {
105 &self.history
106 }
107
108 pub fn is_terminal(&self) -> bool {
110 self.state.is_terminal()
111 }
112
113 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 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}