Skip to main content

driven/workflows/
engine.rs

1//! Workflow Engine
2//!
3//! Manages workflow execution, progress tracking, and resumption.
4
5use super::{StepResult, Workflow, WorkflowPhase, WorkflowProgress, WorkflowSession};
6use crate::{DrivenError, Result};
7use std::collections::HashMap;
8
9/// Engine for managing workflow execution
10#[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    /// Create a new workflow engine
19    pub fn new() -> Self {
20        Self {
21            workflows: HashMap::new(),
22            sessions: HashMap::new(),
23            progress_dir: None,
24        }
25    }
26
27    /// Set the directory for persisting progress
28    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    /// Load all built-in workflows
34    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    /// Register a workflow
42    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    /// Get a workflow by ID
54    pub fn get(&self, id: &str) -> Option<&Workflow> {
55        self.workflows.get(id)
56    }
57
58    /// List all workflows
59    pub fn list(&self) -> Vec<&Workflow> {
60        self.workflows.values().collect()
61    }
62
63    /// List workflows by phase
64    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    /// Start a new workflow session
72    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    /// Resume an existing workflow session
95    pub fn resume(&mut self, session_id: &str) -> Result<WorkflowSession> {
96        // Try to get from memory first
97        if let Some(session) = self.sessions.get(session_id) {
98            return Ok(session.clone());
99        }
100
101        // Try to load from disk
102        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    /// Execute the current step in a session
139    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(&current_step_id)
144            .ok_or_else(|| DrivenError::Config(format!("Step '{}' not found", current_step_id)))?;
145
146        // Check condition if present
147        if let Some(ref condition) = step.condition {
148            if !self.evaluate_condition(condition, &session.progress)? {
149                // Skip this step, move to next
150                if let Some(next) = session.workflow.next_step(&current_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        // Mark step as completed
162        session.progress.complete_step(&current_step_id);
163
164        // Determine next step
165        let next_step = if !step.branches.is_empty() {
166            // Evaluate branches
167            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(&current_step_id)
178                    .map(|s| s.id.clone())
179            })
180        } else {
181            session
182                .workflow
183                .next_step(&current_step_id)
184                .map(|s| s.id.clone())
185        };
186
187        // Update current step
188        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        // Create result
195        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    /// Get progress for a session
207    pub fn get_progress(&self, session_id: &str) -> Option<&WorkflowProgress> {
208        self.sessions.get(session_id).map(|s| &s.progress)
209    }
210
211    /// Substitute variables in a template string
212    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    /// Evaluate a condition expression
221    fn evaluate_condition(&self, condition: &str, progress: &WorkflowProgress) -> Result<bool> {
222        // Simple condition evaluation
223        // Format: "variable == value" or "variable != value"
224        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        // Default to true for unrecognized conditions
251        Ok(true)
252    }
253
254    /// Persist progress to disk
255    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}