use petgraph::algo::toposort;
use super::cycles::{CycleError, detect_cycles};
use super::{AgmGraph, depends_subgraph};
pub fn topological_sort(graph: &AgmGraph) -> Result<Vec<String>, CycleError> {
let sub = depends_subgraph(graph);
match toposort(&sub, None) {
Ok(indices) => {
let ids = indices
.into_iter()
.rev()
.map(|idx| sub[idx].clone())
.collect();
Ok(ids)
}
Err(_cycle) => {
let cycles = detect_cycles(graph);
let path = cycles.into_iter().next().unwrap_or_default();
Err(CycleError::new(path))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::build::build_graph;
use crate::graph::test_helpers::*;
#[test]
fn test_topological_sort_empty_graph_returns_empty() {
let graph = build_graph(&make_file(vec![]));
let result = topological_sort(&graph).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_topological_sort_dag_returns_valid_ordering() {
let mut a = make_node("a");
let mut b = make_node("b");
let c = make_node("c");
a.depends = Some(vec!["b".to_owned()]);
b.depends = Some(vec!["c".to_owned()]);
let graph = build_graph(&make_file(vec![a, b, c]));
let result = topological_sort(&graph).unwrap();
assert_eq!(result.len(), 3);
let pos_a = result.iter().position(|x| x == "a").unwrap();
let pos_b = result.iter().position(|x| x == "b").unwrap();
let pos_c = result.iter().position(|x| x == "c").unwrap();
assert!(pos_b < pos_a, "b should appear before a");
assert!(pos_c < pos_b, "c should appear before b");
}
#[test]
fn test_topological_sort_cycle_returns_cycle_error() {
let mut a = make_node("a");
let mut b = make_node("b");
a.depends = Some(vec!["b".to_owned()]);
b.depends = Some(vec!["a".to_owned()]);
let graph = build_graph(&make_file(vec![a, b]));
let err = topological_sort(&graph).unwrap_err();
let path = err.display_path();
assert!(
path.contains('a') && path.contains('b'),
"cycle path should mention both a and b, got: {path}"
);
}
}