Jesús Pérez fcb928bf74
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
chore: fix format and clippy
2026-02-03 22:03:41 +00:00

285 lines
7.0 KiB
Rust

// vapora-backend: Workflow YAML parser
// Phase 3: Parse workflow definitions from YAML
use std::fs;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::workflow::state::{Phase, StepStatus, Workflow, WorkflowStep};
#[derive(Debug, Error)]
#[allow(dead_code, clippy::enum_variant_names)]
pub enum ParserError {
#[error("Failed to read file: {0}")]
FileError(#[from] std::io::Error),
#[error("Failed to parse YAML: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Invalid workflow definition: {0}")]
ValidationError(String),
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(dead_code)]
pub struct WorkflowYaml {
pub workflow: WorkflowDef,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(dead_code)]
pub struct WorkflowDef {
pub id: String,
pub title: String,
pub phases: Vec<PhaseDef>,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(dead_code)]
pub struct PhaseDef {
pub id: String,
pub name: String,
#[serde(default)]
pub parallel: bool,
#[serde(default = "default_estimated_hours")]
pub estimated_hours: f32,
pub steps: Vec<StepDef>,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(dead_code)]
pub struct StepDef {
pub id: String,
pub name: String,
pub agent: String,
#[serde(default)]
pub depends_on: Vec<String>,
#[serde(default)]
pub parallelizable: bool,
}
#[allow(dead_code)]
fn default_estimated_hours() -> f32 {
1.0
}
#[allow(dead_code)]
pub struct WorkflowParser;
#[allow(dead_code)]
impl WorkflowParser {
/// Parse workflow from YAML file
pub fn parse_file(path: &str) -> Result<Workflow, ParserError> {
let content = fs::read_to_string(path)?;
Self::parse_string(&content)
}
/// Parse workflow from YAML string
pub fn parse_string(yaml: &str) -> Result<Workflow, ParserError> {
let workflow_yaml: WorkflowYaml = serde_yaml::from_str(yaml)?;
Self::validate_and_convert(workflow_yaml)
}
/// Validate and convert YAML definition to runtime Workflow
fn validate_and_convert(yaml: WorkflowYaml) -> Result<Workflow, ParserError> {
let def = yaml.workflow;
// Validate workflow has phases
if def.phases.is_empty() {
return Err(ParserError::ValidationError(
"Workflow must have at least one phase".to_string(),
));
}
// Convert phases
let mut phases = Vec::new();
for phase_def in def.phases {
// Validate phase has steps
if phase_def.steps.is_empty() {
return Err(ParserError::ValidationError(format!(
"Phase '{}' must have at least one step",
phase_def.id
)));
}
// Convert steps
let steps: Vec<WorkflowStep> = phase_def
.steps
.into_iter()
.map(|step_def| WorkflowStep {
id: step_def.id,
name: step_def.name,
agent_role: step_def.agent,
status: StepStatus::Pending,
depends_on: step_def.depends_on,
can_parallelize: step_def.parallelizable,
started_at: None,
completed_at: None,
result: None,
error: None,
})
.collect();
// Validate dependencies exist
Self::validate_dependencies(&steps)?;
phases.push(Phase {
id: phase_def.id,
name: phase_def.name,
status: StepStatus::Pending,
steps,
parallel: phase_def.parallel,
estimated_hours: phase_def.estimated_hours,
});
}
Ok(Workflow::new(def.id, def.title, phases))
}
/// Validate that all step dependencies exist
fn validate_dependencies(steps: &[WorkflowStep]) -> Result<(), ParserError> {
let step_ids: std::collections::HashSet<_> = steps.iter().map(|s| &s.id).collect();
for step in steps {
for dep in &step.depends_on {
if !step_ids.contains(dep) {
return Err(ParserError::ValidationError(format!(
"Step '{}' depends on non-existent step '{}'",
step.id, dep
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_workflow_yaml() {
let yaml = r#"
workflow:
id: feature-auth
title: Implement MFA
phases:
- id: phase_1
name: Design
parallel: false
estimated_hours: 2.0
steps:
- id: step_1_1
name: Architect Design
agent: architect
depends_on: []
parallelizable: false
- id: phase_2
name: Implementation
parallel: true
estimated_hours: 8.0
steps:
- id: step_2_1
name: Backend API
agent: developer
depends_on: []
parallelizable: true
- id: step_2_2
name: Frontend UI
agent: developer
depends_on: []
parallelizable: true
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_ok());
let workflow = result.unwrap();
assert_eq!(workflow.id, "feature-auth");
assert_eq!(workflow.title, "Implement MFA");
assert_eq!(workflow.phases.len(), 2);
assert_eq!(workflow.phases[0].steps.len(), 1);
assert_eq!(workflow.phases[1].steps.len(), 2);
assert!(workflow.phases[1].parallel);
}
#[test]
fn test_empty_phases_error() {
let yaml = r#"
workflow:
id: test
title: Test
phases: []
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_err());
}
#[test]
fn test_empty_steps_error() {
let yaml = r#"
workflow:
id: test
title: Test
phases:
- id: phase_1
name: Phase
steps: []
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_err());
}
#[test]
fn test_invalid_dependency() {
let yaml = r#"
workflow:
id: test
title: Test
phases:
- id: phase_1
name: Phase
steps:
- id: step_1
name: Step 1
agent: developer
depends_on: [nonexistent]
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_err());
}
#[test]
fn test_valid_dependencies() {
let yaml = r#"
workflow:
id: test
title: Test
phases:
- id: phase_1
name: Phase
steps:
- id: step_1
name: Step 1
agent: developer
depends_on: []
- id: step_2
name: Step 2
agent: developer
depends_on: [step_1]
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_ok());
let workflow = result.unwrap();
assert_eq!(workflow.phases[0].steps[1].depends_on, vec!["step_1"]);
}
}