ironflow_engine/fsm/
step_fsm.rs1use chrono::Utc;
4use ironflow_store::entities::StepStatus;
5use serde::{Deserialize, Serialize};
6use strum::Display;
7
8use super::{Transition, TransitionError};
9
10#[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 Started,
26 Succeeded,
28 Failed,
30 Skipped,
32 Suspended,
34 Resumed,
36 Rejected,
38}
39
40#[derive(Debug, Clone)]
67pub struct StepFsm {
68 state: StepStatus,
69 history: Vec<Transition<StepStatus, StepEvent>>,
70}
71
72impl StepFsm {
73 pub fn new() -> Self {
85 Self {
86 state: StepStatus::Pending,
87 history: Vec::new(),
88 }
89 }
90
91 pub fn from_state(state: StepStatus) -> Self {
93 Self {
94 state,
95 history: Vec::new(),
96 }
97 }
98
99 pub fn state(&self) -> StepStatus {
101 self.state
102 }
103
104 pub fn history(&self) -> &[Transition<StepStatus, StepEvent>] {
106 &self.history
107 }
108
109 pub fn is_terminal(&self) -> bool {
111 self.state.is_terminal()
112 }
113
114 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 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}