use anyhow::{bail, Result};
use std::collections::HashMap;
use crate::config::FlowDef;
#[derive(Debug)]
pub struct ValidatedFlow {
pub deps: Vec<Vec<usize>>,
}
pub fn validate(flow: &FlowDef) -> Result<ValidatedFlow> {
let mut id_to_idx: HashMap<&str, usize> = HashMap::new();
for (i, step) in flow.steps.iter().enumerate() {
if let Some(prev) = id_to_idx.insert(&step.id, i) {
bail!(
"Duplicate step ID '{}' (steps {} and {})",
step.id,
prev,
i
);
}
}
let mut deps: Vec<Vec<usize>> = Vec::with_capacity(flow.steps.len());
for step in &flow.steps {
let mut step_deps = Vec::new();
for dep_id in &step.depends_on {
match id_to_idx.get(dep_id.as_str()) {
Some(&idx) => step_deps.push(idx),
None => bail!(
"Step '{}' depends on unknown step '{}'",
step.id,
dep_id
),
}
}
deps.push(step_deps);
}
detect_cycle(flow, &deps)?;
Ok(ValidatedFlow { deps })
}
fn detect_cycle(flow: &FlowDef, deps: &[Vec<usize>]) -> Result<()> {
let n = flow.steps.len();
let mut color = vec![0u8; n]; let mut path: Vec<usize> = Vec::new();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for (step, step_deps) in deps.iter().enumerate() {
for &dep in step_deps {
adj[dep].push(step);
}
}
for start in 0..n {
if color[start] == 0 {
dfs_visit(start, &adj, &mut color, &mut path, flow)?;
}
}
Ok(())
}
fn dfs_visit(
node: usize,
adj: &[Vec<usize>],
color: &mut [u8],
path: &mut Vec<usize>,
flow: &FlowDef,
) -> Result<()> {
color[node] = 1; path.push(node);
for &next in &adj[node] {
if color[next] == 1 {
let cycle_start = path.iter().position(|&x| x == next).unwrap();
let cycle_names: Vec<&str> = path[cycle_start..]
.iter()
.map(|&i| flow.steps[i].id.as_str())
.collect();
bail!(
"Cycle detected: {} → {}",
cycle_names.join(" → "),
flow.steps[next].id
);
}
if color[next] == 0 {
dfs_visit(next, adj, color, path, flow)?;
}
}
path.pop();
color[node] = 2; Ok(())
}