ironflow_engine/fsm/
step_fsm.rs1use std::fmt;
4
5use chrono::Utc;
6use ironflow_store::entities::StepStatus;
7use serde::{Deserialize, Serialize};
8
9use super::{Transition, TransitionError};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum StepEvent {
24 Started,
26 Succeeded,
28 Failed,
30 Skipped,
32 Suspended,
34 Resumed,
36 Rejected,
38}
39
40impl fmt::Display for StepEvent {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 StepEvent::Started => f.write_str("started"),
44 StepEvent::Succeeded => f.write_str("succeeded"),
45 StepEvent::Failed => f.write_str("failed"),
46 StepEvent::Skipped => f.write_str("skipped"),
47 StepEvent::Suspended => f.write_str("suspended"),
48 StepEvent::Resumed => f.write_str("resumed"),
49 StepEvent::Rejected => f.write_str("rejected"),
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
81pub struct StepFsm {
82 state: StepStatus,
83 history: Vec<Transition<StepStatus, StepEvent>>,
84}
85
86impl StepFsm {
87 pub fn new() -> Self {
99 Self {
100 state: StepStatus::Pending,
101 history: Vec::new(),
102 }
103 }
104
105 pub fn from_state(state: StepStatus) -> Self {
107 Self {
108 state,
109 history: Vec::new(),
110 }
111 }
112
113 pub fn state(&self) -> StepStatus {
115 self.state
116 }
117
118 pub fn history(&self) -> &[Transition<StepStatus, StepEvent>] {
120 &self.history
121 }
122
123 pub fn is_terminal(&self) -> bool {
125 self.state.is_terminal()
126 }
127
128 pub fn apply(
144 &mut self,
145 event: StepEvent,
146 ) -> Result<StepStatus, TransitionError<StepStatus, StepEvent>> {
147 let next = next_state(self.state, event).ok_or(TransitionError {
148 from: self.state,
149 event,
150 })?;
151
152 let transition = Transition {
153 from: self.state,
154 to: next,
155 event,
156 at: Utc::now(),
157 };
158
159 self.history.push(transition);
160 self.state = next;
161 Ok(next)
162 }
163
164 pub fn can_apply(&self, event: StepEvent) -> bool {
166 next_state(self.state, event).is_some()
167 }
168}
169
170impl Default for StepFsm {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176fn next_state(from: StepStatus, event: StepEvent) -> Option<StepStatus> {
177 match (from, event) {
178 (StepStatus::Pending, StepEvent::Started) => Some(StepStatus::Running),
179 (StepStatus::Pending, StepEvent::Skipped) => Some(StepStatus::Skipped),
180 (StepStatus::Running, StepEvent::Succeeded) => Some(StepStatus::Completed),
181 (StepStatus::Running, StepEvent::Failed) => Some(StepStatus::Failed),
182 (StepStatus::Running, StepEvent::Suspended) => Some(StepStatus::AwaitingApproval),
183 (StepStatus::AwaitingApproval, StepEvent::Resumed) => Some(StepStatus::Running),
184 (StepStatus::AwaitingApproval, StepEvent::Rejected) => Some(StepStatus::Rejected),
185 (StepStatus::AwaitingApproval, StepEvent::Failed) => Some(StepStatus::Failed),
186 _ => None,
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn success_path() {
196 let mut fsm = StepFsm::new();
197 fsm.apply(StepEvent::Started).unwrap();
198 fsm.apply(StepEvent::Succeeded).unwrap();
199 assert_eq!(fsm.state(), StepStatus::Completed);
200 assert!(fsm.is_terminal());
201 assert_eq!(fsm.history().len(), 2);
202 }
203
204 #[test]
205 fn failure_path() {
206 let mut fsm = StepFsm::new();
207 fsm.apply(StepEvent::Started).unwrap();
208 fsm.apply(StepEvent::Failed).unwrap();
209 assert_eq!(fsm.state(), StepStatus::Failed);
210 assert!(fsm.is_terminal());
211 }
212
213 #[test]
214 fn skip_path() {
215 let mut fsm = StepFsm::new();
216 fsm.apply(StepEvent::Skipped).unwrap();
217 assert_eq!(fsm.state(), StepStatus::Skipped);
218 assert!(fsm.is_terminal());
219 }
220
221 #[test]
222 fn cannot_start_twice() {
223 let mut fsm = StepFsm::new();
224 fsm.apply(StepEvent::Started).unwrap();
225 assert!(fsm.apply(StepEvent::Started).is_err());
226 }
227
228 #[test]
229 fn cannot_succeed_from_pending() {
230 let mut fsm = StepFsm::new();
231 assert!(fsm.apply(StepEvent::Succeeded).is_err());
232 }
233
234 #[test]
235 fn cannot_transition_from_terminal() {
236 let mut fsm = StepFsm::new();
237 fsm.apply(StepEvent::Started).unwrap();
238 fsm.apply(StepEvent::Succeeded).unwrap();
239 assert!(fsm.apply(StepEvent::Started).is_err());
240 assert!(fsm.apply(StepEvent::Failed).is_err());
241 }
242
243 #[test]
244 fn can_apply_without_mutation() {
245 let fsm = StepFsm::new();
246 assert!(fsm.can_apply(StepEvent::Started));
247 assert!(fsm.can_apply(StepEvent::Skipped));
248 assert!(!fsm.can_apply(StepEvent::Succeeded));
249 assert!(!fsm.can_apply(StepEvent::Failed));
250 }
251
252 #[test]
253 fn from_state_resumes() {
254 let mut fsm = StepFsm::from_state(StepStatus::Running);
255 assert!(fsm.history().is_empty());
256 fsm.apply(StepEvent::Failed).unwrap();
257 assert_eq!(fsm.state(), StepStatus::Failed);
258 }
259
260 #[test]
261 fn history_records_all_transitions() {
262 let mut fsm = StepFsm::new();
263 fsm.apply(StepEvent::Started).unwrap();
264 fsm.apply(StepEvent::Succeeded).unwrap();
265
266 let h = fsm.history();
267 assert_eq!(h[0].from, StepStatus::Pending);
268 assert_eq!(h[0].to, StepStatus::Running);
269 assert_eq!(h[0].event, StepEvent::Started);
270 assert_eq!(h[1].from, StepStatus::Running);
271 assert_eq!(h[1].to, StepStatus::Completed);
272 assert_eq!(h[1].event, StepEvent::Succeeded);
273 }
274
275 #[test]
276 fn approval_suspend_and_resume_path() {
277 let mut fsm = StepFsm::new();
278 fsm.apply(StepEvent::Started).unwrap();
279 fsm.apply(StepEvent::Suspended).unwrap();
280 assert_eq!(fsm.state(), StepStatus::AwaitingApproval);
281 assert!(!fsm.is_terminal());
282
283 fsm.apply(StepEvent::Resumed).unwrap();
284 assert_eq!(fsm.state(), StepStatus::Running);
285
286 fsm.apply(StepEvent::Succeeded).unwrap();
287 assert_eq!(fsm.state(), StepStatus::Completed);
288 assert!(fsm.is_terminal());
289 }
290
291 #[test]
292 fn approval_reject_path() {
293 let mut fsm = StepFsm::new();
294 fsm.apply(StepEvent::Started).unwrap();
295 fsm.apply(StepEvent::Suspended).unwrap();
296 assert_eq!(fsm.state(), StepStatus::AwaitingApproval);
297
298 fsm.apply(StepEvent::Rejected).unwrap();
299 assert_eq!(fsm.state(), StepStatus::Rejected);
300 assert!(fsm.is_terminal());
301 }
302
303 #[test]
304 fn cannot_suspend_from_pending() {
305 let mut fsm = StepFsm::new();
306 assert!(fsm.apply(StepEvent::Suspended).is_err());
307 }
308
309 #[test]
310 fn cannot_resume_from_running() {
311 let mut fsm = StepFsm::new();
312 fsm.apply(StepEvent::Started).unwrap();
313 assert!(fsm.apply(StepEvent::Resumed).is_err());
314 }
315}