1use anyhow::{Result, anyhow};
8use petgraph::algo::toposort;
9use petgraph::graph::{DiGraph, NodeIndex};
10use std::collections::{HashMap, HashSet, VecDeque};
11use std::fmt;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct DependencyNode {
20 pub resource_type: crate::core::ResourceType,
22 pub name: String,
24 pub source: Option<String>,
26}
27
28impl DependencyNode {
29 pub fn new(resource_type: crate::core::ResourceType, name: impl Into<String>) -> Self {
31 Self {
32 resource_type,
33 name: name.into(),
34 source: None,
35 }
36 }
37
38 pub fn with_source(
40 resource_type: crate::core::ResourceType,
41 name: impl Into<String>,
42 source: Option<String>,
43 ) -> Self {
44 Self {
45 resource_type,
46 name: name.into(),
47 source,
48 }
49 }
50
51 pub fn display_name(&self) -> String {
53 if let Some(ref source) = self.source {
54 format!("{}/{}@{}", self.resource_type, self.name, source)
55 } else {
56 format!("{}/{}", self.resource_type, self.name)
57 }
58 }
59}
60
61impl fmt::Display for DependencyNode {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(f, "{}", self.display_name())
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69enum Color {
70 White,
72 Gray,
74 Black,
76}
77
78pub struct DependencyGraph {
83 graph: DiGraph<DependencyNode, ()>,
85 node_map: HashMap<DependencyNode, NodeIndex>,
87}
88
89impl DependencyGraph {
90 pub fn new() -> Self {
92 Self {
93 graph: DiGraph::new(),
94 node_map: HashMap::new(),
95 }
96 }
97
98 fn ensure_node(&mut self, node: DependencyNode) -> NodeIndex {
102 if let Some(&index) = self.node_map.get(&node) {
103 index
104 } else {
105 let index = self.graph.add_node(node.clone());
106 self.node_map.insert(node, index);
107 index
108 }
109 }
110
111 pub fn add_dependency(&mut self, from: DependencyNode, to: DependencyNode) {
115 let from_idx = self.ensure_node(from);
116 let to_idx = self.ensure_node(to);
117
118 if !self.graph.contains_edge(from_idx, to_idx) {
120 self.graph.add_edge(from_idx, to_idx, ());
121 }
122 }
123
124 pub fn detect_cycles(&self) -> Result<()> {
128 let mut colors: HashMap<NodeIndex, Color> = HashMap::new();
129 let mut path: Vec<DependencyNode> = Vec::new();
130
131 for node in self.graph.node_indices() {
133 colors.insert(node, Color::White);
134 }
135
136 for node in self.graph.node_indices() {
138 if matches!(colors.get(&node), Some(Color::White))
139 && let Some(cycle) = self.dfs_visit(node, &mut colors, &mut path)
140 {
141 let cycle_str =
142 cycle.iter().map(DependencyNode::display_name).collect::<Vec<_>>().join(" → ");
143 return Err(anyhow!("Circular dependency detected: {cycle_str}"));
144 }
145 }
146
147 Ok(())
148 }
149
150 fn dfs_visit(
154 &self,
155 node: NodeIndex,
156 colors: &mut HashMap<NodeIndex, Color>,
157 path: &mut Vec<DependencyNode>,
158 ) -> Option<Vec<DependencyNode>> {
159 colors.insert(node, Color::Gray);
160 path.push(self.graph[node].clone());
161
162 for neighbor in self.graph.neighbors(node) {
163 match colors.get(&neighbor) {
164 Some(Color::Gray) => {
165 let cycle_start = path.iter().position(|n| *n == self.graph[neighbor]).unwrap();
167 let mut cycle = path[cycle_start..].to_vec();
168 cycle.push(self.graph[neighbor].clone());
170 return Some(cycle);
171 }
172 Some(Color::White) => {
173 if let Some(cycle) = self.dfs_visit(neighbor, colors, path) {
174 return Some(cycle);
175 }
176 }
177 _ => {}
178 }
179 }
180
181 path.pop();
182 colors.insert(node, Color::Black);
183 None
184 }
185
186 pub fn topological_order(&self) -> Result<Vec<DependencyNode>> {
191 self.detect_cycles()?;
193
194 match toposort(&self.graph, None) {
196 Ok(indices) => {
197 let mut order = Vec::new();
198 for idx in indices.into_iter().rev() {
200 order.push(self.graph[idx].clone());
201 }
202 Ok(order)
203 }
204 Err(_) => {
205 Err(anyhow!("Failed to determine installation order"))
207 }
208 }
209 }
210
211 pub fn get_transitive_deps(&self, node: &DependencyNode) -> HashSet<DependencyNode> {
216 let mut deps = HashSet::new();
217 let mut queue = VecDeque::new();
218
219 if let Some(&node_idx) = self.node_map.get(node) {
221 queue.push_back(node_idx);
222
223 while let Some(current) = queue.pop_front() {
224 for neighbor in self.graph.neighbors(current) {
226 let dep_node = &self.graph[neighbor];
227 if deps.insert(dep_node.clone()) {
228 queue.push_back(neighbor);
230 }
231 }
232 }
233 }
234
235 deps
236 }
237
238 pub fn get_direct_deps(&self, node: &DependencyNode) -> Vec<DependencyNode> {
242 if let Some(&node_idx) = self.node_map.get(node) {
243 self.graph.neighbors(node_idx).map(|idx| self.graph[idx].clone()).collect()
244 } else {
245 Vec::new()
246 }
247 }
248
249 pub fn is_empty(&self) -> bool {
251 self.graph.node_count() == 0
252 }
253
254 pub fn node_count(&self) -> usize {
256 self.graph.node_count()
257 }
258
259 pub fn edge_count(&self) -> usize {
261 self.graph.edge_count()
262 }
263
264 pub fn nodes(&self) -> Vec<DependencyNode> {
266 self.graph.node_indices().map(|idx| self.graph[idx].clone()).collect()
267 }
268
269 pub fn to_tree_string(&self, root: &DependencyNode) -> String {
273 let mut result = String::new();
274 let mut visited = HashSet::new();
275 self.build_tree_string(root, &mut result, "", true, &mut visited);
276 result
277 }
278
279 fn build_tree_string(
280 &self,
281 node: &DependencyNode,
282 result: &mut String,
283 prefix: &str,
284 is_last: bool,
285 visited: &mut HashSet<DependencyNode>,
286 ) {
287 let connector = if is_last {
288 "└── "
289 } else {
290 "├── "
291 };
292 result.push_str(&format!("{}{}{}\n", prefix, connector, node.display_name()));
293
294 if !visited.insert(node.clone()) {
295 let child_prefix = if is_last {
297 format!("{prefix} ")
298 } else {
299 format!("{prefix}│ ")
300 };
301 result.push_str(&format!("{child_prefix}└── (circular reference)\n"));
302 return;
303 }
304
305 let deps = self.get_direct_deps(node);
306 let child_prefix = if is_last {
307 format!("{prefix} ")
308 } else {
309 format!("{prefix}│ ")
310 };
311
312 for (i, dep) in deps.iter().enumerate() {
313 let is_last_child = i == deps.len() - 1;
314 self.build_tree_string(dep, result, &child_prefix, is_last_child, visited);
315 }
316 }
317}
318
319impl Default for DependencyGraph {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_simple_dependency_chain() {
331 let mut graph = DependencyGraph::new();
332
333 graph.add_dependency(
335 DependencyNode::new(crate::core::ResourceType::Command, "A"),
336 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
337 );
338 graph.add_dependency(
339 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
340 DependencyNode::new(crate::core::ResourceType::Snippet, "C"),
341 );
342
343 assert!(graph.detect_cycles().is_ok());
344
345 let order = graph.topological_order().unwrap();
346 assert_eq!(order.len(), 3);
347
348 let c_idx = order.iter().position(|n| n.name == "C").unwrap();
350 let b_idx = order.iter().position(|n| n.name == "B").unwrap();
351 let a_idx = order.iter().position(|n| n.name == "A").unwrap();
352 assert!(c_idx < b_idx);
353 assert!(b_idx < a_idx);
354 }
355
356 #[test]
357 fn test_circular_dependency_detection() {
358 let mut graph = DependencyGraph::new();
359
360 graph.add_dependency(
362 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
363 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
364 );
365 graph.add_dependency(
366 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
367 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
368 );
369 graph.add_dependency(
370 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
371 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
372 );
373
374 let result = graph.detect_cycles();
375 assert!(result.is_err());
376 let error_msg = result.unwrap_err().to_string();
377 assert!(error_msg.contains("Circular dependency"));
378 assert!(error_msg.contains("agent/A"));
379 }
380
381 #[test]
382 fn test_diamond_dependency() {
383 let mut graph = DependencyGraph::new();
384
385 graph.add_dependency(
387 DependencyNode::new(crate::core::ResourceType::Command, "A"),
388 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
389 );
390 graph.add_dependency(
391 DependencyNode::new(crate::core::ResourceType::Command, "A"),
392 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
393 );
394 graph.add_dependency(
395 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
396 DependencyNode::new(crate::core::ResourceType::Snippet, "D"),
397 );
398 graph.add_dependency(
399 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
400 DependencyNode::new(crate::core::ResourceType::Snippet, "D"),
401 );
402
403 assert!(graph.detect_cycles().is_ok());
404
405 let order = graph.topological_order().unwrap();
406 assert_eq!(order.len(), 4);
407
408 let d_idx = order.iter().position(|n| n.name == "D").unwrap();
410 let b_idx = order.iter().position(|n| n.name == "B").unwrap();
411 let c_idx = order.iter().position(|n| n.name == "C").unwrap();
412 let a_idx = order.iter().position(|n| n.name == "A").unwrap();
413
414 assert!(d_idx < b_idx);
415 assert!(d_idx < c_idx);
416 assert!(b_idx < a_idx);
417 assert!(c_idx < a_idx);
418 }
419
420 #[test]
421 fn test_get_transitive_deps() {
422 let mut graph = DependencyGraph::new();
423
424 graph.add_dependency(
426 DependencyNode::new(crate::core::ResourceType::Command, "A"),
427 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
428 );
429 graph.add_dependency(
430 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
431 DependencyNode::new(crate::core::ResourceType::Snippet, "C"),
432 );
433 graph.add_dependency(
434 DependencyNode::new(crate::core::ResourceType::Command, "A"),
435 DependencyNode::new(crate::core::ResourceType::Snippet, "D"),
436 );
437
438 let deps = graph
439 .get_transitive_deps(&DependencyNode::new(crate::core::ResourceType::Command, "A"));
440 assert_eq!(deps.len(), 3);
441 assert!(deps.contains(&DependencyNode::new(crate::core::ResourceType::Agent, "B")));
442 assert!(deps.contains(&DependencyNode::new(crate::core::ResourceType::Snippet, "C")));
443 assert!(deps.contains(&DependencyNode::new(crate::core::ResourceType::Snippet, "D")));
444 }
445
446 #[test]
447 fn test_self_dependency() {
448 let mut graph = DependencyGraph::new();
449
450 graph.add_dependency(
452 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
453 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
454 );
455
456 let result = graph.detect_cycles();
457 assert!(result.is_err());
458 assert!(result.unwrap_err().to_string().contains("Circular dependency"));
459 }
460
461 #[test]
462 fn test_empty_graph() {
463 let graph = DependencyGraph::new();
464 assert!(graph.is_empty());
465 assert_eq!(graph.node_count(), 0);
466 assert_eq!(graph.edge_count(), 0);
467 assert!(graph.detect_cycles().is_ok());
468 assert!(graph.topological_order().unwrap().is_empty());
469 }
470
471 #[test]
472 fn test_duplicate_edges() {
473 let mut graph = DependencyGraph::new();
474
475 graph.add_dependency(
477 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
478 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
479 );
480 graph.add_dependency(
481 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
482 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
483 );
484
485 assert_eq!(graph.edge_count(), 1); assert_eq!(graph.node_count(), 2);
487 }
488
489 #[test]
490 fn test_cross_source_no_false_cycle() {
491 let mut graph = DependencyGraph::new();
492
493 graph.add_dependency(
497 DependencyNode::with_source(
498 crate::core::ResourceType::Agent,
499 "A",
500 Some("sourceA".to_string()),
501 ),
502 DependencyNode::with_source(
503 crate::core::ResourceType::Agent,
504 "helper",
505 Some("sourceA".to_string()),
506 ),
507 );
508 graph.add_dependency(
509 DependencyNode::with_source(
510 crate::core::ResourceType::Agent,
511 "B",
512 Some("sourceB".to_string()),
513 ),
514 DependencyNode::with_source(
515 crate::core::ResourceType::Agent,
516 "helper",
517 Some("sourceB".to_string()),
518 ),
519 );
520
521 assert_eq!(graph.node_count(), 4);
523 assert_eq!(graph.edge_count(), 2);
524
525 assert!(graph.detect_cycles().is_ok());
527
528 let order = graph.topological_order().unwrap();
530 assert_eq!(order.len(), 4);
531 }
532
533 #[test]
534 fn test_cross_source_real_cycle() {
535 let mut graph = DependencyGraph::new();
536
537 graph.add_dependency(
539 DependencyNode::with_source(
540 crate::core::ResourceType::Agent,
541 "A",
542 Some("sourceX".to_string()),
543 ),
544 DependencyNode::with_source(
545 crate::core::ResourceType::Agent,
546 "B",
547 Some("sourceX".to_string()),
548 ),
549 );
550 graph.add_dependency(
551 DependencyNode::with_source(
552 crate::core::ResourceType::Agent,
553 "B",
554 Some("sourceX".to_string()),
555 ),
556 DependencyNode::with_source(
557 crate::core::ResourceType::Agent,
558 "A",
559 Some("sourceX".to_string()),
560 ),
561 );
562
563 let result = graph.detect_cycles();
565 assert!(result.is_err());
566 assert!(result.unwrap_err().to_string().contains("Circular dependency"));
567 }
568}