use super::*;
pub(super) fn build_task_index(
config: &OrchestrationConfig,
) -> Result<HashMap<String, usize>, String> {
let mut index = HashMap::new();
for (idx, task) in config.tasks.iter().enumerate() {
if index.insert(task.name.clone(), idx).is_some() {
return Err(format!("duplicate task name: {}", task.name));
}
}
Ok(index)
}
pub(super) fn topological_order(
config: &OrchestrationConfig,
target: Option<&str>,
) -> Result<Vec<String>, String> {
let index = build_task_index(config)?;
let selected: HashSet<String> = if let Some(target_name) = target {
if !index.contains_key(target_name) {
return Err(format!("target task not found: {target_name}"));
}
collect_target_subset(config, &index, target_name)?
} else {
config.tasks.iter().map(|task| task.name.clone()).collect()
};
let mut indegree: HashMap<String, usize> =
selected.iter().map(|name| (name.clone(), 0usize)).collect();
let mut adjacency: HashMap<String, Vec<String>> = selected
.iter()
.map(|name| (name.clone(), Vec::new()))
.collect();
for task in &config.tasks {
if !selected.contains(&task.name) {
continue;
}
for dep in &task.depends_on {
if selected.contains(dep) {
let value = indegree
.get_mut(&task.name)
.ok_or_else(|| format!("internal error: missing indegree for {}", task.name))?;
*value += 1;
adjacency
.get_mut(dep)
.ok_or_else(|| format!("internal error: missing adjacency for {dep}"))?
.push(task.name.clone());
}
}
}
let order_index: HashMap<String, usize> = config
.tasks
.iter()
.enumerate()
.map(|(idx, task)| (task.name.clone(), idx))
.collect();
let mut ready: VecDeque<String> = config
.tasks
.iter()
.filter(|task| selected.contains(&task.name))
.filter(|task| indegree.get(&task.name).copied().unwrap_or(0) == 0)
.map(|task| task.name.clone())
.collect();
let mut ordered = Vec::new();
while let Some(current) = ready.pop_front() {
ordered.push(current.clone());
if let Some(dependents) = adjacency.get(¤t) {
for next in dependents {
let value = indegree
.get_mut(next)
.ok_or_else(|| format!("internal error: missing indegree for {next}"))?;
*value = value.saturating_sub(1);
if *value == 0 && !ready.iter().any(|task| task == next) {
ready.push_back(next.clone());
}
}
}
let mut sorted = ready.into_iter().collect::<Vec<_>>();
sorted.sort_by_key(|name| order_index.get(name).copied().unwrap_or(usize::MAX));
ready = sorted.into();
}
if ordered.len() != selected.len() {
return Err(String::from(
"dependency cycle detected in orchestrate config tasks",
));
}
Ok(ordered)
}
pub(super) fn filtered_ordered_tasks(
config: &OrchestrationConfig,
target: Option<&str>,
group: Option<&str>,
) -> Result<Vec<String>, String> {
let full_order = topological_order(config, target)?;
if group.is_none() {
return Ok(full_order
.into_iter()
.filter(|name| {
let task = config.tasks.iter().find(|task| &task.name == name);
task.is_some_and(|task| task.enabled)
})
.collect());
}
let task_index = build_task_index(config)?;
let group_name = group.unwrap_or_default();
let mut selected = HashSet::new();
for name in &full_order {
let idx = task_index
.get(name)
.copied()
.ok_or_else(|| format!("internal error: task not found: {name}"))?;
let task = &config.tasks[idx];
if task.enabled && task.group.as_deref() == Some(group_name) {
selected.insert(task.name.clone());
collect_dependencies(&task.name, config, &task_index, &mut selected)?;
}
}
Ok(full_order
.into_iter()
.filter(|name| selected.contains(name))
.collect())
}
fn collect_dependencies(
task_name: &str,
config: &OrchestrationConfig,
task_index: &HashMap<String, usize>,
selected: &mut HashSet<String>,
) -> Result<(), String> {
let idx = task_index
.get(task_name)
.copied()
.ok_or_else(|| format!("task not found: {task_name}"))?;
let task = &config.tasks[idx];
for dep in &task.depends_on {
if selected.insert(dep.clone()) {
collect_dependencies(dep, config, task_index, selected)?;
}
}
Ok(())
}
fn collect_target_subset(
config: &OrchestrationConfig,
index: &HashMap<String, usize>,
target_name: &str,
) -> Result<HashSet<String>, String> {
let mut visited = HashSet::new();
let mut stack = vec![target_name.to_string()];
while let Some(current) = stack.pop() {
if !visited.insert(current.clone()) {
continue;
}
let idx = index
.get(¤t)
.copied()
.ok_or_else(|| format!("target task not found: {current}"))?;
let task = &config.tasks[idx];
for dep in &task.depends_on {
stack.push(dep.clone());
}
}
Ok(visited)
}