use std::collections::HashMap;
use crate::Workflow;
pub trait StateMachineView: Workflow {
fn state_info(state: &Self::State) -> StateInfo;
fn transitions(state: &Self::State) -> Vec<TransitionInfo>;
fn state_machine() -> Option<StateMachineDefinition> {
None
}
}
#[derive(Debug, Clone)]
pub struct StateInfo {
pub status: String,
pub fields: HashMap<String, FieldValue>,
pub is_terminal: bool,
}
impl StateInfo {
pub fn new(status: impl Into<String>) -> Self {
Self {
status: status.into(),
fields: HashMap::new(),
is_terminal: false,
}
}
pub fn with_field(mut self, name: impl Into<String>, value: &impl std::fmt::Display) -> Self {
self.fields
.insert(name.into(), FieldValue::String(value.to_string()));
self
}
pub fn with_numeric_field(mut self, name: impl Into<String>, value: f64) -> Self {
self.fields.insert(name.into(), FieldValue::Number(value));
self
}
pub fn with_bool_field(mut self, name: impl Into<String>, value: bool) -> Self {
self.fields.insert(name.into(), FieldValue::Bool(value));
self
}
pub fn terminal(mut self, is_terminal: bool) -> Self {
self.is_terminal = is_terminal;
self
}
}
#[derive(Debug, Clone)]
pub enum FieldValue {
String(String),
Number(f64),
Bool(bool),
}
impl std::fmt::Display for FieldValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldValue::String(s) => write!(f, "{}", s),
FieldValue::Number(n) => write!(f, "{}", n),
FieldValue::Bool(b) => write!(f, "{}", b),
}
}
}
#[derive(Debug, Clone)]
pub struct TransitionInfo {
pub input: String,
pub target_status: String,
pub effects: Vec<String>,
pub timers: Vec<String>,
pub description: Option<String>,
}
impl TransitionInfo {
pub fn new(input: impl Into<String>, target_status: impl Into<String>) -> Self {
Self {
input: input.into(),
target_status: target_status.into(),
effects: vec![],
timers: vec![],
description: None,
}
}
pub fn with_effect(mut self, effect: impl Into<String>) -> Self {
self.effects.push(effect.into());
self
}
pub fn with_timer(mut self, timer: impl Into<String>) -> Self {
self.timers.push(timer.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone)]
pub struct StateMachineDefinition {
pub states: Vec<StateDefinition>,
pub transitions: Vec<TransitionDefinition>,
pub initial_state: String,
}
#[derive(Debug, Clone)]
pub struct StateDefinition {
pub name: String,
pub is_terminal: bool,
pub description: Option<String>,
}
impl StateDefinition {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
is_terminal: false,
description: None,
}
}
pub fn terminal(mut self) -> Self {
self.is_terminal = true;
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone)]
pub struct TransitionDefinition {
pub from: String,
pub to: String,
pub input: String,
pub effects: Vec<String>,
pub timers: Vec<String>,
}
impl TransitionDefinition {
pub fn new(from: impl Into<String>, to: impl Into<String>, input: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
input: input.into(),
effects: vec![],
timers: vec![],
}
}
pub fn with_effect(mut self, effect: impl Into<String>) -> Self {
self.effects.push(effect.into());
self
}
pub fn with_timer(mut self, timer: impl Into<String>) -> Self {
self.timers.push(timer.into());
self
}
}
impl StateMachineDefinition {
pub fn new(initial_state: impl Into<String>) -> Self {
Self {
states: vec![],
transitions: vec![],
initial_state: initial_state.into(),
}
}
pub fn with_state(mut self, state: StateDefinition) -> Self {
self.states.push(state);
self
}
pub fn with_transition(mut self, transition: TransitionDefinition) -> Self {
self.transitions.push(transition);
self
}
pub fn to_mermaid(&self) -> String {
let mut lines = vec!["stateDiagram-v2".to_string()];
lines.push(format!(" [*] --> {}", self.initial_state));
for t in &self.transitions {
let label = if t.effects.is_empty() && t.timers.is_empty() {
t.input.clone()
} else {
let mut parts = vec![t.input.clone()];
if !t.effects.is_empty() {
parts.push(format!("[{}]", t.effects.join(", ")));
}
if !t.timers.is_empty() {
parts.push(format!("⏱{}", t.timers.join(", ")));
}
parts.join(" ")
};
lines.push(format!(" {} --> {} : {}", t.from, t.to, label));
}
for s in &self.states {
if s.is_terminal {
lines.push(format!(" {} --> [*]", s.name));
}
}
lines.join("\n")
}
pub fn to_dot(&self) -> String {
let mut lines = vec![
"digraph workflow {".to_string(),
" rankdir=LR;".to_string(),
" node [shape=box];".to_string(),
"".to_string(),
];
for t in &self.transitions {
let label = if t.effects.is_empty() && t.timers.is_empty() {
t.input.clone()
} else {
let mut parts = vec![t.input.clone()];
if !t.effects.is_empty() {
parts.push(format!("[{}]", t.effects.join(", ")));
}
if !t.timers.is_empty() {
parts.push(format!("timer:{}", t.timers.join(", ")));
}
parts.join("\\n")
};
lines.push(format!(" {} -> {} [label=\"{}\"];", t.from, t.to, label));
}
lines.push("".to_string());
for s in &self.states {
if s.is_terminal {
lines.push(format!(" {} [shape=doublecircle];", s.name));
}
}
lines.push("}".to_string());
lines.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_info_builder() {
let info = StateInfo::new("processing")
.with_field("order_id", &"ord-123")
.with_numeric_field("total", 99.99)
.with_bool_field("paid", true)
.terminal(false);
assert_eq!(info.status, "processing");
assert_eq!(info.fields.len(), 3);
assert!(!info.is_terminal);
}
#[test]
fn transition_info_builder() {
let transition = TransitionInfo::new("Ship", "shipped")
.with_effect("SendShippingEmail")
.with_timer("DeliveryCheck")
.with_description("Ships the order");
assert_eq!(transition.input, "Ship");
assert_eq!(transition.target_status, "shipped");
assert_eq!(transition.effects, vec!["SendShippingEmail"]);
assert_eq!(transition.timers, vec!["DeliveryCheck"]);
}
#[test]
fn state_machine_to_mermaid() {
let sm = StateMachineDefinition::new("created")
.with_state(StateDefinition::new("created"))
.with_state(StateDefinition::new("paid"))
.with_state(StateDefinition::new("completed").terminal())
.with_transition(TransitionDefinition::new("created", "paid", "Pay"))
.with_transition(
TransitionDefinition::new("paid", "completed", "Complete")
.with_effect("SendReceipt"),
);
let mermaid = sm.to_mermaid();
assert!(mermaid.contains("stateDiagram-v2"));
assert!(mermaid.contains("[*] --> created"));
assert!(mermaid.contains("created --> paid : Pay"));
assert!(mermaid.contains("completed --> [*]"));
}
#[test]
fn state_machine_to_dot() {
let sm = StateMachineDefinition::new("created")
.with_state(StateDefinition::new("created"))
.with_state(StateDefinition::new("completed").terminal())
.with_transition(TransitionDefinition::new(
"created",
"completed",
"Complete",
));
let dot = sm.to_dot();
assert!(dot.contains("digraph workflow"));
assert!(dot.contains("created -> completed"));
assert!(dot.contains("[shape=doublecircle]"));
}
}