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(|&(_, &degree)| 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}