ascii_dag/layout/generic.rs
1//! Generic topological sorting for any data structure.
2//!
3//! This module provides topological sorting that works with any graph-like
4//! structure via higher-order functions, similar to the generic cycle detection.
5//!
6//! ## Submodules
7//!
8//! - [`impact`] - Impact analysis (descendants, ancestors, blast radius)
9//! - [`metrics`] - Graph metrics and statistics
10//!
11//! # Examples
12//!
13//! ```
14//! use ascii_dag::layout::generic::topological_sort_fn;
15//!
16//! // Example: Sort tasks by dependencies
17//! let get_deps = |task: &&str| match *task {
18//! "deploy" => vec!["test", "build"],
19//! "test" => vec!["build"],
20//! "build" => vec!["compile"],
21//! "compile" => vec![],
22//! _ => vec![],
23//! };
24//!
25//! let tasks = ["deploy", "test", "build", "compile"];
26//! if let Ok(sorted) = topological_sort_fn(&tasks, get_deps) {
27//! // sorted = ["compile", "build", "test", "deploy"]
28//! assert_eq!(sorted[0], "compile"); // No dependencies, comes first
29//! assert_eq!(sorted[3], "deploy"); // Depends on everything, comes last
30//! }
31//! ```
32
33pub mod impact;
34pub mod metrics;
35
36use alloc::collections::BTreeMap;
37use alloc::vec::Vec;
38use core::hash::Hash;
39
40/// Performs topological sorting on a collection of items using a dependency function.
41///
42/// # Arguments
43/// * `items` - Slice of all items to sort
44/// * `get_dependencies` - Function that returns the dependencies for each item
45///
46/// # Returns
47/// * `Ok(Vec<Id>)` - Items in topological order (items with no dependencies first)
48/// * `Err(Vec<Id>)` - A cycle was detected, returns one of the cycles found
49///
50/// # Examples
51///
52/// ```
53/// use ascii_dag::layout::generic::topological_sort_fn;
54///
55/// let get_deps = |&id: &usize| match id {
56/// 1 => vec![], // No dependencies
57/// 2 => vec![1], // Depends on 1
58/// 3 => vec![1, 2], // Depends on 1 and 2
59/// _ => vec![],
60/// };
61///
62/// let items = [1, 2, 3];
63/// let sorted = topological_sort_fn(&items, get_deps).unwrap();
64/// assert_eq!(sorted, vec![1, 2, 3]);
65/// ```
66pub fn topological_sort_fn<Id, F>(items: &[Id], get_dependencies: F) -> Result<Vec<Id>, Vec<Id>>
67where
68 Id: Clone + Eq + Hash + Ord,
69 F: Fn(&Id) -> Vec<Id>,
70{
71 use crate::cycles::generic::detect_cycle_fn;
72
73 // First check for cycles
74 if let Some(cycle) = detect_cycle_fn(items, &get_dependencies) {
75 return Err(cycle);
76 }
77
78 // Kahn's algorithm with BTreeMap for deterministic ordering
79 let mut in_degree: BTreeMap<Id, usize> = BTreeMap::new();
80 let mut result = Vec::new();
81
82 // Initialize in-degrees
83 for item in items {
84 in_degree.entry(item.clone()).or_insert(0);
85 }
86
87 // Calculate in-degrees: if item depends on dep, then item has incoming edge from dep
88 for item in items {
89 let deps_count = get_dependencies(item).len();
90 *in_degree.entry(item.clone()).or_insert(0) += deps_count;
91 }
92
93 // Find all items with no dependencies (in_degree == 0)
94 let mut queue: Vec<Id> = in_degree
95 .iter()
96 .filter(|&(_, °ree)| degree == 0)
97 .map(|(id, _)| id.clone())
98 .collect();
99
100 // Process queue
101 while let Some(item) = queue.pop() {
102 result.push(item.clone());
103
104 // Find all items that depend on the current item
105 for candidate in items {
106 let deps = get_dependencies(candidate);
107 if deps.contains(&item) {
108 if let Some(degree) = in_degree.get_mut(candidate) {
109 *degree -= 1;
110 if *degree == 0 {
111 queue.push(candidate.clone());
112 }
113 }
114 }
115 }
116 }
117
118 // If we processed all items, we have a valid topological order
119 if result.len() == items.len() {
120 Ok(result)
121 } else {
122 // This shouldn't happen since we checked for cycles, but handle it anyway
123 Err(vec![])
124 }
125}
126
127/// Trait for types that support topological sorting.
128///
129/// Implement this trait to get convenient `topological_sort()` methods.
130///
131/// # Examples
132///
133/// ```
134/// use ascii_dag::layout::generic::TopologicallySortable;
135/// use std::collections::HashMap;
136///
137/// struct TaskGraph {
138/// tasks: Vec<String>,
139/// dependencies: HashMap<String, Vec<String>>,
140/// }
141///
142/// impl TopologicallySortable for TaskGraph {
143/// type Id = String;
144///
145/// fn get_all_ids(&self) -> Vec<String> {
146/// self.tasks.clone()
147/// }
148///
149/// fn get_dependencies(&self, id: &String) -> Vec<String> {
150/// self.dependencies.get(id).cloned().unwrap_or_default()
151/// }
152/// }
153///
154/// // Now you can call:
155/// // let sorted = task_graph.topological_sort().unwrap();
156/// ```
157pub trait TopologicallySortable {
158 /// The type of identifiers in the graph.
159 type Id: Clone + Eq + Hash + Ord;
160
161 /// Get all item IDs in the collection.
162 fn get_all_ids(&self) -> Vec<Self::Id>;
163
164 /// Get the dependencies for a given item.
165 fn get_dependencies(&self, id: &Self::Id) -> Vec<Self::Id>;
166
167 /// Perform topological sorting on this collection.
168 ///
169 /// # Returns
170 /// * `Ok(Vec<Id>)` - Items in dependency order
171 /// * `Err(Vec<Id>)` - A cycle was detected
172 fn topological_sort(&self) -> Result<Vec<Self::Id>, Vec<Self::Id>> {
173 let ids = self.get_all_ids();
174 topological_sort_fn(&ids, |id| self.get_dependencies(id))
175 }
176
177 /// Check if a valid topological ordering exists (i.e., no cycles).
178 fn has_valid_ordering(&self) -> bool {
179 self.topological_sort().is_ok()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_simple_chain() {
189 let get_deps = |&id: &usize| match id {
190 1 => vec![],
191 2 => vec![1],
192 3 => vec![2],
193 _ => vec![],
194 };
195
196 let items = [3, 1, 2]; // Unsorted input
197 let sorted = topological_sort_fn(&items, get_deps).unwrap();
198 assert_eq!(sorted, vec![1, 2, 3]);
199 }
200
201 #[test]
202 fn test_diamond_dependency() {
203 let get_deps = |&id: &usize| match id {
204 1 => vec![],
205 2 => vec![1],
206 3 => vec![1],
207 4 => vec![2, 3],
208 _ => vec![],
209 };
210
211 let items = [4, 3, 2, 1]; // Unsorted
212 let sorted = topological_sort_fn(&items, get_deps).unwrap();
213
214 // 1 must come first
215 assert_eq!(sorted[0], 1);
216 // 4 must come last
217 assert_eq!(sorted[3], 4);
218 // 2 and 3 can be in any order, but both after 1 and before 4
219 assert!(sorted[1] == 2 || sorted[1] == 3);
220 }
221
222 #[test]
223 fn test_cycle_detection() {
224 let get_deps = |&id: &usize| match id {
225 1 => vec![2],
226 2 => vec![3],
227 3 => vec![1], // Cycle!
228 _ => vec![],
229 };
230
231 let items = [1, 2, 3];
232 let result = topological_sort_fn(&items, get_deps);
233 assert!(result.is_err());
234 }
235
236 #[test]
237 fn test_multiple_roots() {
238 let get_deps = |&id: &usize| match id {
239 1 => vec![],
240 2 => vec![],
241 3 => vec![1, 2],
242 _ => vec![],
243 };
244
245 let items = [3, 2, 1];
246 let sorted = topological_sort_fn(&items, get_deps).unwrap();
247
248 // 1 and 2 must come before 3
249 assert!(
250 sorted.iter().position(|&x| x == 1).unwrap()
251 < sorted.iter().position(|&x| x == 3).unwrap()
252 );
253 assert!(
254 sorted.iter().position(|&x| x == 2).unwrap()
255 < sorted.iter().position(|&x| x == 3).unwrap()
256 );
257 }
258
259 #[test]
260 fn test_trait_based_sorting() {
261 use alloc::collections::BTreeMap;
262
263 struct SimpleGraph {
264 deps: BTreeMap<usize, Vec<usize>>,
265 }
266
267 impl TopologicallySortable for SimpleGraph {
268 type Id = usize;
269
270 fn get_all_ids(&self) -> Vec<usize> {
271 self.deps.keys().copied().collect()
272 }
273
274 fn get_dependencies(&self, id: &usize) -> Vec<usize> {
275 self.deps.get(id).cloned().unwrap_or_default()
276 }
277 }
278
279 let mut deps = BTreeMap::new();
280 deps.insert(1, vec![]);
281 deps.insert(2, vec![1]);
282 deps.insert(3, vec![2]);
283
284 let graph = SimpleGraph { deps };
285 let sorted = graph.topological_sort().unwrap();
286 assert_eq!(sorted, vec![1, 2, 3]);
287 assert!(graph.has_valid_ordering());
288 }
289}