molten_workflow/
engine.rs1use crate::error::WorkflowError;
4use molten_core::document::Document;
5use molten_core::workflow::{WorkflowDefinition, WorkflowGraph};
6
7pub fn transition(
32 doc: &mut Document,
33 workflow: &WorkflowDefinition,
34 target_phase_id: &str,
35) -> Result<(), WorkflowError> {
36 if doc.workflow_id != workflow.id() {
38 return Err(WorkflowError::WorkflowMismatch {
39 doc_wf: doc.workflow_id.clone(),
40 provided_wf: workflow.id().to_string(),
41 });
42 }
43
44 if workflow.get_phase(target_phase_id).is_none() {
46 return Err(WorkflowError::UnknownPhase(target_phase_id.to_string()));
47 }
48
49 if doc.current_phase.is_empty() {
52 if let Some(start_phase) = workflow.get_start_phase() {
53 if start_phase.id == target_phase_id {
54 doc.current_phase = target_phase_id.to_string();
55 return Ok(());
56 } else {
57 return Err(WorkflowError::InvalidTransition {
58 current: "WAITING_TO_START".to_string(),
59 target: target_phase_id.to_string(),
60 });
61 }
62 } else {
63 return Err(WorkflowError::UnknownPhase(
65 "No start phase defined".to_string(),
66 ));
67 }
68 }
69
70 if !workflow.can_transition(&doc.current_phase, target_phase_id) {
73 return Err(WorkflowError::InvalidTransition {
74 current: doc.current_phase.clone(),
75 target: target_phase_id.to_string(),
76 });
77 }
78
79 doc.current_phase = target_phase_id.to_string();
81
82 Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use molten_core::workflow::{Phase, PhaseType, Transition, WorkflowBuilder};
92
93 fn create_simple_workflow() -> WorkflowDefinition {
94 WorkflowBuilder::new("wf_ticket", "Ticket Workflow")
95 .add_phase(Phase::new("draft", "Draft", PhaseType::Start))
96 .add_phase(Phase::new("review", "Review", PhaseType::Normal))
97 .add_phase(Phase::new("closed", "Closed", PhaseType::End))
98 .add_transition(Transition::new("submit", "draft", "review"))
100 .add_transition(Transition::new("approve", "review", "closed"))
101 .add_transition(Transition::new("reject", "review", "draft"))
103 .build()
104 .unwrap()
105 }
106
107 #[test]
108 fn test_valid_transitions() {
109 let wf = create_simple_workflow();
110 let mut doc = Document::new("doc1", "form_ticket", "wf_ticket");
111
112 assert!(transition(&mut doc, &wf, "draft").is_ok());
114 assert_eq!(doc.current_phase, "draft");
115
116 assert!(transition(&mut doc, &wf, "review").is_ok());
118 assert_eq!(doc.current_phase, "review");
119
120 assert!(transition(&mut doc, &wf, "closed").is_ok());
122 assert_eq!(doc.current_phase, "closed");
123 }
124
125 #[test]
126 fn test_invalid_jump() {
127 let wf = create_simple_workflow();
128 let mut doc = Document::new("doc1", "doc_ticket", "wf_ticket");
129
130 let _ = transition(&mut doc, &wf, "draft");
132
133 let res = transition(&mut doc, &wf, "closed");
135 assert!(res.is_err());
136 assert!(matches!(
137 res.unwrap_err(),
138 WorkflowError::InvalidTransition { .. }
139 ));
140
141 assert_eq!(doc.current_phase, "draft");
143 }
144
145 #[test]
146 fn test_workflow_mismatch() {
147 let wf = create_simple_workflow(); let mut doc = Document::new("doc1", "doc_ticket", "other_workflow_id");
149
150 let res = transition(&mut doc, &wf, "draft");
151 assert!(matches!(
152 res.unwrap_err(),
153 WorkflowError::WorkflowMismatch { .. }
154 ));
155 }
156}