1use crate::error::{Result, WorkflowError};
4use crate::task::{Task, TaskType};
5use crate::workflow::Workflow;
6use std::collections::{HashMap, HashSet};
7use std::path::Path;
8
9pub struct WorkflowValidator {
11 rules: Vec<Box<dyn ValidationRule>>,
13}
14
15impl WorkflowValidator {
16 #[must_use]
18 pub fn new() -> Self {
19 Self { rules: Vec::new() }
20 }
21
22 #[must_use]
24 pub fn add_rule(mut self, rule: Box<dyn ValidationRule>) -> Self {
25 self.rules.push(rule);
26 self
27 }
28
29 #[must_use]
31 pub fn with_defaults(self) -> Self {
32 self.add_rule(Box::new(NoCyclesRule))
33 .add_rule(Box::new(ValidEdgesRule))
34 .add_rule(Box::new(ValidTaskTypesRule))
35 .add_rule(Box::new(NoOrphanTasksRule))
36 .add_rule(Box::new(ValidDependenciesRule))
37 }
38
39 pub fn validate(&self, workflow: &Workflow) -> Result<ValidationReport> {
41 let mut report = ValidationReport::new();
42
43 for rule in &self.rules {
44 if let Err(e) = rule.validate(workflow) {
45 report.add_error(e.to_string());
46 }
47 }
48
49 if let Err(e) = workflow.validate() {
50 report.add_error(e.to_string());
51 }
52
53 Ok(report)
54 }
55}
56
57impl Default for WorkflowValidator {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63pub trait ValidationRule: Send + Sync {
65 fn validate(&self, workflow: &Workflow) -> Result<()>;
67
68 fn name(&self) -> &str;
70}
71
72pub struct NoCyclesRule;
74
75impl ValidationRule for NoCyclesRule {
76 fn validate(&self, workflow: &Workflow) -> Result<()> {
77 if workflow.has_cycle() {
78 return Err(WorkflowError::CycleDetected);
79 }
80 Ok(())
81 }
82
83 fn name(&self) -> &'static str {
84 "NoCyclesRule"
85 }
86}
87
88pub struct ValidEdgesRule;
90
91impl ValidationRule for ValidEdgesRule {
92 fn validate(&self, workflow: &Workflow) -> Result<()> {
93 for edge in &workflow.edges {
94 if !workflow.tasks.contains_key(&edge.from) {
95 return Err(WorkflowError::TaskNotFound(edge.from.to_string()));
96 }
97 if !workflow.tasks.contains_key(&edge.to) {
98 return Err(WorkflowError::TaskNotFound(edge.to.to_string()));
99 }
100 }
101 Ok(())
102 }
103
104 fn name(&self) -> &'static str {
105 "ValidEdgesRule"
106 }
107}
108
109pub struct ValidTaskTypesRule;
111
112impl ValidationRule for ValidTaskTypesRule {
113 fn validate(&self, workflow: &Workflow) -> Result<()> {
114 for task in workflow.tasks.values() {
115 validate_task_type(&task.task_type)?;
116 }
117 Ok(())
118 }
119
120 fn name(&self) -> &'static str {
121 "ValidTaskTypesRule"
122 }
123}
124
125pub struct NoOrphanTasksRule;
127
128impl ValidationRule for NoOrphanTasksRule {
129 fn validate(&self, workflow: &Workflow) -> Result<()> {
130 if workflow.tasks.is_empty() {
131 return Ok(());
132 }
133
134 let root_tasks = workflow.get_root_tasks();
135 if root_tasks.is_empty() {
136 return Err(WorkflowError::InvalidConfiguration(
137 "No root tasks found".to_string(),
138 ));
139 }
140
141 let mut reachable = HashSet::new();
143 let mut queue = root_tasks.clone();
144
145 while let Some(task_id) = queue.pop() {
146 if reachable.insert(task_id) {
147 let dependents = workflow.get_dependents(&task_id);
148 queue.extend(dependents);
149 }
150 }
151
152 for task_id in workflow.tasks.keys() {
154 if !reachable.contains(task_id) {
155 return Err(WorkflowError::InvalidConfiguration(format!(
156 "Task {task_id} is not reachable"
157 )));
158 }
159 }
160
161 Ok(())
162 }
163
164 fn name(&self) -> &'static str {
165 "NoOrphanTasksRule"
166 }
167}
168
169pub struct ValidDependenciesRule;
171
172impl ValidationRule for ValidDependenciesRule {
173 fn validate(&self, workflow: &Workflow) -> Result<()> {
174 for task in workflow.tasks.values() {
175 for dep_id in &task.dependencies {
176 if !workflow.tasks.contains_key(dep_id) {
177 return Err(WorkflowError::TaskNotFound(dep_id.to_string()));
178 }
179 }
180 }
181 Ok(())
182 }
183
184 fn name(&self) -> &'static str {
185 "ValidDependenciesRule"
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct ValidationReport {
192 pub errors: Vec<String>,
194 pub warnings: Vec<String>,
196}
197
198impl ValidationReport {
199 #[must_use]
201 pub fn new() -> Self {
202 Self {
203 errors: Vec::new(),
204 warnings: Vec::new(),
205 }
206 }
207
208 pub fn add_error(&mut self, error: String) {
210 self.errors.push(error);
211 }
212
213 pub fn add_warning(&mut self, warning: String) {
215 self.warnings.push(warning);
216 }
217
218 #[must_use]
220 pub fn is_valid(&self) -> bool {
221 self.errors.is_empty()
222 }
223
224 #[must_use]
226 pub fn error_count(&self) -> usize {
227 self.errors.len()
228 }
229
230 #[must_use]
232 pub fn warning_count(&self) -> usize {
233 self.warnings.len()
234 }
235}
236
237impl Default for ValidationReport {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243fn validate_task_type(task_type: &TaskType) -> Result<()> {
245 match task_type {
246 TaskType::Transcode {
247 input,
248 output,
249 preset,
250 ..
251 } => {
252 if preset.is_empty() {
253 return Err(WorkflowError::InvalidConfiguration(
254 "Transcode preset cannot be empty".to_string(),
255 ));
256 }
257 validate_file_path(input)?;
258 validate_file_path(output)?;
259 }
260
261 TaskType::QualityControl { input, profile, .. } => {
262 if profile.is_empty() {
263 return Err(WorkflowError::InvalidConfiguration(
264 "QC profile cannot be empty".to_string(),
265 ));
266 }
267 validate_file_path(input)?;
268 }
269
270 TaskType::Transfer {
271 source,
272 destination,
273 ..
274 } => {
275 if source.is_empty() {
276 return Err(WorkflowError::InvalidConfiguration(
277 "Transfer source cannot be empty".to_string(),
278 ));
279 }
280 if destination.is_empty() {
281 return Err(WorkflowError::InvalidConfiguration(
282 "Transfer destination cannot be empty".to_string(),
283 ));
284 }
285 }
286
287 TaskType::Notification { message, .. } => {
288 if message.is_empty() {
289 return Err(WorkflowError::InvalidConfiguration(
290 "Notification message cannot be empty".to_string(),
291 ));
292 }
293 }
294
295 TaskType::CustomScript { script, .. } => {
296 validate_file_path(script)?;
297 }
298
299 TaskType::Analysis { input, .. } => {
300 validate_file_path(input)?;
301 }
302
303 TaskType::HttpRequest { url, .. } => {
304 if url.is_empty() {
305 return Err(WorkflowError::InvalidConfiguration(
306 "HTTP URL cannot be empty".to_string(),
307 ));
308 }
309 }
310
311 TaskType::Wait { duration } => {
312 if duration.is_zero() {
313 return Err(WorkflowError::InvalidConfiguration(
314 "Wait duration cannot be zero".to_string(),
315 ));
316 }
317 }
318
319 TaskType::Conditional { condition, .. } => {
320 if condition.is_empty() {
321 return Err(WorkflowError::InvalidConfiguration(
322 "Conditional expression cannot be empty".to_string(),
323 ));
324 }
325 }
326 }
327
328 Ok(())
329}
330
331fn validate_file_path(path: &Path) -> Result<()> {
332 let path_str = path.to_string_lossy();
333 if path_str.is_empty() {
334 return Err(WorkflowError::InvalidConfiguration(
335 "File path cannot be empty".to_string(),
336 ));
337 }
338
339 if path_str.contains("..") {
341 return Err(WorkflowError::InvalidConfiguration(
342 "Path traversal detected".to_string(),
343 ));
344 }
345
346 Ok(())
347}
348
349pub struct TaskValidator;
351
352impl TaskValidator {
353 pub fn validate(task: &Task) -> Result<()> {
355 if task.name.is_empty() {
357 return Err(WorkflowError::InvalidConfiguration(
358 "Task name cannot be empty".to_string(),
359 ));
360 }
361
362 validate_task_type(&task.task_type)?;
364
365 if task.timeout.is_zero() {
367 return Err(WorkflowError::InvalidConfiguration(
368 "Task timeout cannot be zero".to_string(),
369 ));
370 }
371
372 if task.retry.max_attempts == 0 {
374 return Err(WorkflowError::InvalidConfiguration(
375 "Retry max_attempts must be at least 1".to_string(),
376 ));
377 }
378
379 Ok(())
380 }
381}
382
383pub struct ComplexityAnalyzer;
385
386impl ComplexityAnalyzer {
387 #[must_use]
389 pub fn analyze(workflow: &Workflow) -> ComplexityMetrics {
390 let task_count = workflow.tasks.len();
391 let edge_count = workflow.edges.len();
392
393 let depth = Self::calculate_depth(workflow);
395
396 let width = Self::calculate_width(workflow);
398
399 let branching_factor = if task_count > 0 {
401 edge_count as f64 / task_count as f64
402 } else {
403 0.0
404 };
405
406 let cyclomatic_complexity = edge_count.saturating_sub(task_count) + 2;
408
409 ComplexityMetrics {
410 task_count,
411 edge_count,
412 depth,
413 width,
414 branching_factor,
415 cyclomatic_complexity,
416 }
417 }
418
419 fn calculate_depth(workflow: &Workflow) -> usize {
420 let roots = workflow.get_root_tasks();
421 let mut max_depth = 0;
422
423 for root in roots {
424 let depth = Self::dfs_depth(workflow, root, &mut HashSet::new());
425 max_depth = max_depth.max(depth);
426 }
427
428 max_depth
429 }
430
431 fn dfs_depth(
432 workflow: &Workflow,
433 task_id: crate::task::TaskId,
434 visited: &mut HashSet<crate::task::TaskId>,
435 ) -> usize {
436 if visited.contains(&task_id) {
437 return 0;
438 }
439
440 visited.insert(task_id);
441
442 let dependents = workflow.get_dependents(&task_id);
443 if dependents.is_empty() {
444 return 1;
445 }
446
447 let mut max_depth = 0;
448 for dep in dependents {
449 let depth = Self::dfs_depth(workflow, dep, visited);
450 max_depth = max_depth.max(depth);
451 }
452
453 max_depth + 1
454 }
455
456 fn calculate_width(workflow: &Workflow) -> usize {
457 let roots = workflow.get_root_tasks();
459 let mut levels: HashMap<crate::task::TaskId, usize> = HashMap::new();
460 let mut level_counts: HashMap<usize, usize> = HashMap::new();
461
462 for root in roots {
463 Self::assign_levels(workflow, root, 0, &mut levels);
464 }
465
466 for level in levels.values() {
467 *level_counts.entry(*level).or_insert(0) += 1;
468 }
469
470 level_counts.values().max().copied().unwrap_or(0)
471 }
472
473 fn assign_levels(
474 workflow: &Workflow,
475 task_id: crate::task::TaskId,
476 level: usize,
477 levels: &mut HashMap<crate::task::TaskId, usize>,
478 ) {
479 if let Some(&existing_level) = levels.get(&task_id) {
480 if level <= existing_level {
481 return;
482 }
483 }
484
485 levels.insert(task_id, level);
486
487 for dep in workflow.get_dependents(&task_id) {
488 Self::assign_levels(workflow, dep, level + 1, levels);
489 }
490 }
491}
492
493#[derive(Debug, Clone)]
495pub struct ComplexityMetrics {
496 pub task_count: usize,
498 pub edge_count: usize,
500 pub depth: usize,
502 pub width: usize,
504 pub branching_factor: f64,
506 pub cyclomatic_complexity: usize,
508}
509
510impl ComplexityMetrics {
511 #[must_use]
513 pub fn score(&self) -> f64 {
514 let task_score = (self.task_count as f64 / 100.0).min(1.0) * 25.0;
515 let depth_score = (self.depth as f64 / 10.0).min(1.0) * 25.0;
516 let branching_score = (self.branching_factor / 3.0).min(1.0) * 25.0;
517 let cyclomatic_score = (self.cyclomatic_complexity as f64 / 20.0).min(1.0) * 25.0;
518
519 task_score + depth_score + branching_score + cyclomatic_score
520 }
521
522 #[must_use]
524 pub fn level(&self) -> ComplexityLevel {
525 let score = self.score();
526 if score < 25.0 {
527 ComplexityLevel::Low
528 } else if score < 50.0 {
529 ComplexityLevel::Medium
530 } else if score < 75.0 {
531 ComplexityLevel::High
532 } else {
533 ComplexityLevel::VeryHigh
534 }
535 }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum ComplexityLevel {
541 Low,
543 Medium,
545 High,
547 VeryHigh,
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use std::time::Duration;
555
556 fn create_test_task(name: &str) -> Task {
557 Task::new(
558 name,
559 TaskType::Wait {
560 duration: Duration::from_secs(1),
561 },
562 )
563 }
564
565 #[test]
566 fn test_workflow_validator() {
567 let validator = WorkflowValidator::new().with_defaults();
568 let workflow = Workflow::new("test");
569
570 let report = validator
571 .validate(&workflow)
572 .expect("should succeed in test");
573 assert!(report.is_valid());
574 }
575
576 #[test]
577 fn test_cycle_detection() {
578 let mut workflow = Workflow::new("test");
579 let task1 = create_test_task("task1");
580 let task2 = create_test_task("task2");
581
582 let id1 = workflow.add_task(task1);
583 let id2 = workflow.add_task(task2);
584
585 workflow.add_edge(id1, id2).expect("should succeed in test");
586 workflow.add_edge(id2, id1).expect("should succeed in test");
587
588 let rule = NoCyclesRule;
589 assert!(rule.validate(&workflow).is_err());
590 }
591
592 #[test]
593 fn test_valid_edges_rule() {
594 let mut workflow = Workflow::new("test");
595 let task = create_test_task("task1");
596 workflow.add_task(task);
597
598 let rule = ValidEdgesRule;
599 assert!(rule.validate(&workflow).is_ok());
600 }
601
602 #[test]
603 fn test_task_validator() {
604 let task = create_test_task("test-task");
605 assert!(TaskValidator::validate(&task).is_ok());
606 }
607
608 #[test]
609 fn test_task_validator_empty_name() {
610 let task = create_test_task("");
611 assert!(TaskValidator::validate(&task).is_err());
612 }
613
614 #[test]
615 fn test_complexity_analyzer() {
616 let mut workflow = Workflow::new("test");
617 let task1 = create_test_task("task1");
618 let task2 = create_test_task("task2");
619 let task3 = create_test_task("task3");
620
621 let id1 = workflow.add_task(task1);
622 let id2 = workflow.add_task(task2);
623 let id3 = workflow.add_task(task3);
624
625 workflow.add_edge(id1, id2).expect("should succeed in test");
626 workflow.add_edge(id2, id3).expect("should succeed in test");
627
628 let metrics = ComplexityAnalyzer::analyze(&workflow);
629
630 assert_eq!(metrics.task_count, 3);
631 assert_eq!(metrics.edge_count, 2);
632 assert_eq!(metrics.depth, 3);
633 }
634
635 #[test]
636 fn test_complexity_score() {
637 let metrics = ComplexityMetrics {
638 task_count: 10,
639 edge_count: 12,
640 depth: 5,
641 width: 3,
642 branching_factor: 1.2,
643 cyclomatic_complexity: 4,
644 };
645
646 let score = metrics.score();
647 assert!(score > 0.0 && score <= 100.0);
648 }
649
650 #[test]
651 fn test_complexity_level() {
652 let low_metrics = ComplexityMetrics {
653 task_count: 2,
654 edge_count: 1,
655 depth: 2,
656 width: 1,
657 branching_factor: 0.5,
658 cyclomatic_complexity: 1,
659 };
660
661 assert_eq!(low_metrics.level(), ComplexityLevel::Low);
662 }
663
664 #[test]
665 fn test_validation_report() {
666 let mut report = ValidationReport::new();
667 assert!(report.is_valid());
668
669 report.add_error("Test error".to_string());
670 assert!(!report.is_valid());
671 assert_eq!(report.error_count(), 1);
672
673 report.add_warning("Test warning".to_string());
674 assert_eq!(report.warning_count(), 1);
675 }
676}