1use crate::error::{Result, WorkflowError};
4use crate::task::TaskType;
5use crate::workflow::Workflow;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10pub fn parse_duration(s: &str) -> Result<Duration> {
12 let s = s.trim();
13
14 if let Ok(secs) = s.parse::<u64>() {
16 return Ok(Duration::from_secs(secs));
17 }
18
19 let mut total_secs = 0u64;
21 let mut current_num = String::new();
22
23 for ch in s.chars() {
24 if ch.is_ascii_digit() || ch == '.' {
25 current_num.push(ch);
26 } else {
27 if current_num.is_empty() {
28 continue;
29 }
30
31 let value: f64 = current_num.parse().map_err(|_| {
32 WorkflowError::InvalidConfiguration(format!("Invalid duration: {s}"))
33 })?;
34
35 let unit_secs = match ch {
36 's' => 1,
37 'm' => 60,
38 'h' => 3600,
39 'd' => 86400,
40 _ => {
41 return Err(WorkflowError::InvalidConfiguration(format!(
42 "Invalid duration unit: {ch}"
43 )))
44 }
45 };
46
47 total_secs += (value * f64::from(unit_secs)) as u64;
48 current_num.clear();
49 }
50 }
51
52 if total_secs == 0 {
53 return Err(WorkflowError::InvalidConfiguration(format!(
54 "Invalid duration: {s}"
55 )));
56 }
57
58 Ok(Duration::from_secs(total_secs))
59}
60
61#[must_use]
63pub fn format_duration(duration: Duration) -> String {
64 let secs = duration.as_secs();
65
66 if secs < 60 {
67 return format!("{secs}s");
68 }
69
70 if secs < 3600 {
71 let mins = secs / 60;
72 let secs = secs % 60;
73 if secs == 0 {
74 return format!("{mins}m");
75 }
76 return format!("{mins}m{secs}s");
77 }
78
79 if secs < 86400 {
80 let hours = secs / 3600;
81 let mins = (secs % 3600) / 60;
82 if mins == 0 {
83 return format!("{hours}h");
84 }
85 return format!("{hours}h{mins}m");
86 }
87
88 let days = secs / 86400;
89 let hours = (secs % 86400) / 3600;
90 if hours == 0 {
91 return format!("{days}d");
92 }
93 format!("{days}d{hours}h")
94}
95
96#[must_use]
98pub fn sanitize_task_name(name: &str) -> String {
99 name.chars()
100 .map(|c| {
101 if c.is_alphanumeric() || c == '-' || c == '_' {
102 c
103 } else {
104 '_'
105 }
106 })
107 .collect()
108}
109
110#[must_use]
112pub fn generate_task_name(prefix: &str, index: usize) -> String {
113 format!("{}-{}", sanitize_task_name(prefix), index)
114}
115
116#[must_use]
118pub fn expand_env_vars(s: &str) -> String {
119 let mut result = s.to_string();
120
121 for (key, value) in std::env::vars() {
122 let placeholder = format!("${{{key}}}");
123 result = result.replace(&placeholder, &value);
124
125 let placeholder = format!("${key}");
126 result = result.replace(&placeholder, &value);
127 }
128
129 result
130}
131
132#[must_use]
134pub fn expand_template(template: &str, variables: &HashMap<String, String>) -> String {
135 let mut result = template.to_string();
136
137 for (key, value) in variables {
138 let placeholder = format!("{{{key}}}");
139 result = result.replace(&placeholder, value);
140
141 let placeholder = format!("${{{key}}}");
142 result = result.replace(&placeholder, value);
143 }
144
145 result
146}
147
148#[must_use]
150pub fn estimate_workflow_duration(workflow: &Workflow) -> Duration {
151 let mut max_duration = Duration::ZERO;
152
153 if let Ok(sorted_tasks) = workflow.topological_sort() {
155 let mut task_end_times: HashMap<crate::task::TaskId, Duration> = HashMap::new();
156
157 for &task_id in &sorted_tasks {
158 if let Some(task) = workflow.get_task(&task_id) {
159 let mut start_time = Duration::ZERO;
162
163 let edge_deps = workflow.get_dependencies(&task_id);
164 let all_deps: Vec<crate::task::TaskId> = if edge_deps.is_empty() {
165 task.dependencies.clone()
166 } else {
167 edge_deps
168 };
169
170 for dep_id in &all_deps {
171 if let Some(&dep_end_time) = task_end_times.get(dep_id) {
172 start_time = start_time.max(dep_end_time);
173 }
174 }
175
176 let end_time = start_time + task.timeout;
178 task_end_times.insert(task_id, end_time);
179
180 max_duration = max_duration.max(end_time);
181 }
182 }
183 }
184
185 max_duration
186}
187
188#[must_use]
190pub fn find_critical_path(workflow: &Workflow) -> Vec<crate::task::TaskId> {
191 let mut task_durations: HashMap<_, _> = HashMap::new();
192 let mut task_paths: HashMap<_, Vec<_>> = HashMap::new();
193
194 if let Ok(sorted_tasks) = workflow.topological_sort() {
195 for &task_id in &sorted_tasks {
196 if let Some(task) = workflow.get_task(&task_id) {
197 let deps = workflow.get_dependencies(&task_id);
198
199 if deps.is_empty() {
200 task_durations.insert(task_id, task.timeout);
201 task_paths.insert(task_id, vec![task_id]);
202 } else {
203 let mut max_duration = Duration::ZERO;
204 let mut max_path = Vec::new();
205
206 for dep_id in deps {
207 if let Some(&dep_duration) = task_durations.get(&dep_id) {
208 let total_duration = dep_duration + task.timeout;
209 if total_duration > max_duration {
210 max_duration = total_duration;
211 max_path = task_paths.get(&dep_id).cloned().unwrap_or_default();
212 }
213 }
214 }
215
216 max_path.push(task_id);
217 task_durations.insert(task_id, max_duration);
218 task_paths.insert(task_id, max_path);
219 }
220 }
221 }
222
223 let mut critical_path = Vec::new();
225 let mut max_duration = Duration::ZERO;
226
227 for (task_id, &duration) in &task_durations {
228 if duration > max_duration {
229 max_duration = duration;
230 critical_path = task_paths.get(task_id).cloned().unwrap_or_default();
231 }
232 }
233
234 return critical_path;
235 }
236
237 Vec::new()
238}
239
240#[must_use]
242pub fn merge_configs(
243 base: &crate::workflow::WorkflowConfig,
244 override_config: &crate::workflow::WorkflowConfig,
245) -> crate::workflow::WorkflowConfig {
246 let mut merged = base.clone();
247
248 if override_config.max_concurrent_tasks != base.max_concurrent_tasks {
249 merged.max_concurrent_tasks = override_config.max_concurrent_tasks;
250 }
251
252 if override_config.global_timeout.is_some() {
253 merged.global_timeout = override_config.global_timeout;
254 }
255
256 merged.fail_fast = override_config.fail_fast || base.fail_fast;
257 merged.continue_on_error = override_config.continue_on_error || base.continue_on_error;
258
259 for (key, value) in &override_config.variables {
261 merged.variables.insert(key.clone(), value.clone());
262 }
263
264 merged
265}
266
267#[must_use]
269pub fn clone_workflow(workflow: &Workflow, new_name: Option<String>) -> Workflow {
270 let mut cloned = Workflow::new(new_name.unwrap_or_else(|| format!("{}-copy", workflow.name)));
271 cloned.description = workflow.description.clone();
272 cloned.config = workflow.config.clone();
273 cloned.metadata = workflow.metadata.clone();
274
275 let mut id_map = HashMap::new();
277
278 for task in workflow.tasks.values() {
279 let mut new_task = task.clone();
280 let old_id = new_task.id;
281 new_task.id = crate::task::TaskId::new();
282 id_map.insert(old_id, new_task.id);
283 cloned.tasks.insert(new_task.id, new_task);
284 }
285
286 for edge in &workflow.edges {
288 if let (Some(&from_id), Some(&to_id)) = (id_map.get(&edge.from), id_map.get(&edge.to)) {
289 cloned.edges.push(crate::workflow::Edge {
290 from: from_id,
291 to: to_id,
292 condition: edge.condition.clone(),
293 });
294 }
295 }
296
297 cloned
298}
299
300pub fn normalize_paths(workflow: &mut Workflow, base_path: &Path) -> Result<()> {
302 for task in workflow.tasks.values_mut() {
303 match &mut task.task_type {
304 TaskType::Transcode { input, output, .. } => {
305 *input = normalize_path(input, base_path);
306 *output = normalize_path(output, base_path);
307 }
308 TaskType::QualityControl { input, .. } => {
309 *input = normalize_path(input, base_path);
310 }
311 TaskType::Analysis { input, output, .. } => {
312 *input = normalize_path(input, base_path);
313 if let Some(out) = output {
314 *out = normalize_path(out, base_path);
315 }
316 }
317 TaskType::CustomScript { script, .. } => {
318 *script = normalize_path(script, base_path);
319 }
320 _ => {}
321 }
322 }
323
324 Ok(())
325}
326
327fn normalize_path(path: &Path, base_path: &Path) -> PathBuf {
328 if path.is_absolute() {
329 path.to_path_buf()
330 } else {
331 base_path.join(path)
332 }
333}
334
335#[must_use]
337pub fn calculate_parallelism(workflow: &Workflow) -> Vec<Vec<crate::task::TaskId>> {
338 let mut levels: Vec<Vec<crate::task::TaskId>> = Vec::new();
339
340 if let Ok(sorted_tasks) = workflow.topological_sort() {
341 let mut task_levels: HashMap<_, _> = HashMap::new();
342
343 for &task_id in &sorted_tasks {
344 let deps = workflow.get_dependencies(&task_id);
345
346 if deps.is_empty() {
347 task_levels.insert(task_id, 0);
348 } else {
349 let mut max_level = 0;
350 for dep_id in deps {
351 if let Some(&level) = task_levels.get(&dep_id) {
352 max_level = max_level.max(level + 1);
353 }
354 }
355 task_levels.insert(task_id, max_level);
356 }
357 }
358
359 let max_level = task_levels.values().max().copied().unwrap_or(0);
361 levels = vec![Vec::new(); max_level + 1];
362
363 for (task_id, level) in task_levels {
364 levels[level].push(task_id);
365 }
366 }
367
368 levels
369}
370
371#[must_use]
373pub fn get_workflow_statistics(workflow: &Workflow) -> WorkflowStatistics {
374 let task_count = workflow.tasks.len();
375 let edge_count = workflow.edges.len();
376
377 let mut task_type_counts: HashMap<String, usize> = HashMap::new();
378
379 for task in workflow.tasks.values() {
380 let type_name = match &task.task_type {
381 TaskType::Transcode { .. } => "Transcode",
382 TaskType::QualityControl { .. } => "QualityControl",
383 TaskType::Transfer { .. } => "Transfer",
384 TaskType::Notification { .. } => "Notification",
385 TaskType::CustomScript { .. } => "CustomScript",
386 TaskType::Analysis { .. } => "Analysis",
387 TaskType::Conditional { .. } => "Conditional",
388 TaskType::Wait { .. } => "Wait",
389 TaskType::HttpRequest { .. } => "HttpRequest",
390 };
391
392 *task_type_counts.entry(type_name.to_string()).or_insert(0) += 1;
393 }
394
395 let root_count = workflow.get_root_tasks().len();
396 let leaf_count = workflow.get_leaf_tasks().len();
397
398 let estimated_duration = estimate_workflow_duration(workflow);
399 let critical_path = find_critical_path(workflow);
400
401 WorkflowStatistics {
402 task_count,
403 edge_count,
404 root_task_count: root_count,
405 leaf_task_count: leaf_count,
406 task_type_counts,
407 estimated_duration,
408 critical_path_length: critical_path.len(),
409 }
410}
411
412#[derive(Debug, Clone)]
414pub struct WorkflowStatistics {
415 pub task_count: usize,
417 pub edge_count: usize,
419 pub root_task_count: usize,
421 pub leaf_task_count: usize,
423 pub task_type_counts: HashMap<String, usize>,
425 pub estimated_duration: Duration,
427 pub critical_path_length: usize,
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::task::Task;
435
436 #[test]
437 fn test_parse_duration() {
438 assert_eq!(
439 parse_duration("60").expect("should succeed in test"),
440 Duration::from_secs(60)
441 );
442 assert_eq!(
443 parse_duration("1m").expect("should succeed in test"),
444 Duration::from_secs(60)
445 );
446 assert_eq!(
447 parse_duration("1h").expect("should succeed in test"),
448 Duration::from_secs(3600)
449 );
450 assert_eq!(
451 parse_duration("1h30m").expect("should succeed in test"),
452 Duration::from_secs(5400)
453 );
454 assert_eq!(
455 parse_duration("1d").expect("should succeed in test"),
456 Duration::from_secs(86400)
457 );
458 }
459
460 #[test]
461 fn test_format_duration() {
462 assert_eq!(format_duration(Duration::from_secs(30)), "30s");
463 assert_eq!(format_duration(Duration::from_secs(60)), "1m");
464 assert_eq!(format_duration(Duration::from_secs(90)), "1m30s");
465 assert_eq!(format_duration(Duration::from_secs(3600)), "1h");
466 assert_eq!(format_duration(Duration::from_secs(3660)), "1h1m");
467 }
468
469 #[test]
470 fn test_sanitize_task_name() {
471 assert_eq!(sanitize_task_name("hello world"), "hello_world");
472 assert_eq!(sanitize_task_name("test-task"), "test-task");
473 assert_eq!(sanitize_task_name("test@task!"), "test_task_");
474 }
475
476 #[test]
477 fn test_generate_task_name() {
478 assert_eq!(generate_task_name("test", 1), "test-1");
479 assert_eq!(generate_task_name("my task", 5), "my_task-5");
480 }
481
482 #[test]
483 fn test_expand_template() {
484 let mut vars = HashMap::new();
485 vars.insert("name".to_string(), "world".to_string());
486 vars.insert("num".to_string(), "42".to_string());
487
488 let result = expand_template("Hello {name}, answer is {num}", &vars);
489 assert_eq!(result, "Hello world, answer is 42");
490 }
491
492 #[test]
493 fn test_estimate_workflow_duration() {
494 let mut workflow = Workflow::new("test");
495 let task1 = Task::new(
496 "task1",
497 TaskType::Wait {
498 duration: Duration::from_secs(10),
499 },
500 )
501 .with_timeout(Duration::from_secs(10));
502
503 let task2 = Task::new(
504 "task2",
505 TaskType::Wait {
506 duration: Duration::from_secs(20),
507 },
508 )
509 .with_timeout(Duration::from_secs(20));
510
511 let id1 = workflow.add_task(task1);
512 let id2 = workflow.add_task(task2);
513 workflow.add_edge(id1, id2).expect("should succeed in test");
514
515 let duration = estimate_workflow_duration(&workflow);
516 assert_eq!(duration, Duration::from_secs(30));
517 }
518
519 #[test]
520 fn test_clone_workflow() {
521 let mut workflow = Workflow::new("original");
522 let task = Task::new(
523 "task1",
524 TaskType::Wait {
525 duration: Duration::from_secs(1),
526 },
527 );
528 workflow.add_task(task);
529
530 let cloned = clone_workflow(&workflow, Some("cloned".to_string()));
531
532 assert_eq!(cloned.name, "cloned");
533 assert_eq!(cloned.tasks.len(), workflow.tasks.len());
534 assert_ne!(cloned.id, workflow.id);
535 }
536
537 #[test]
538 fn test_calculate_parallelism() {
539 let mut workflow = Workflow::new("test");
540
541 let task1 = Task::new(
542 "task1",
543 TaskType::Wait {
544 duration: Duration::from_secs(1),
545 },
546 );
547 let task2 = Task::new(
548 "task2",
549 TaskType::Wait {
550 duration: Duration::from_secs(1),
551 },
552 );
553 let task3 = Task::new(
554 "task3",
555 TaskType::Wait {
556 duration: Duration::from_secs(1),
557 },
558 );
559
560 let id1 = workflow.add_task(task1);
561 let id2 = workflow.add_task(task2);
562 let id3 = workflow.add_task(task3);
563
564 workflow.add_edge(id1, id3).expect("should succeed in test");
565 workflow.add_edge(id2, id3).expect("should succeed in test");
566
567 let levels = calculate_parallelism(&workflow);
568 assert_eq!(levels.len(), 2);
569 assert_eq!(levels[0].len(), 2); assert_eq!(levels[1].len(), 1); }
572
573 #[test]
574 fn test_get_workflow_statistics() {
575 let mut workflow = Workflow::new("test");
576 let task = Task::new(
577 "task1",
578 TaskType::Wait {
579 duration: Duration::from_secs(1),
580 },
581 );
582 workflow.add_task(task);
583
584 let stats = get_workflow_statistics(&workflow);
585 assert_eq!(stats.task_count, 1);
586 assert_eq!(stats.edge_count, 0);
587 }
588
589 #[test]
590 fn test_merge_configs() {
591 let base = crate::workflow::WorkflowConfig {
592 max_concurrent_tasks: 2,
593 ..Default::default()
594 };
595
596 let override_config = crate::workflow::WorkflowConfig {
597 max_concurrent_tasks: 4,
598 fail_fast: true,
599 ..Default::default()
600 };
601
602 let merged = merge_configs(&base, &override_config);
603 assert_eq!(merged.max_concurrent_tasks, 4);
604 assert!(merged.fail_fast);
605 }
606}