use super::{StepResult, Workflow, WorkflowPhase, WorkflowProgress, WorkflowSession};
use crate::{DrivenError, Result};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct WorkflowEngine {
workflows: HashMap<String, Workflow>,
sessions: HashMap<String, WorkflowSession>,
progress_dir: Option<std::path::PathBuf>,
}
impl WorkflowEngine {
pub fn new() -> Self {
Self {
workflows: HashMap::new(),
sessions: HashMap::new(),
progress_dir: None,
}
}
pub fn with_progress_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
self.progress_dir = Some(dir.into());
self
}
pub fn load_builtin(&mut self) {
let builtin_workflows = super::builtin::all_workflows();
for workflow in builtin_workflows {
self.workflows.insert(workflow.id.clone(), workflow);
}
}
pub fn register(&mut self, workflow: Workflow) -> Result<()> {
if self.workflows.contains_key(&workflow.id) {
return Err(DrivenError::Config(format!(
"Workflow with id '{}' already registered",
workflow.id
)));
}
self.workflows.insert(workflow.id.clone(), workflow);
Ok(())
}
pub fn get(&self, id: &str) -> Option<&Workflow> {
self.workflows.get(id)
}
pub fn list(&self) -> Vec<&Workflow> {
self.workflows.values().collect()
}
pub fn list_by_phase(&self, phase: WorkflowPhase) -> Vec<&Workflow> {
self.workflows
.values()
.filter(|w| w.phase == phase)
.collect()
}
pub fn start(&mut self, workflow_id: &str) -> Result<WorkflowSession> {
let workflow = self
.workflows
.get(workflow_id)
.ok_or_else(|| DrivenError::Config(format!("Workflow '{}' not found", workflow_id)))?
.clone();
let session_id = uuid::Uuid::new_v4().to_string();
let progress = WorkflowProgress::new(&session_id, &workflow);
let session = WorkflowSession {
id: session_id.clone(),
workflow,
progress,
};
self.sessions.insert(session_id.clone(), session.clone());
self.persist_progress(&session)?;
Ok(session)
}
pub fn resume(&mut self, session_id: &str) -> Result<WorkflowSession> {
if let Some(session) = self.sessions.get(session_id) {
return Ok(session.clone());
}
if let Some(ref dir) = self.progress_dir {
let path = dir.join(format!("{}.json", session_id));
if path.exists() {
let content = std::fs::read_to_string(&path)?;
let progress: WorkflowProgress = serde_json::from_str(&content)
.map_err(|e| DrivenError::Config(format!("Failed to parse progress: {}", e)))?;
let workflow = self
.workflows
.get(&progress.workflow_id)
.ok_or_else(|| {
DrivenError::Config(format!(
"Workflow '{}' not found",
progress.workflow_id
))
})?
.clone();
let session = WorkflowSession {
id: session_id.to_string(),
workflow,
progress,
};
self.sessions
.insert(session_id.to_string(), session.clone());
return Ok(session);
}
}
Err(DrivenError::Config(format!(
"Session '{}' not found",
session_id
)))
}
pub fn execute_step(&mut self, session: &mut WorkflowSession) -> Result<StepResult> {
let current_step_id = session.progress.current_step.clone();
let step = session
.workflow
.get_step(¤t_step_id)
.ok_or_else(|| DrivenError::Config(format!("Step '{}' not found", current_step_id)))?;
if let Some(ref condition) = step.condition {
if !self.evaluate_condition(condition, &session.progress)? {
if let Some(next) = session.workflow.next_step(¤t_step_id) {
session.progress.set_current_step(&next.id);
self.persist_progress(session)?;
return Ok(StepResult::success(Some(
"Step skipped due to condition".to_string(),
))
.with_next_step(&next.id));
}
}
}
session.progress.complete_step(¤t_step_id);
let next_step = if !step.branches.is_empty() {
let mut next = None;
for branch in &step.branches {
if self.evaluate_condition(&branch.condition, &session.progress)? {
next = Some(branch.next_step.clone());
break;
}
}
next.or_else(|| {
session
.workflow
.next_step(¤t_step_id)
.map(|s| s.id.clone())
})
} else {
session
.workflow
.next_step(¤t_step_id)
.map(|s| s.id.clone())
};
if let Some(ref next_id) = next_step {
session.progress.set_current_step(next_id);
}
self.persist_progress(session)?;
let mut result = StepResult::success(Some(format!("Completed step: {}", step.name)));
if let Some(next_id) = next_step {
result = result.with_next_step(next_id);
}
if step.is_checkpoint {
result = result.with_pause();
}
Ok(result)
}
pub fn get_progress(&self, session_id: &str) -> Option<&WorkflowProgress> {
self.sessions.get(session_id).map(|s| &s.progress)
}
pub fn substitute_variables(&self, template: &str, progress: &WorkflowProgress) -> String {
let mut result = template.to_string();
for (key, value) in &progress.variables {
result = result.replace(&format!("{{{}}}", key), value);
}
result
}
fn evaluate_condition(&self, condition: &str, progress: &WorkflowProgress) -> Result<bool> {
let condition = condition.trim();
if condition.contains("==") {
let parts: Vec<&str> = condition.split("==").collect();
if parts.len() == 2 {
let var_name = parts[0].trim();
let expected = parts[1].trim().trim_matches('"');
let actual = progress
.get_variable(var_name)
.map(|s| s.as_str())
.unwrap_or("");
return Ok(actual == expected);
}
} else if condition.contains("!=") {
let parts: Vec<&str> = condition.split("!=").collect();
if parts.len() == 2 {
let var_name = parts[0].trim();
let expected = parts[1].trim().trim_matches('"');
let actual = progress
.get_variable(var_name)
.map(|s| s.as_str())
.unwrap_or("");
return Ok(actual != expected);
}
}
Ok(true)
}
fn persist_progress(&self, session: &WorkflowSession) -> Result<()> {
if let Some(ref dir) = self.progress_dir {
std::fs::create_dir_all(dir)?;
let path = dir.join(format!("{}.json", session.id));
let content = serde_json::to_string_pretty(&session.progress)
.map_err(|e| DrivenError::Config(format!("Failed to serialize progress: {}", e)))?;
std::fs::write(path, content)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_engine_new() {
let engine = WorkflowEngine::new();
assert!(engine.workflows.is_empty());
}
#[test]
fn test_load_builtin() {
let mut engine = WorkflowEngine::new();
engine.load_builtin();
assert!(!engine.workflows.is_empty());
assert!(engine.workflows.len() >= 30);
}
#[test]
fn test_start_workflow() {
let mut engine = WorkflowEngine::new();
engine.load_builtin();
let session = engine.start("quick-bug-fix");
assert!(session.is_ok());
let session = session.unwrap();
assert!(!session.id.is_empty());
assert_eq!(session.workflow.id, "quick-bug-fix");
}
#[test]
fn test_list_by_phase() {
let mut engine = WorkflowEngine::new();
engine.load_builtin();
let analysis = engine.list_by_phase(WorkflowPhase::Analysis);
assert!(!analysis.is_empty());
let quick_flow = engine.list_by_phase(WorkflowPhase::QuickFlow);
assert!(!quick_flow.is_empty());
}
#[test]
fn test_variable_substitution() {
let engine = WorkflowEngine::new();
let workflow = Workflow::new("test", "Test", WorkflowPhase::Analysis, "Test workflow")
.with_variable("project", "my-project")
.with_variable("author", "John");
let progress = WorkflowProgress::new("session-1", &workflow);
let template = "Project: {project}, Author: {author}";
let result = engine.substitute_variables(template, &progress);
assert_eq!(result, "Project: my-project, Author: John");
}
}