fleetreach_core/
depgraph.rs1use std::collections::{BTreeMap, BTreeSet, VecDeque};
14
15#[derive(Debug, Default, Clone)]
17pub struct DepGraph {
18 root: String,
19 edges: BTreeMap<String, BTreeSet<String>>,
20}
21
22impl DepGraph {
23 pub fn new(root: impl Into<String>) -> Self {
25 DepGraph {
26 root: root.into(),
27 edges: BTreeMap::new(),
28 }
29 }
30
31 pub fn add_edges<I, S>(&mut self, from: &str, tos: I)
33 where
34 I: IntoIterator<Item = S>,
35 S: Into<String>,
36 {
37 let set = self.edges.entry(from.to_string()).or_default();
38 for to in tos {
39 set.insert(to.into());
40 }
41 }
42
43 pub fn is_empty(&self) -> bool {
45 self.edges.is_empty()
46 }
47
48 pub fn chain_to(&self, target: &str) -> Vec<String> {
52 if self.edges.is_empty() || self.root == target {
53 return Vec::new();
54 }
55 let mut pred: BTreeMap<&str, &str> = BTreeMap::new();
56 let mut visited: BTreeSet<&str> = BTreeSet::new();
57 visited.insert(self.root.as_str());
58 let mut queue: VecDeque<&str> = VecDeque::from([self.root.as_str()]);
59 while let Some(node) = queue.pop_front() {
60 if node == target {
61 break;
62 }
63 if let Some(deps) = self.edges.get(node) {
64 for dep in deps {
65 if visited.insert(dep.as_str()) {
66 pred.insert(dep.as_str(), node);
67 queue.push_back(dep.as_str());
68 }
69 }
70 }
71 }
72 if !visited.contains(target) {
73 return Vec::new();
74 }
75 let mut path = vec![target.to_string()];
76 let mut cursor = target;
77 while let Some(&parent) = pred.get(cursor) {
78 path.push(parent.to_string());
79 cursor = parent;
80 }
81 path.reverse();
82 path
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn chain_includes_root_and_target() {
92 let mut g = DepGraph::new("app");
93 g.add_edges("app", ["express", "lodash"]);
94 g.add_edges("express", ["minimist"]);
95 assert_eq!(g.chain_to("minimist"), vec!["app", "express", "minimist"]);
96 assert_eq!(g.chain_to("lodash"), vec!["app", "lodash"]);
97 assert!(g.chain_to("absent").is_empty());
98 }
99
100 #[test]
101 fn empty_graph_yields_empty_chain() {
102 assert!(DepGraph::default().chain_to("anything").is_empty());
103 assert!(DepGraph::new("app").chain_to("x").is_empty());
104 }
105
106 #[test]
107 fn shortest_of_multiple_paths_wins() {
108 let mut g = DepGraph::new("app");
110 g.add_edges("app", ["a", "b"]);
111 g.add_edges("a", ["target"]);
112 g.add_edges("b", ["c"]);
113 g.add_edges("c", ["target"]);
114 assert_eq!(g.chain_to("target"), vec!["app", "a", "target"]);
115 }
116}