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() {
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 assert!(graph.detect_cycles().is_ok());
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 }
358
359 #[test]
360 fn test_circular_dependency_detection() {
361 let mut graph = DependencyGraph::new();
362
363 graph.add_dependency(
365 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
366 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
367 );
368 graph.add_dependency(
369 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
370 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
371 );
372 graph.add_dependency(
373 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
374 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
375 );
376
377 let result = graph.detect_cycles();
378 assert!(result.is_err());
379 let error_msg = result.unwrap_err().to_string();
380 assert!(error_msg.contains("Circular dependency"));
381 assert!(error_msg.contains("agent:A"));
382 }
383
384 #[test]
385 fn test_diamond_dependency() {
386 let mut graph = DependencyGraph::new();
387
388 graph.add_dependency(
390 DependencyNode::new(crate::core::ResourceType::Command, "A"),
391 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
392 );
393 graph.add_dependency(
394 DependencyNode::new(crate::core::ResourceType::Command, "A"),
395 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
396 );
397 graph.add_dependency(
398 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
399 DependencyNode::new(crate::core::ResourceType::Snippet, "D"),
400 );
401 graph.add_dependency(
402 DependencyNode::new(crate::core::ResourceType::Agent, "C"),
403 DependencyNode::new(crate::core::ResourceType::Snippet, "D"),
404 );
405
406 assert!(graph.detect_cycles().is_ok());
407
408 let order = graph.topological_order().unwrap();
409 assert_eq!(order.len(), 4);
410
411 let d_idx = order.iter().position(|n| n.name == "D").unwrap();
413 let b_idx = order.iter().position(|n| n.name == "B").unwrap();
414 let c_idx = order.iter().position(|n| n.name == "C").unwrap();
415 let a_idx = order.iter().position(|n| n.name == "A").unwrap();
416
417 assert!(d_idx < b_idx);
418 assert!(d_idx < c_idx);
419 assert!(b_idx < a_idx);
420 assert!(c_idx < a_idx);
421 }
422
423 #[test]
424 fn test_get_transitive_deps() {
425 let mut graph = DependencyGraph::new();
426
427 graph.add_dependency(
429 DependencyNode::new(crate::core::ResourceType::Command, "A"),
430 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
431 );
432 graph.add_dependency(
433 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
434 DependencyNode::new(crate::core::ResourceType::Snippet, "C"),
435 );
436 graph.add_dependency(
437 DependencyNode::new(crate::core::ResourceType::Command, "A"),
438 DependencyNode::new(crate::core::ResourceType::Snippet, "D"),
439 );
440
441 let deps = graph
442 .get_transitive_deps(&DependencyNode::new(crate::core::ResourceType::Command, "A"));
443 assert_eq!(deps.len(), 3);
444 assert!(deps.contains(&DependencyNode::new(crate::core::ResourceType::Agent, "B")));
445 assert!(deps.contains(&DependencyNode::new(crate::core::ResourceType::Snippet, "C")));
446 assert!(deps.contains(&DependencyNode::new(crate::core::ResourceType::Snippet, "D")));
447 }
448
449 #[test]
450 fn test_self_dependency() {
451 let mut graph = DependencyGraph::new();
452
453 graph.add_dependency(
455 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
456 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
457 );
458
459 let result = graph.detect_cycles();
460 assert!(result.is_err());
461 assert!(result.unwrap_err().to_string().contains("Circular dependency"));
462 }
463
464 #[test]
465 fn test_empty_graph() {
466 let graph = DependencyGraph::new();
467 assert!(graph.is_empty());
468 assert_eq!(graph.node_count(), 0);
469 assert_eq!(graph.edge_count(), 0);
470 assert!(graph.detect_cycles().is_ok());
471 assert!(graph.topological_order().unwrap().is_empty());
472 }
473
474 #[test]
475 fn test_duplicate_edges() {
476 let mut graph = DependencyGraph::new();
477
478 graph.add_dependency(
480 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
481 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
482 );
483 graph.add_dependency(
484 DependencyNode::new(crate::core::ResourceType::Agent, "A"),
485 DependencyNode::new(crate::core::ResourceType::Agent, "B"),
486 );
487
488 assert_eq!(graph.edge_count(), 1); assert_eq!(graph.node_count(), 2);
490 }
491
492 #[test]
493 fn test_cross_source_no_false_cycle() {
494 let mut graph = DependencyGraph::new();
495
496 graph.add_dependency(
500 DependencyNode::with_source(
501 crate::core::ResourceType::Agent,
502 "A",
503 Some("sourceA".to_string()),
504 ),
505 DependencyNode::with_source(
506 crate::core::ResourceType::Agent,
507 "helper",
508 Some("sourceA".to_string()),
509 ),
510 );
511 graph.add_dependency(
512 DependencyNode::with_source(
513 crate::core::ResourceType::Agent,
514 "B",
515 Some("sourceB".to_string()),
516 ),
517 DependencyNode::with_source(
518 crate::core::ResourceType::Agent,
519 "helper",
520 Some("sourceB".to_string()),
521 ),
522 );
523
524 assert_eq!(graph.node_count(), 4);
526 assert_eq!(graph.edge_count(), 2);
527
528 assert!(graph.detect_cycles().is_ok());
530
531 let order = graph.topological_order().unwrap();
533 assert_eq!(order.len(), 4);
534 }
535
536 #[test]
537 fn test_cross_source_real_cycle() {
538 let mut graph = DependencyGraph::new();
539
540 graph.add_dependency(
542 DependencyNode::with_source(
543 crate::core::ResourceType::Agent,
544 "A",
545 Some("sourceX".to_string()),
546 ),
547 DependencyNode::with_source(
548 crate::core::ResourceType::Agent,
549 "B",
550 Some("sourceX".to_string()),
551 ),
552 );
553 graph.add_dependency(
554 DependencyNode::with_source(
555 crate::core::ResourceType::Agent,
556 "B",
557 Some("sourceX".to_string()),
558 ),
559 DependencyNode::with_source(
560 crate::core::ResourceType::Agent,
561 "A",
562 Some("sourceX".to_string()),
563 ),
564 );
565
566 let result = graph.detect_cycles();
568 assert!(result.is_err());
569 assert!(result.unwrap_err().to_string().contains("Circular dependency"));
570 }
571}