use super::definition::Workflow;
use crate::error::CliError;
type Result<T> = std::result::Result<T, CliError>;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationError {
DuplicateStepName(String),
MissingDependency { step: String, dependency: String },
CircularDependency(Vec<String>),
InvalidCondition { step: String, reason: String },
InvalidParameter {
step: String,
parameter: String,
reason: String,
},
EmptyWorkflow,
InvalidForEachVariable { step: String, variable: String },
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateStepName(name) => write!(f, "Duplicate step name: {}", name),
Self::MissingDependency { step, dependency } => {
write!(
f,
"Step '{}' depends on non-existent step '{}'",
step, dependency
)
}
Self::CircularDependency(cycle) => {
write!(f, "Circular dependency detected: {}", cycle.join(" -> "))
}
Self::InvalidCondition { step, reason } => {
write!(f, "Invalid condition in step '{}': {}", step, reason)
}
Self::InvalidParameter {
step,
parameter,
reason,
} => write!(
f,
"Invalid parameter '{}' in step '{}': {}",
parameter, step, reason
),
Self::EmptyWorkflow => write!(f, "Workflow has no steps"),
Self::InvalidForEachVariable { step, variable } => {
write!(
f,
"Invalid for-each variable '{}' in step '{}'",
variable, step
)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn failure(errors: Vec<ValidationError>) -> Self {
Self {
valid: false,
errors,
warnings: Vec::new(),
}
}
pub fn with_warning(mut self, warning: String) -> Self {
self.warnings.push(warning);
self
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
}
pub struct WorkflowValidator {
max_steps: usize,
max_dependency_depth: usize,
}
impl WorkflowValidator {
pub fn new() -> Self {
Self {
max_steps: 1000,
max_dependency_depth: 100,
}
}
pub fn with_limits(max_steps: usize, max_dependency_depth: usize) -> Self {
Self {
max_steps,
max_dependency_depth,
}
}
pub fn validate(&self, workflow: &Workflow) -> Result<ValidationResult> {
let mut errors = Vec::new();
let mut warnings = Vec::new();
if workflow.steps.is_empty() {
errors.push(ValidationError::EmptyWorkflow);
return Ok(ValidationResult::failure(errors));
}
if workflow.steps.len() > self.max_steps {
warnings.push(format!(
"Workflow has {} steps, which exceeds recommended limit of {}",
workflow.steps.len(),
self.max_steps
));
}
let mut step_names = HashSet::new();
for step in &workflow.steps {
if !step_names.insert(&step.name) {
errors.push(ValidationError::DuplicateStepName(step.name.clone()));
}
}
let step_map: HashMap<&String, &super::definition::Step> =
workflow.steps.iter().map(|s| (&s.name, s)).collect();
for step in &workflow.steps {
for dep in &step.depends_on {
if !step_map.contains_key(&dep.step_name) {
errors.push(ValidationError::MissingDependency {
step: step.name.clone(),
dependency: dep.step_name.clone(),
});
}
}
}
if let Some(cycle) = self.detect_cycles(workflow) {
errors.push(ValidationError::CircularDependency(cycle));
}
for step in &workflow.steps {
let depth = self.calculate_dependency_depth(&step.name, workflow, &mut HashSet::new());
if depth > self.max_dependency_depth {
warnings.push(format!(
"Step '{}' has dependency depth of {}, which exceeds recommended limit of {}",
step.name, depth, self.max_dependency_depth
));
}
}
for step in &workflow.steps {
if let Some(ref condition) = step.condition {
if condition.left.is_empty() || condition.right.is_empty() {
errors.push(ValidationError::InvalidCondition {
step: step.name.clone(),
reason: "Condition operands cannot be empty".to_string(),
});
}
}
}
for step in &workflow.steps {
if let Some(ref for_each_var) = step.for_each {
if !for_each_var.starts_with("${") || !for_each_var.ends_with('}') {
errors.push(ValidationError::InvalidForEachVariable {
step: step.name.clone(),
variable: for_each_var.clone(),
});
}
}
}
if errors.is_empty() {
let mut result = ValidationResult::success();
result.warnings = warnings;
Ok(result)
} else {
let mut result = ValidationResult::failure(errors);
result.warnings = warnings;
Ok(result)
}
}
fn detect_cycles(&self, workflow: &Workflow) -> Option<Vec<String>> {
let mut visited = HashSet::new();
let mut recursion_stack = Vec::new();
for step in &workflow.steps {
if self.has_cycle_dfs(&step.name, workflow, &mut visited, &mut recursion_stack) {
return Some(recursion_stack);
}
}
None
}
fn has_cycle_dfs(
&self,
node: &str,
workflow: &Workflow,
visited: &mut HashSet<String>,
recursion_stack: &mut Vec<String>,
) -> bool {
if recursion_stack.iter().any(|s| s == node) {
recursion_stack.push(node.to_string());
return true;
}
if visited.contains(node) {
return false;
}
visited.insert(node.to_string());
recursion_stack.push(node.to_string());
if let Some(step) = workflow.steps.iter().find(|s| s.name == node) {
for dep in &step.depends_on {
if self.has_cycle_dfs(&dep.step_name, workflow, visited, recursion_stack) {
return true;
}
}
}
recursion_stack.pop();
false
}
fn calculate_dependency_depth(
&self,
step_name: &str,
workflow: &Workflow,
visited: &mut HashSet<String>,
) -> usize {
if visited.contains(step_name) {
return 0;
}
visited.insert(step_name.to_string());
let step = workflow.steps.iter().find(|s| s.name == step_name);
if let Some(step) = step {
if step.depends_on.is_empty() {
return 1;
}
let max_dep_depth = step
.depends_on
.iter()
.map(|dep| self.calculate_dependency_depth(&dep.step_name, workflow, visited))
.max()
.unwrap_or(0);
max_dep_depth + 1
} else {
0
}
}
}
impl Default for WorkflowValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workflow::definition::{Step, StepDependency, StepType};
use std::collections::HashMap;
#[test]
fn test_validator_creation() {
let validator = WorkflowValidator::new();
assert_eq!(validator.max_steps, 1000);
}
#[test]
fn test_validate_empty_workflow() {
let validator = WorkflowValidator::new();
let workflow = Workflow::new("test", "1.0", "Test");
let result = validator.validate(&workflow).unwrap();
assert!(!result.valid);
assert!(result.has_errors());
}
#[test]
fn test_validate_valid_workflow() {
let validator = WorkflowValidator::new();
let mut workflow = Workflow::new("test", "1.0", "Test");
let step = Step {
name: "step1".to_string(),
step_type: StepType::Command,
description: None,
parameters: HashMap::new(),
condition: None,
depends_on: Vec::new(),
retry: None,
for_each: None,
parallel: false,
};
workflow.add_step(step);
let result = validator.validate(&workflow).unwrap();
assert!(result.valid);
assert!(!result.has_errors());
}
#[test]
fn test_validate_duplicate_step_names() {
let validator = WorkflowValidator::new();
let mut workflow = Workflow::new("test", "1.0", "Test");
let step1 = Step {
name: "duplicate".to_string(),
step_type: StepType::Command,
description: None,
parameters: HashMap::new(),
condition: None,
depends_on: Vec::new(),
retry: None,
for_each: None,
parallel: false,
};
workflow.add_step(step1.clone());
workflow.add_step(step1);
let result = validator.validate(&workflow).unwrap();
assert!(!result.valid);
assert!(result.has_errors());
}
#[test]
fn test_validate_missing_dependency() {
let validator = WorkflowValidator::new();
let mut workflow = Workflow::new("test", "1.0", "Test");
let step = Step {
name: "step1".to_string(),
step_type: StepType::Command,
description: None,
parameters: HashMap::new(),
condition: None,
depends_on: vec![StepDependency {
step_name: "nonexistent".to_string(),
must_succeed: true,
}],
retry: None,
for_each: None,
parallel: false,
};
workflow.add_step(step);
let result = validator.validate(&workflow).unwrap();
assert!(!result.valid);
assert!(result.has_errors());
}
#[test]
fn test_validation_result_success() {
let result = ValidationResult::success();
assert!(result.valid);
assert!(!result.has_errors());
}
#[test]
fn test_validation_result_with_warning() {
let result = ValidationResult::success().with_warning("Test warning".to_string());
assert!(result.valid);
assert!(result.has_warnings());
assert_eq!(result.warnings.len(), 1);
}
}