driven/workflows/
engine.rs1use super::{StepResult, Workflow, WorkflowPhase, WorkflowProgress, WorkflowSession};
6use crate::{DrivenError, Result};
7use std::collections::HashMap;
8
9#[derive(Debug, Default)]
11pub struct WorkflowEngine {
12 workflows: HashMap<String, Workflow>,
13 sessions: HashMap<String, WorkflowSession>,
14 progress_dir: Option<std::path::PathBuf>,
15}
16
17impl WorkflowEngine {
18 pub fn new() -> Self {
20 Self {
21 workflows: HashMap::new(),
22 sessions: HashMap::new(),
23 progress_dir: None,
24 }
25 }
26
27 pub fn with_progress_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
29 self.progress_dir = Some(dir.into());
30 self
31 }
32
33 pub fn load_builtin(&mut self) {
35 let builtin_workflows = super::builtin::all_workflows();
36 for workflow in builtin_workflows {
37 self.workflows.insert(workflow.id.clone(), workflow);
38 }
39 }
40
41 pub fn register(&mut self, workflow: Workflow) -> Result<()> {
43 if self.workflows.contains_key(&workflow.id) {
44 return Err(DrivenError::Config(format!(
45 "Workflow with id '{}' already registered",
46 workflow.id
47 )));
48 }
49 self.workflows.insert(workflow.id.clone(), workflow);
50 Ok(())
51 }
52
53 pub fn get(&self, id: &str) -> Option<&Workflow> {
55 self.workflows.get(id)
56 }
57
58 pub fn list(&self) -> Vec<&Workflow> {
60 self.workflows.values().collect()
61 }
62
63 pub fn list_by_phase(&self, phase: WorkflowPhase) -> Vec<&Workflow> {
65 self.workflows
66 .values()
67 .filter(|w| w.phase == phase)
68 .collect()
69 }
70
71 pub fn start(&mut self, workflow_id: &str) -> Result<WorkflowSession> {
73 let workflow = self
74 .workflows
75 .get(workflow_id)
76 .ok_or_else(|| DrivenError::Config(format!("Workflow '{}' not found", workflow_id)))?
77 .clone();
78
79 let session_id = uuid::Uuid::new_v4().to_string();
80 let progress = WorkflowProgress::new(&session_id, &workflow);
81
82 let session = WorkflowSession {
83 id: session_id.clone(),
84 workflow,
85 progress,
86 };
87
88 self.sessions.insert(session_id.clone(), session.clone());
89 self.persist_progress(&session)?;
90
91 Ok(session)
92 }
93
94 pub fn resume(&mut self, session_id: &str) -> Result<WorkflowSession> {
96 if let Some(session) = self.sessions.get(session_id) {
98 return Ok(session.clone());
99 }
100
101 if let Some(ref dir) = self.progress_dir {
103 let path = dir.join(format!("{}.json", session_id));
104 if path.exists() {
105 let content = std::fs::read_to_string(&path)?;
106 let progress: WorkflowProgress = serde_json::from_str(&content)
107 .map_err(|e| DrivenError::Config(format!("Failed to parse progress: {}", e)))?;
108
109 let workflow = self
110 .workflows
111 .get(&progress.workflow_id)
112 .ok_or_else(|| {
113 DrivenError::Config(format!(
114 "Workflow '{}' not found",
115 progress.workflow_id
116 ))
117 })?
118 .clone();
119
120 let session = WorkflowSession {
121 id: session_id.to_string(),
122 workflow,
123 progress,
124 };
125
126 self.sessions
127 .insert(session_id.to_string(), session.clone());
128 return Ok(session);
129 }
130 }
131
132 Err(DrivenError::Config(format!(
133 "Session '{}' not found",
134 session_id
135 )))
136 }
137
138 pub fn execute_step(&mut self, session: &mut WorkflowSession) -> Result<StepResult> {
140 let current_step_id = session.progress.current_step.clone();
141 let step = session
142 .workflow
143 .get_step(¤t_step_id)
144 .ok_or_else(|| DrivenError::Config(format!("Step '{}' not found", current_step_id)))?;
145
146 if let Some(ref condition) = step.condition {
148 if !self.evaluate_condition(condition, &session.progress)? {
149 if let Some(next) = session.workflow.next_step(¤t_step_id) {
151 session.progress.set_current_step(&next.id);
152 self.persist_progress(session)?;
153 return Ok(StepResult::success(Some(
154 "Step skipped due to condition".to_string(),
155 ))
156 .with_next_step(&next.id));
157 }
158 }
159 }
160
161 session.progress.complete_step(¤t_step_id);
163
164 let next_step = if !step.branches.is_empty() {
166 let mut next = None;
168 for branch in &step.branches {
169 if self.evaluate_condition(&branch.condition, &session.progress)? {
170 next = Some(branch.next_step.clone());
171 break;
172 }
173 }
174 next.or_else(|| {
175 session
176 .workflow
177 .next_step(¤t_step_id)
178 .map(|s| s.id.clone())
179 })
180 } else {
181 session
182 .workflow
183 .next_step(¤t_step_id)
184 .map(|s| s.id.clone())
185 };
186
187 if let Some(ref next_id) = next_step {
189 session.progress.set_current_step(next_id);
190 }
191
192 self.persist_progress(session)?;
193
194 let mut result = StepResult::success(Some(format!("Completed step: {}", step.name)));
196 if let Some(next_id) = next_step {
197 result = result.with_next_step(next_id);
198 }
199 if step.is_checkpoint {
200 result = result.with_pause();
201 }
202
203 Ok(result)
204 }
205
206 pub fn get_progress(&self, session_id: &str) -> Option<&WorkflowProgress> {
208 self.sessions.get(session_id).map(|s| &s.progress)
209 }
210
211 pub fn substitute_variables(&self, template: &str, progress: &WorkflowProgress) -> String {
213 let mut result = template.to_string();
214 for (key, value) in &progress.variables {
215 result = result.replace(&format!("{{{}}}", key), value);
216 }
217 result
218 }
219
220 fn evaluate_condition(&self, condition: &str, progress: &WorkflowProgress) -> Result<bool> {
222 let condition = condition.trim();
225
226 if condition.contains("==") {
227 let parts: Vec<&str> = condition.split("==").collect();
228 if parts.len() == 2 {
229 let var_name = parts[0].trim();
230 let expected = parts[1].trim().trim_matches('"');
231 let actual = progress
232 .get_variable(var_name)
233 .map(|s| s.as_str())
234 .unwrap_or("");
235 return Ok(actual == expected);
236 }
237 } else if condition.contains("!=") {
238 let parts: Vec<&str> = condition.split("!=").collect();
239 if parts.len() == 2 {
240 let var_name = parts[0].trim();
241 let expected = parts[1].trim().trim_matches('"');
242 let actual = progress
243 .get_variable(var_name)
244 .map(|s| s.as_str())
245 .unwrap_or("");
246 return Ok(actual != expected);
247 }
248 }
249
250 Ok(true)
252 }
253
254 fn persist_progress(&self, session: &WorkflowSession) -> Result<()> {
256 if let Some(ref dir) = self.progress_dir {
257 std::fs::create_dir_all(dir)?;
258 let path = dir.join(format!("{}.json", session.id));
259 let content = serde_json::to_string_pretty(&session.progress)
260 .map_err(|e| DrivenError::Config(format!("Failed to serialize progress: {}", e)))?;
261 std::fs::write(path, content)?;
262 }
263 Ok(())
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_engine_new() {
273 let engine = WorkflowEngine::new();
274 assert!(engine.workflows.is_empty());
275 }
276
277 #[test]
278 fn test_load_builtin() {
279 let mut engine = WorkflowEngine::new();
280 engine.load_builtin();
281 assert!(!engine.workflows.is_empty());
282 assert!(engine.workflows.len() >= 30);
283 }
284
285 #[test]
286 fn test_start_workflow() {
287 let mut engine = WorkflowEngine::new();
288 engine.load_builtin();
289
290 let session = engine.start("quick-bug-fix");
291 assert!(session.is_ok());
292
293 let session = session.unwrap();
294 assert!(!session.id.is_empty());
295 assert_eq!(session.workflow.id, "quick-bug-fix");
296 }
297
298 #[test]
299 fn test_list_by_phase() {
300 let mut engine = WorkflowEngine::new();
301 engine.load_builtin();
302
303 let analysis = engine.list_by_phase(WorkflowPhase::Analysis);
304 assert!(!analysis.is_empty());
305
306 let quick_flow = engine.list_by_phase(WorkflowPhase::QuickFlow);
307 assert!(!quick_flow.is_empty());
308 }
309
310 #[test]
311 fn test_variable_substitution() {
312 let engine = WorkflowEngine::new();
313 let workflow = Workflow::new("test", "Test", WorkflowPhase::Analysis, "Test workflow")
314 .with_variable("project", "my-project")
315 .with_variable("author", "John");
316
317 let progress = WorkflowProgress::new("session-1", &workflow);
318
319 let template = "Project: {project}, Author: {author}";
320 let result = engine.substitute_variables(template, &progress);
321
322 assert_eq!(result, "Project: my-project, Author: John");
323 }
324}