1use std::fmt::Debug;
5use std::hash::Hash;
6use thiserror::Error;
7
8#[derive(Debug, Error, PartialEq, Eq)]
9pub enum ToposortError<N: Debug> {
10 #[error("dependency cycle detected: {0:?}")]
11 Cycle(Vec<N>),
12}
13
14pub fn sort<N, F>(nodes: &[N], deps: F) -> Result<Vec<N>, ToposortError<N>>
19where
20 N: Clone + Eq + Hash + Debug,
21 F: Fn(&N) -> Vec<N>,
22{
23 if nodes.is_empty() {
24 return Ok(Vec::new());
25 }
26
27 let mut indegree: std::collections::HashMap<N, usize> =
28 nodes.iter().map(|n| (n.clone(), 0)).collect();
29 let mut dependents: std::collections::HashMap<N, Vec<N>> = std::collections::HashMap::new();
30
31 for n in nodes {
32 for d in deps(n) {
33 if !indegree.contains_key(&d) {
34 continue;
35 }
36 *indegree.get_mut(n).unwrap() += 1;
37 dependents.entry(d).or_default().push(n.clone());
38 }
39 }
40
41 let mut queue: std::collections::VecDeque<N> =
42 nodes.iter().filter(|n| indegree[n] == 0).cloned().collect();
43
44 let mut result = Vec::with_capacity(nodes.len());
45
46 while let Some(n) = queue.pop_front() {
47 result.push(n.clone());
48
49 if let Some(deps_of_n) = dependents.get(&n) {
50 for dependent in deps_of_n.clone() {
51 let entry = indegree.get_mut(&dependent).unwrap();
52 *entry -= 1;
53 if *entry == 0 {
54 queue.push_back(dependent);
55 }
56 }
57 }
58 }
59
60 if result.len() != nodes.len() {
61 let cycle: Vec<N> = nodes.iter().filter(|n| indegree[n] > 0).cloned().collect();
62 return Err(ToposortError::Cycle(cycle));
63 }
64
65 Ok(result)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use std::collections::HashMap;
72
73 #[test]
75 fn sort_fixed_order_cases() {
76 struct Case {
77 name: &'static str,
78 nodes: Vec<&'static str>,
79 deps: HashMap<&'static str, Vec<&'static str>>,
80 want: Vec<&'static str>,
81 }
82
83 let cases = vec![
84 Case {
85 name: "dependency comes first",
86 nodes: vec!["app", "db"],
87 deps: HashMap::from([("app", vec!["db"])]),
88 want: vec!["db", "app"],
89 },
90 Case {
91 name: "chain is ordered end to end",
92 nodes: vec!["c", "b", "a"],
93 deps: HashMap::from([("c", vec!["b"]), ("b", vec!["a"])]),
94 want: vec!["a", "b", "c"],
95 },
96 Case {
97 name: "dependency outside the node set is ignored",
98 nodes: vec!["a", "b"],
99 deps: HashMap::from([("a", vec!["elsewhere"]), ("b", vec!["a"])]),
100 want: vec!["a", "b"],
101 },
102 ];
103
104 for case in cases {
105 let got = sort(&case.nodes, |n| {
106 case.deps.get(n).cloned().unwrap_or_default()
107 })
108 .unwrap_or_else(|e| panic!("{}: {e}", case.name));
109 assert_eq!(got, case.want, "{}", case.name);
110 }
111 }
112
113 #[test]
115 fn sort_independent_nodes_all_come_through() {
116 let nodes = vec!["c", "a", "b"];
117 let mut got = sort(&nodes, |_| Vec::new()).unwrap();
118 got.sort_unstable();
119 let mut want = nodes.clone();
120 want.sort_unstable();
121 assert_eq!(got, want);
122 }
123
124 #[test]
126 fn sort_diamond_resolves_branches_before_tip() {
127 let nodes = vec!["tip", "left", "right", "base"];
128 let deps: HashMap<&str, Vec<&str>> = HashMap::from([
129 ("tip", vec!["left", "right"]),
130 ("left", vec!["base"]),
131 ("right", vec!["base"]),
132 ]);
133
134 let got = sort(&nodes, |n| deps.get(n).cloned().unwrap_or_default()).unwrap();
135 assert_eq!(got.len(), nodes.len());
136 assert_eq!(got[0], "base");
137 assert_eq!(got[3], "tip");
138 let mut middle = got[1..3].to_vec();
139 middle.sort_unstable();
140 assert_eq!(middle, vec!["left", "right"]);
141 }
142
143 #[test]
145 fn sort_cycle_names_its_nodes() {
146 let nodes = vec!["alpha", "beta", "gamma"];
147 let deps: HashMap<&str, Vec<&str>> =
148 HashMap::from([("alpha", vec!["beta"]), ("beta", vec!["alpha"])]);
149
150 let err = sort(&nodes, |n| deps.get(n).cloned().unwrap_or_default()).unwrap_err();
151 let msg = err.to_string();
152 assert!(msg.contains("alpha"));
153 assert!(msg.contains("beta"));
154 assert!(
155 !msg.contains("gamma"),
156 "a node outside the cycle is not blamed"
157 );
158 }
159
160 #[test]
162 fn sort_self_dependency_is_a_cycle() {
163 let nodes = vec!["a"];
164 let err = sort(&nodes, |_| vec!["a"]).unwrap_err();
165 assert!(matches!(err, ToposortError::Cycle(_)));
166 }
167}