robotrt-cli 0.1.0-beta.1

RobotRT modular robotics runtime and middleware components.
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(&current) {
            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(&current)
            .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)
}