1use std::collections::HashMap;
2
3use chrono::Utc;
4use uuid::Uuid;
5
6use crate::types::{
7 State, StateMachine, Transition, TransitionRecord,
8 WorkflowError, WorkflowResult,
9};
10
11pub struct FsmEngine {
13 machines: HashMap<String, StateMachine>,
14 history: HashMap<String, Vec<TransitionRecord>>,
15}
16
17impl FsmEngine {
18 pub fn new() -> Self {
19 Self {
20 machines: HashMap::new(),
21 history: HashMap::new(),
22 }
23 }
24
25 pub fn create_fsm(
27 &mut self,
28 name: &str,
29 states: Vec<State>,
30 transitions: Vec<Transition>,
31 initial_state: &str,
32 ) -> WorkflowResult<String> {
33 if !states.iter().any(|s| s.name == initial_state) {
35 return Err(WorkflowError::InvalidTransition {
36 from: "".to_string(),
37 to: initial_state.to_string(),
38 });
39 }
40
41 let id = Uuid::new_v4().to_string();
42 let now = Utc::now();
43
44 let fsm = StateMachine {
45 id: id.clone(),
46 name: name.to_string(),
47 states,
48 transitions,
49 initial_state: initial_state.to_string(),
50 current_state: initial_state.to_string(),
51 context: HashMap::new(),
52 created_at: now,
53 updated_at: now,
54 };
55
56 self.machines.insert(id.clone(), fsm);
57 self.history.insert(id.clone(), Vec::new());
58 Ok(id)
59 }
60
61 pub fn transition(
63 &mut self,
64 fsm_id: &str,
65 event: &str,
66 ) -> WorkflowResult<String> {
67 let fsm = self
68 .machines
69 .get_mut(fsm_id)
70 .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
71
72 let current = fsm.current_state.clone();
73
74 let transition = fsm
76 .transitions
77 .iter()
78 .find(|t| t.from == current && t.event == event)
79 .ok_or_else(|| WorkflowError::InvalidTransition {
80 from: current.clone(),
81 to: format!("(event: {})", event),
82 })?
83 .clone();
84
85 let to_state = transition.to.clone();
86
87 let record = TransitionRecord {
89 fsm_id: fsm_id.to_string(),
90 from_state: current.clone(),
91 to_state: to_state.clone(),
92 event: event.to_string(),
93 timestamp: Utc::now(),
94 context_snapshot: fsm.context.clone(),
95 };
96
97 fsm.current_state = to_state.clone();
99 fsm.updated_at = Utc::now();
100
101 self.history
102 .entry(fsm_id.to_string())
103 .or_default()
104 .push(record);
105
106 Ok(to_state)
107 }
108
109 pub fn current_state(&self, fsm_id: &str) -> WorkflowResult<&str> {
111 let fsm = self
112 .machines
113 .get(fsm_id)
114 .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
115
116 Ok(&fsm.current_state)
117 }
118
119 pub fn valid_next(&self, fsm_id: &str) -> WorkflowResult<Vec<&Transition>> {
121 let fsm = self
122 .machines
123 .get(fsm_id)
124 .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
125
126 Ok(fsm.valid_transitions())
127 }
128
129 pub fn get_history(&self, fsm_id: &str) -> WorkflowResult<&[TransitionRecord]> {
131 self.history
132 .get(fsm_id)
133 .map(|v| v.as_slice())
134 .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))
135 }
136
137 pub fn get_fsm(&self, fsm_id: &str) -> WorkflowResult<&StateMachine> {
139 self.machines
140 .get(fsm_id)
141 .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))
142 }
143
144 pub fn diagram(&self, fsm_id: &str) -> WorkflowResult<String> {
146 let fsm = self.get_fsm(fsm_id)?;
147 let mut lines = vec!["stateDiagram-v2".to_string()];
148
149 lines.push(format!(" [*] --> {}", fsm.initial_state));
150
151 for t in &fsm.transitions {
152 lines.push(format!(" {} --> {} : {}", t.from, t.to, t.event));
153 }
154
155 for state in &fsm.states {
156 if state.is_terminal {
157 lines.push(format!(" {} --> [*]", state.name));
158 }
159 }
160
161 Ok(lines.join("\n"))
162 }
163}
164
165impl Default for FsmEngine {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn order_states() -> (Vec<State>, Vec<Transition>) {
176 let states = vec![
177 State { name: "Created".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
178 State { name: "Paid".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
179 State { name: "Shipped".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
180 State { name: "Delivered".into(), description: None, entry_action: None, exit_action: None, is_terminal: true },
181 ];
182
183 let transitions = vec![
184 Transition { from: "Created".into(), to: "Paid".into(), event: "pay".into(), guard: None, action: None },
185 Transition { from: "Paid".into(), to: "Shipped".into(), event: "ship".into(), guard: None, action: None },
186 Transition { from: "Shipped".into(), to: "Delivered".into(), event: "deliver".into(), guard: None, action: None },
187 ];
188
189 (states, transitions)
190 }
191
192 #[test]
193 fn test_fsm_transitions() {
194 let mut engine = FsmEngine::new();
195 let (states, transitions) = order_states();
196 let fid = engine.create_fsm("order", states, transitions, "Created").unwrap();
197
198 assert_eq!(engine.current_state(&fid).unwrap(), "Created");
199 engine.transition(&fid, "pay").unwrap();
200 assert_eq!(engine.current_state(&fid).unwrap(), "Paid");
201 engine.transition(&fid, "ship").unwrap();
202 assert_eq!(engine.current_state(&fid).unwrap(), "Shipped");
203
204 assert!(engine.transition(&fid, "pay").is_err());
206 }
207}