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