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}
33
34impl fmt::Display for StepEvent {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 StepEvent::Started => f.write_str("started"),
38 StepEvent::Succeeded => f.write_str("succeeded"),
39 StepEvent::Failed => f.write_str("failed"),
40 StepEvent::Skipped => f.write_str("skipped"),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
68pub struct StepFsm {
69 state: StepStatus,
70 history: Vec<Transition<StepStatus, StepEvent>>,
71}
72
73impl StepFsm {
74 pub fn new() -> Self {
86 Self {
87 state: StepStatus::Pending,
88 history: Vec::new(),
89 }
90 }
91
92 pub fn from_state(state: StepStatus) -> Self {
94 Self {
95 state,
96 history: Vec::new(),
97 }
98 }
99
100 pub fn state(&self) -> StepStatus {
102 self.state
103 }
104
105 pub fn history(&self) -> &[Transition<StepStatus, StepEvent>] {
107 &self.history
108 }
109
110 pub fn is_terminal(&self) -> bool {
112 self.state.is_terminal()
113 }
114
115 pub fn apply(
131 &mut self,
132 event: StepEvent,
133 ) -> Result<StepStatus, TransitionError<StepStatus, StepEvent>> {
134 let next = next_state(self.state, event).ok_or(TransitionError {
135 from: self.state,
136 event,
137 })?;
138
139 let transition = Transition {
140 from: self.state,
141 to: next,
142 event,
143 at: Utc::now(),
144 };
145
146 self.history.push(transition);
147 self.state = next;
148 Ok(next)
149 }
150
151 pub fn can_apply(&self, event: StepEvent) -> bool {
153 next_state(self.state, event).is_some()
154 }
155}
156
157impl Default for StepFsm {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163fn next_state(from: StepStatus, event: StepEvent) -> Option<StepStatus> {
164 match (from, event) {
165 (StepStatus::Pending, StepEvent::Started) => Some(StepStatus::Running),
166 (StepStatus::Pending, StepEvent::Skipped) => Some(StepStatus::Skipped),
167 (StepStatus::Running, StepEvent::Succeeded) => Some(StepStatus::Completed),
168 (StepStatus::Running, StepEvent::Failed) => Some(StepStatus::Failed),
169 _ => None,
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn success_path() {
179 let mut fsm = StepFsm::new();
180 fsm.apply(StepEvent::Started).unwrap();
181 fsm.apply(StepEvent::Succeeded).unwrap();
182 assert_eq!(fsm.state(), StepStatus::Completed);
183 assert!(fsm.is_terminal());
184 assert_eq!(fsm.history().len(), 2);
185 }
186
187 #[test]
188 fn failure_path() {
189 let mut fsm = StepFsm::new();
190 fsm.apply(StepEvent::Started).unwrap();
191 fsm.apply(StepEvent::Failed).unwrap();
192 assert_eq!(fsm.state(), StepStatus::Failed);
193 assert!(fsm.is_terminal());
194 }
195
196 #[test]
197 fn skip_path() {
198 let mut fsm = StepFsm::new();
199 fsm.apply(StepEvent::Skipped).unwrap();
200 assert_eq!(fsm.state(), StepStatus::Skipped);
201 assert!(fsm.is_terminal());
202 }
203
204 #[test]
205 fn cannot_start_twice() {
206 let mut fsm = StepFsm::new();
207 fsm.apply(StepEvent::Started).unwrap();
208 assert!(fsm.apply(StepEvent::Started).is_err());
209 }
210
211 #[test]
212 fn cannot_succeed_from_pending() {
213 let mut fsm = StepFsm::new();
214 assert!(fsm.apply(StepEvent::Succeeded).is_err());
215 }
216
217 #[test]
218 fn cannot_transition_from_terminal() {
219 let mut fsm = StepFsm::new();
220 fsm.apply(StepEvent::Started).unwrap();
221 fsm.apply(StepEvent::Succeeded).unwrap();
222 assert!(fsm.apply(StepEvent::Started).is_err());
223 assert!(fsm.apply(StepEvent::Failed).is_err());
224 }
225
226 #[test]
227 fn can_apply_without_mutation() {
228 let fsm = StepFsm::new();
229 assert!(fsm.can_apply(StepEvent::Started));
230 assert!(fsm.can_apply(StepEvent::Skipped));
231 assert!(!fsm.can_apply(StepEvent::Succeeded));
232 assert!(!fsm.can_apply(StepEvent::Failed));
233 }
234
235 #[test]
236 fn from_state_resumes() {
237 let mut fsm = StepFsm::from_state(StepStatus::Running);
238 assert!(fsm.history().is_empty());
239 fsm.apply(StepEvent::Failed).unwrap();
240 assert_eq!(fsm.state(), StepStatus::Failed);
241 }
242
243 #[test]
244 fn history_records_all_transitions() {
245 let mut fsm = StepFsm::new();
246 fsm.apply(StepEvent::Started).unwrap();
247 fsm.apply(StepEvent::Succeeded).unwrap();
248
249 let h = fsm.history();
250 assert_eq!(h[0].from, StepStatus::Pending);
251 assert_eq!(h[0].to, StepStatus::Running);
252 assert_eq!(h[0].event, StepEvent::Started);
253 assert_eq!(h[1].from, StepStatus::Running);
254 assert_eq!(h[1].to, StepStatus::Completed);
255 assert_eq!(h[1].event, StepEvent::Succeeded);
256 }
257}