use std::collections::HashMap;
use chrono::Utc;
use uuid::Uuid;
use crate::types::{
State, StateMachine, Transition, TransitionRecord,
WorkflowError, WorkflowResult,
};
pub struct FsmEngine {
machines: HashMap<String, StateMachine>,
history: HashMap<String, Vec<TransitionRecord>>,
}
impl FsmEngine {
pub fn new() -> Self {
Self {
machines: HashMap::new(),
history: HashMap::new(),
}
}
pub fn create_fsm(
&mut self,
name: &str,
states: Vec<State>,
transitions: Vec<Transition>,
initial_state: &str,
) -> WorkflowResult<String> {
if !states.iter().any(|s| s.name == initial_state) {
return Err(WorkflowError::InvalidTransition {
from: "".to_string(),
to: initial_state.to_string(),
});
}
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let fsm = StateMachine {
id: id.clone(),
name: name.to_string(),
states,
transitions,
initial_state: initial_state.to_string(),
current_state: initial_state.to_string(),
context: HashMap::new(),
created_at: now,
updated_at: now,
};
self.machines.insert(id.clone(), fsm);
self.history.insert(id.clone(), Vec::new());
Ok(id)
}
pub fn transition(
&mut self,
fsm_id: &str,
event: &str,
) -> WorkflowResult<String> {
let fsm = self
.machines
.get_mut(fsm_id)
.ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
let current = fsm.current_state.clone();
let transition = fsm
.transitions
.iter()
.find(|t| t.from == current && t.event == event)
.ok_or_else(|| WorkflowError::InvalidTransition {
from: current.clone(),
to: format!("(event: {})", event),
})?
.clone();
let to_state = transition.to.clone();
let record = TransitionRecord {
fsm_id: fsm_id.to_string(),
from_state: current.clone(),
to_state: to_state.clone(),
event: event.to_string(),
timestamp: Utc::now(),
context_snapshot: fsm.context.clone(),
};
fsm.current_state = to_state.clone();
fsm.updated_at = Utc::now();
self.history
.entry(fsm_id.to_string())
.or_default()
.push(record);
Ok(to_state)
}
pub fn current_state(&self, fsm_id: &str) -> WorkflowResult<&str> {
let fsm = self
.machines
.get(fsm_id)
.ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
Ok(&fsm.current_state)
}
pub fn valid_next(&self, fsm_id: &str) -> WorkflowResult<Vec<&Transition>> {
let fsm = self
.machines
.get(fsm_id)
.ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
Ok(fsm.valid_transitions())
}
pub fn get_history(&self, fsm_id: &str) -> WorkflowResult<&[TransitionRecord]> {
self.history
.get(fsm_id)
.map(|v| v.as_slice())
.ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))
}
pub fn get_fsm(&self, fsm_id: &str) -> WorkflowResult<&StateMachine> {
self.machines
.get(fsm_id)
.ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))
}
pub fn diagram(&self, fsm_id: &str) -> WorkflowResult<String> {
let fsm = self.get_fsm(fsm_id)?;
let mut lines = vec!["stateDiagram-v2".to_string()];
lines.push(format!(" [*] --> {}", fsm.initial_state));
for t in &fsm.transitions {
lines.push(format!(" {} --> {} : {}", t.from, t.to, t.event));
}
for state in &fsm.states {
if state.is_terminal {
lines.push(format!(" {} --> [*]", state.name));
}
}
Ok(lines.join("\n"))
}
}
impl Default for FsmEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn order_states() -> (Vec<State>, Vec<Transition>) {
let states = vec![
State { name: "Created".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
State { name: "Paid".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
State { name: "Shipped".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
State { name: "Delivered".into(), description: None, entry_action: None, exit_action: None, is_terminal: true },
];
let transitions = vec![
Transition { from: "Created".into(), to: "Paid".into(), event: "pay".into(), guard: None, action: None },
Transition { from: "Paid".into(), to: "Shipped".into(), event: "ship".into(), guard: None, action: None },
Transition { from: "Shipped".into(), to: "Delivered".into(), event: "deliver".into(), guard: None, action: None },
];
(states, transitions)
}
#[test]
fn test_fsm_transitions() {
let mut engine = FsmEngine::new();
let (states, transitions) = order_states();
let fid = engine.create_fsm("order", states, transitions, "Created").unwrap();
assert_eq!(engine.current_state(&fid).unwrap(), "Created");
engine.transition(&fid, "pay").unwrap();
assert_eq!(engine.current_state(&fid).unwrap(), "Paid");
engine.transition(&fid, "ship").unwrap();
assert_eq!(engine.current_state(&fid).unwrap(), "Shipped");
assert!(engine.transition(&fid, "pay").is_err());
}
}