use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TopoError {
pub cycle: Vec<String>,
}
impl std::fmt::Display for TopoError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"dependency cycle detected among: {}",
self.cycle.join(", ")
)
}
}
impl std::error::Error for TopoError {}
pub(crate) fn topological_order(
edges: &BTreeMap<String, BTreeSet<String>>,
) -> Result<Vec<String>, TopoError> {
let mut in_degree: BTreeMap<String, usize> = BTreeMap::new();
let mut dependents: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (node, deps) in edges {
in_degree.entry(node.clone()).or_insert(0);
dependents.entry(node.clone()).or_default();
for dep in deps {
in_degree.entry(dep.clone()).or_insert(0);
dependents.entry(dep.clone()).or_default();
dependents.get_mut(dep).unwrap().insert(node.clone());
*in_degree.get_mut(node).unwrap() += 1;
}
}
let mut queue: BTreeSet<String> = in_degree
.iter()
.filter(|(_, deg)| **deg == 0)
.map(|(name, _)| name.clone())
.collect();
let mut result = Vec::with_capacity(in_degree.len());
let total = in_degree.len();
while let Some(node) = queue.iter().next().cloned() {
queue.remove(&node);
result.push(node.clone());
if let Some(deps) = dependents.get(&node) {
for dep in deps {
let deg = in_degree.get_mut(dep).unwrap();
*deg -= 1;
if *deg == 0 {
queue.insert(dep.clone());
}
}
}
}
if result.len() == total {
Ok(result)
} else {
let remaining: Vec<String> = in_degree
.iter()
.filter(|&(_, deg)| *deg > 0)
.map(|(name, _)| name.clone())
.collect();
Err(TopoError { cycle: remaining })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn graph(edges: &[(&str, &[&str])]) -> BTreeMap<String, BTreeSet<String>> {
edges
.iter()
.map(|(node, deps)| {
(
(*node).to_string(),
deps.iter().map(|d| (*d).to_string()).collect(),
)
})
.collect()
}
#[test]
fn empty_graph_produces_empty_order() {
let g: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
assert_eq!(topological_order(&g).unwrap(), Vec::<String>::new());
}
#[test]
fn single_node_no_deps() {
let g = graph(&[("a", &[])]);
assert_eq!(topological_order(&g).unwrap(), vec!["a"]);
}
#[test]
fn linear_chain() {
let g = graph(&[("a", &[]), ("b", &["a"]), ("c", &["b"])]);
assert_eq!(topological_order(&g).unwrap(), vec!["a", "b", "c"]);
}
#[test]
fn independent_nodes_sorted_by_name() {
let g = graph(&[("c", &[]), ("a", &[]), ("b", &[])]);
assert_eq!(topological_order(&g).unwrap(), vec!["a", "b", "c"]);
}
#[test]
fn diamond_dependency() {
let g = graph(&[("a", &[]), ("b", &["a"]), ("c", &["a"]), ("d", &["b", "c"])]);
let order = topological_order(&g).unwrap();
assert_eq!(order[0], "a");
assert_eq!(order[3], "d");
assert_eq!(order[1], "b");
assert_eq!(order[2], "c");
}
#[test]
fn self_cycle_detected() {
let g = graph(&[("a", &["a"])]);
let err = topological_order(&g).expect_err("self-cycle");
assert_eq!(err.cycle, vec!["a"]);
}
#[test]
fn two_node_cycle_detected() {
let g = graph(&[("a", &["b"]), ("b", &["a"])]);
let err = topological_order(&g).expect_err("cycle");
assert!(err.cycle.contains(&"a".to_string()));
assert!(err.cycle.contains(&"b".to_string()));
}
#[test]
fn three_node_cycle_detected() {
let g = graph(&[("a", &["c"]), ("b", &["a"]), ("c", &["b"])]);
let err = topological_order(&g).expect_err("cycle");
assert_eq!(err.cycle.len(), 3);
}
#[test]
fn deterministic_repeated() {
let g = graph(&[("z", &["a"]), ("a", &[]), ("m", &["a", "z"]), ("b", &["a"])]);
let first = topological_order(&g).unwrap();
let second = topological_order(&g).unwrap();
assert_eq!(first, second);
assert_eq!(first, vec!["a", "b", "z", "m"]);
}
#[test]
fn external_dependency_not_in_graph_is_ignored() {
let g = graph(&[("a", &["external"])]);
let order = topological_order(&g).unwrap();
assert!(order.contains(&"external".to_string()));
assert!(order.contains(&"a".to_string()));
}
}