1use super::definition::Workflow;
6use crate::error::CliError;
7
8type Result<T> = std::result::Result<T, CliError>;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum ValidationError {
15 DuplicateStepName(String),
17 MissingDependency { step: String, dependency: String },
19 CircularDependency(Vec<String>),
21 InvalidCondition { step: String, reason: String },
23 InvalidParameter {
25 step: String,
26 parameter: String,
27 reason: String,
28 },
29 EmptyWorkflow,
31 InvalidForEachVariable { step: String, variable: String },
33}
34
35impl std::fmt::Display for ValidationError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::DuplicateStepName(name) => write!(f, "Duplicate step name: {}", name),
39 Self::MissingDependency { step, dependency } => {
40 write!(
41 f,
42 "Step '{}' depends on non-existent step '{}'",
43 step, dependency
44 )
45 }
46 Self::CircularDependency(cycle) => {
47 write!(f, "Circular dependency detected: {}", cycle.join(" -> "))
48 }
49 Self::InvalidCondition { step, reason } => {
50 write!(f, "Invalid condition in step '{}': {}", step, reason)
51 }
52 Self::InvalidParameter {
53 step,
54 parameter,
55 reason,
56 } => write!(
57 f,
58 "Invalid parameter '{}' in step '{}': {}",
59 parameter, step, reason
60 ),
61 Self::EmptyWorkflow => write!(f, "Workflow has no steps"),
62 Self::InvalidForEachVariable { step, variable } => {
63 write!(
64 f,
65 "Invalid for-each variable '{}' in step '{}'",
66 variable, step
67 )
68 }
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ValidationResult {
76 pub valid: bool,
78 pub errors: Vec<ValidationError>,
80 pub warnings: Vec<String>,
82}
83
84impl ValidationResult {
85 pub fn success() -> Self {
87 Self {
88 valid: true,
89 errors: Vec::new(),
90 warnings: Vec::new(),
91 }
92 }
93
94 pub fn failure(errors: Vec<ValidationError>) -> Self {
96 Self {
97 valid: false,
98 errors,
99 warnings: Vec::new(),
100 }
101 }
102
103 pub fn with_warning(mut self, warning: String) -> Self {
105 self.warnings.push(warning);
106 self
107 }
108
109 pub fn has_errors(&self) -> bool {
111 !self.errors.is_empty()
112 }
113
114 pub fn has_warnings(&self) -> bool {
116 !self.warnings.is_empty()
117 }
118}
119
120pub struct WorkflowValidator {
122 max_steps: usize,
124 max_dependency_depth: usize,
126}
127
128impl WorkflowValidator {
129 pub fn new() -> Self {
131 Self {
132 max_steps: 1000,
133 max_dependency_depth: 100,
134 }
135 }
136
137 pub fn with_limits(max_steps: usize, max_dependency_depth: usize) -> Self {
139 Self {
140 max_steps,
141 max_dependency_depth,
142 }
143 }
144
145 pub fn validate(&self, workflow: &Workflow) -> Result<ValidationResult> {
147 let mut errors = Vec::new();
148 let mut warnings = Vec::new();
149
150 if workflow.steps.is_empty() {
152 errors.push(ValidationError::EmptyWorkflow);
153 return Ok(ValidationResult::failure(errors));
154 }
155
156 if workflow.steps.len() > self.max_steps {
158 warnings.push(format!(
159 "Workflow has {} steps, which exceeds recommended limit of {}",
160 workflow.steps.len(),
161 self.max_steps
162 ));
163 }
164
165 let mut step_names = HashSet::new();
167 for step in &workflow.steps {
168 if !step_names.insert(&step.name) {
169 errors.push(ValidationError::DuplicateStepName(step.name.clone()));
170 }
171 }
172
173 let step_map: HashMap<&String, &super::definition::Step> =
175 workflow.steps.iter().map(|s| (&s.name, s)).collect();
176
177 for step in &workflow.steps {
179 for dep in &step.depends_on {
180 if !step_map.contains_key(&dep.step_name) {
181 errors.push(ValidationError::MissingDependency {
182 step: step.name.clone(),
183 dependency: dep.step_name.clone(),
184 });
185 }
186 }
187 }
188
189 if let Some(cycle) = self.detect_cycles(workflow) {
191 errors.push(ValidationError::CircularDependency(cycle));
192 }
193
194 for step in &workflow.steps {
196 let depth = self.calculate_dependency_depth(&step.name, workflow, &mut HashSet::new());
197 if depth > self.max_dependency_depth {
198 warnings.push(format!(
199 "Step '{}' has dependency depth of {}, which exceeds recommended limit of {}",
200 step.name, depth, self.max_dependency_depth
201 ));
202 }
203 }
204
205 for step in &workflow.steps {
207 if let Some(ref condition) = step.condition {
208 if condition.left.is_empty() || condition.right.is_empty() {
210 errors.push(ValidationError::InvalidCondition {
211 step: step.name.clone(),
212 reason: "Condition operands cannot be empty".to_string(),
213 });
214 }
215 }
216 }
217
218 for step in &workflow.steps {
220 if let Some(ref for_each_var) = step.for_each {
221 if !for_each_var.starts_with("${") || !for_each_var.ends_with('}') {
223 errors.push(ValidationError::InvalidForEachVariable {
224 step: step.name.clone(),
225 variable: for_each_var.clone(),
226 });
227 }
228 }
229 }
230
231 if errors.is_empty() {
232 let mut result = ValidationResult::success();
233 result.warnings = warnings;
234 Ok(result)
235 } else {
236 let mut result = ValidationResult::failure(errors);
237 result.warnings = warnings;
238 Ok(result)
239 }
240 }
241
242 fn detect_cycles(&self, workflow: &Workflow) -> Option<Vec<String>> {
244 let mut visited = HashSet::new();
245 let mut recursion_stack = Vec::new();
246
247 for step in &workflow.steps {
248 if self.has_cycle_dfs(&step.name, workflow, &mut visited, &mut recursion_stack) {
249 return Some(recursion_stack);
250 }
251 }
252
253 None
254 }
255
256 fn has_cycle_dfs(
258 &self,
259 node: &str,
260 workflow: &Workflow,
261 visited: &mut HashSet<String>,
262 recursion_stack: &mut Vec<String>,
263 ) -> bool {
264 if recursion_stack.iter().any(|s| s == node) {
265 recursion_stack.push(node.to_string());
266 return true;
267 }
268
269 if visited.contains(node) {
270 return false;
271 }
272
273 visited.insert(node.to_string());
274 recursion_stack.push(node.to_string());
275
276 if let Some(step) = workflow.steps.iter().find(|s| s.name == node) {
278 for dep in &step.depends_on {
279 if self.has_cycle_dfs(&dep.step_name, workflow, visited, recursion_stack) {
280 return true;
281 }
282 }
283 }
284
285 recursion_stack.pop();
286 false
287 }
288
289 fn calculate_dependency_depth(
291 &self,
292 step_name: &str,
293 workflow: &Workflow,
294 visited: &mut HashSet<String>,
295 ) -> usize {
296 if visited.contains(step_name) {
297 return 0;
298 }
299
300 visited.insert(step_name.to_string());
301
302 let step = workflow.steps.iter().find(|s| s.name == step_name);
303
304 if let Some(step) = step {
305 if step.depends_on.is_empty() {
306 return 1;
307 }
308
309 let max_dep_depth = step
310 .depends_on
311 .iter()
312 .map(|dep| self.calculate_dependency_depth(&dep.step_name, workflow, visited))
313 .max()
314 .unwrap_or(0);
315
316 max_dep_depth + 1
317 } else {
318 0
319 }
320 }
321}
322
323impl Default for WorkflowValidator {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::workflow::definition::{Step, StepDependency, StepType};
333 use std::collections::HashMap;
334
335 #[test]
336 fn test_validator_creation() {
337 let validator = WorkflowValidator::new();
338 assert_eq!(validator.max_steps, 1000);
339 }
340
341 #[test]
342 fn test_validate_empty_workflow() {
343 let validator = WorkflowValidator::new();
344 let workflow = Workflow::new("test", "1.0", "Test");
345
346 let result = validator.validate(&workflow).unwrap();
347 assert!(!result.valid);
348 assert!(result.has_errors());
349 }
350
351 #[test]
352 fn test_validate_valid_workflow() {
353 let validator = WorkflowValidator::new();
354 let mut workflow = Workflow::new("test", "1.0", "Test");
355
356 let step = Step {
357 name: "step1".to_string(),
358 step_type: StepType::Command,
359 description: None,
360 parameters: HashMap::new(),
361 condition: None,
362 depends_on: Vec::new(),
363 retry: None,
364 for_each: None,
365 parallel: false,
366 };
367
368 workflow.add_step(step);
369
370 let result = validator.validate(&workflow).unwrap();
371 assert!(result.valid);
372 assert!(!result.has_errors());
373 }
374
375 #[test]
376 fn test_validate_duplicate_step_names() {
377 let validator = WorkflowValidator::new();
378 let mut workflow = Workflow::new("test", "1.0", "Test");
379
380 let step1 = Step {
381 name: "duplicate".to_string(),
382 step_type: StepType::Command,
383 description: None,
384 parameters: HashMap::new(),
385 condition: None,
386 depends_on: Vec::new(),
387 retry: None,
388 for_each: None,
389 parallel: false,
390 };
391
392 workflow.add_step(step1.clone());
393 workflow.add_step(step1);
394
395 let result = validator.validate(&workflow).unwrap();
396 assert!(!result.valid);
397 assert!(result.has_errors());
398 }
399
400 #[test]
401 fn test_validate_missing_dependency() {
402 let validator = WorkflowValidator::new();
403 let mut workflow = Workflow::new("test", "1.0", "Test");
404
405 let step = Step {
406 name: "step1".to_string(),
407 step_type: StepType::Command,
408 description: None,
409 parameters: HashMap::new(),
410 condition: None,
411 depends_on: vec![StepDependency {
412 step_name: "nonexistent".to_string(),
413 must_succeed: true,
414 }],
415 retry: None,
416 for_each: None,
417 parallel: false,
418 };
419
420 workflow.add_step(step);
421
422 let result = validator.validate(&workflow).unwrap();
423 assert!(!result.valid);
424 assert!(result.has_errors());
425 }
426
427 #[test]
428 fn test_validation_result_success() {
429 let result = ValidationResult::success();
430 assert!(result.valid);
431 assert!(!result.has_errors());
432 }
433
434 #[test]
435 fn test_validation_result_with_warning() {
436 let result = ValidationResult::success().with_warning("Test warning".to_string());
437 assert!(result.valid);
438 assert!(result.has_warnings());
439 assert_eq!(result.warnings.len(), 1);
440 }
441}