1use crate::error::{OrchestrationError, Result};
4use crate::models::{DependencyType, Project, ProjectDependency};
5use std::collections::{HashMap, HashSet, VecDeque};
6
7#[derive(Debug, Clone)]
9pub struct DependencyAnalyzer {
10 dependencies: HashMap<String, Vec<ProjectDependency>>,
12
13 projects: HashMap<String, Project>,
15}
16
17impl DependencyAnalyzer {
18 pub fn new() -> Self {
20 Self {
21 dependencies: HashMap::new(),
22 projects: HashMap::new(),
23 }
24 }
25
26 pub fn add_project(&mut self, project: Project) {
28 self.projects.insert(project.name.clone(), project);
29 }
30
31 pub fn add_dependency(&mut self, dependency: ProjectDependency) {
33 self.dependencies
34 .entry(dependency.from.clone())
35 .or_default()
36 .push(dependency);
37 }
38
39 pub fn parse_cargo_toml(&mut self, content: &str, project_name: &str) -> Result<()> {
42 for line in content.lines() {
45 let trimmed = line.trim();
46
47 if trimmed.starts_with('[') && trimmed.ends_with(']') {
49 continue;
51 }
52
53 if let Some(eq_pos) = trimmed.find('=') {
55 let dep_name = trimmed[..eq_pos].trim();
56 let _version = trimmed[eq_pos + 1..].trim();
57
58 if !dep_name.is_empty() && dep_name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
60 let dependency = ProjectDependency {
61 from: project_name.to_string(),
62 to: dep_name.to_string(),
63 dependency_type: DependencyType::Direct,
64 version_constraint: "^0.1.0".to_string(),
65 };
66 self.add_dependency(dependency);
67 }
68 }
69 }
70
71 Ok(())
72 }
73
74 pub fn get_direct_dependencies(&self, project_name: &str) -> Vec<ProjectDependency> {
76 self.dependencies
77 .get(project_name)
78 .cloned()
79 .unwrap_or_default()
80 }
81
82 pub fn get_transitive_dependencies(&self, project_name: &str) -> Result<Vec<ProjectDependency>> {
84 let mut transitive = Vec::new();
85 let mut visited = HashSet::new();
86 let mut queue = VecDeque::new();
87
88 for dep in self.get_direct_dependencies(project_name) {
90 queue.push_back(dep.to.clone());
91 }
92
93 while let Some(current) = queue.pop_front() {
94 if visited.contains(¤t) {
95 continue;
96 }
97 visited.insert(current.clone());
98
99 for dep in self.get_direct_dependencies(¤t) {
101 if !visited.contains(&dep.to) {
102 transitive.push(ProjectDependency {
103 from: project_name.to_string(),
104 to: dep.to.clone(),
105 dependency_type: DependencyType::Transitive,
106 version_constraint: dep.version_constraint.clone(),
107 });
108 queue.push_back(dep.to.clone());
109 }
110 }
111 }
112
113 Ok(transitive)
114 }
115
116 pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<String>>> {
118 let mut cycles = Vec::new();
119 let mut visited = HashSet::new();
120 let mut rec_stack = HashSet::new();
121
122 for project_name in self.projects.keys() {
123 if !visited.contains(project_name) {
124 self.dfs_cycle_detection(
125 project_name,
126 &mut visited,
127 &mut rec_stack,
128 &mut cycles,
129 Vec::new(),
130 )?;
131 }
132 }
133
134 if !cycles.is_empty() {
135 return Err(OrchestrationError::CircularDependency(format!(
136 "Found {} circular dependencies",
137 cycles.len()
138 )));
139 }
140
141 Ok(cycles)
142 }
143
144 fn dfs_cycle_detection(
146 &self,
147 node: &str,
148 visited: &mut HashSet<String>,
149 rec_stack: &mut HashSet<String>,
150 cycles: &mut Vec<Vec<String>>,
151 mut path: Vec<String>,
152 ) -> Result<()> {
153 visited.insert(node.to_string());
154 rec_stack.insert(node.to_string());
155 path.push(node.to_string());
156
157 for dep in self.get_direct_dependencies(node) {
158 if !visited.contains(&dep.to) {
159 self.dfs_cycle_detection(&dep.to, visited, rec_stack, cycles, path.clone())?;
160 } else if rec_stack.contains(&dep.to) {
161 if let Some(start_idx) = path.iter().position(|x| x == &dep.to) {
163 let cycle = path[start_idx..].to_vec();
164 cycles.push(cycle);
165 }
166 }
167 }
168
169 rec_stack.remove(node);
170 Ok(())
171 }
172
173 pub fn get_upstream_dependents(&self, project_name: &str) -> Vec<String> {
175 let mut dependents = Vec::new();
176
177 for (from, deps) in &self.dependencies {
178 for dep in deps {
179 if dep.to == project_name {
180 dependents.push(from.clone());
181 }
182 }
183 }
184
185 dependents
186 }
187
188 pub fn get_downstream_dependencies(&self, project_name: &str) -> Vec<String> {
190 self.get_direct_dependencies(project_name)
191 .iter()
192 .map(|d| d.to.clone())
193 .collect()
194 }
195
196 pub fn validate_dependencies(&self) -> Result<()> {
198 for (from, deps) in &self.dependencies {
199 if !self.projects.contains_key(from) {
201 return Err(OrchestrationError::DependencyValidationFailed(format!(
202 "Project '{}' not found",
203 from
204 )));
205 }
206
207 for dep in deps {
209 if !self.projects.contains_key(&dep.to) {
210 return Err(OrchestrationError::DependencyValidationFailed(format!(
211 "Dependency target '{}' not found for project '{}'",
212 dep.to, from
213 )));
214 }
215 }
216 }
217
218 Ok(())
219 }
220
221 pub fn get_adjacency_list(&self) -> HashMap<String, Vec<String>> {
223 let mut adj_list = HashMap::new();
224
225 for project_name in self.projects.keys() {
226 let deps = self.get_direct_dependencies(project_name);
227 let dep_names: Vec<String> = deps.iter().map(|d| d.to.clone()).collect();
228 adj_list.insert(project_name.clone(), dep_names);
229 }
230
231 adj_list
232 }
233
234 pub fn topological_sort(&self) -> Result<Vec<String>> {
236 let mut in_degree: HashMap<String, usize> = HashMap::new();
237 let mut adj_list: HashMap<String, Vec<String>> = HashMap::new();
238
239 for project_name in self.projects.keys() {
241 in_degree.insert(project_name.clone(), 0);
242 adj_list.insert(project_name.clone(), Vec::new());
243 }
244
245 for (from, deps) in &self.dependencies {
247 for dep in deps {
248 adj_list.entry(from.clone()).or_default().push(dep.to.clone());
249 *in_degree.entry(dep.to.clone()).or_insert(0) += 1;
250 }
251 }
252
253 let mut queue: VecDeque<String> = in_degree
255 .iter()
256 .filter(|(_, °ree)| degree == 0)
257 .map(|(name, _)| name.clone())
258 .collect();
259
260 let mut sorted = Vec::new();
261
262 while let Some(node) = queue.pop_front() {
263 sorted.push(node.clone());
264
265 if let Some(neighbors) = adj_list.get(&node) {
266 for neighbor in neighbors {
267 let degree = in_degree.entry(neighbor.clone()).or_insert(0);
268 *degree -= 1;
269 if *degree == 0 {
270 queue.push_back(neighbor.clone());
271 }
272 }
273 }
274 }
275
276 if sorted.len() != self.projects.len() {
277 return Err(OrchestrationError::CircularDependency(
278 "Topological sort failed: circular dependencies detected".to_string(),
279 ));
280 }
281
282 Ok(sorted)
283 }
284
285 pub fn get_projects(&self) -> Vec<Project> {
287 self.projects.values().cloned().collect()
288 }
289
290 pub fn get_all_dependencies(&self) -> Vec<ProjectDependency> {
292 self.dependencies
293 .values()
294 .flat_map(|deps| deps.clone())
295 .collect()
296 }
297
298 pub fn clear(&mut self) {
300 self.dependencies.clear();
301 self.projects.clear();
302 }
303}
304
305impl Default for DependencyAnalyzer {
306 fn default() -> Self {
307 Self::new()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::path::PathBuf;
315
316 fn create_test_project(name: &str) -> Project {
317 Project {
318 path: PathBuf::from(format!("/path/to/{}", name)),
319 name: name.to_string(),
320 project_type: "rust".to_string(),
321 version: "0.1.0".to_string(),
322 status: crate::models::ProjectStatus::Healthy,
323 }
324 }
325
326 #[test]
327 fn test_add_project() {
328 let mut analyzer = DependencyAnalyzer::new();
329 let project = create_test_project("project-a");
330
331 analyzer.add_project(project.clone());
332
333 assert_eq!(analyzer.get_projects().len(), 1);
334 assert_eq!(analyzer.get_projects()[0].name, "project-a");
335 }
336
337 #[test]
338 fn test_add_dependency() {
339 let mut analyzer = DependencyAnalyzer::new();
340 analyzer.add_project(create_test_project("project-a"));
341 analyzer.add_project(create_test_project("project-b"));
342
343 let dep = ProjectDependency {
344 from: "project-a".to_string(),
345 to: "project-b".to_string(),
346 dependency_type: DependencyType::Direct,
347 version_constraint: "^0.1.0".to_string(),
348 };
349
350 analyzer.add_dependency(dep);
351
352 let deps = analyzer.get_direct_dependencies("project-a");
353 assert_eq!(deps.len(), 1);
354 assert_eq!(deps[0].to, "project-b");
355 }
356
357 #[test]
358 fn test_get_direct_dependencies() {
359 let mut analyzer = DependencyAnalyzer::new();
360 analyzer.add_project(create_test_project("project-a"));
361 analyzer.add_project(create_test_project("project-b"));
362 analyzer.add_project(create_test_project("project-c"));
363
364 analyzer.add_dependency(ProjectDependency {
365 from: "project-a".to_string(),
366 to: "project-b".to_string(),
367 dependency_type: DependencyType::Direct,
368 version_constraint: "^0.1.0".to_string(),
369 });
370
371 analyzer.add_dependency(ProjectDependency {
372 from: "project-a".to_string(),
373 to: "project-c".to_string(),
374 dependency_type: DependencyType::Direct,
375 version_constraint: "^0.1.0".to_string(),
376 });
377
378 let deps = analyzer.get_direct_dependencies("project-a");
379 assert_eq!(deps.len(), 2);
380 }
381
382 #[test]
383 fn test_get_transitive_dependencies() {
384 let mut analyzer = DependencyAnalyzer::new();
385 analyzer.add_project(create_test_project("project-a"));
386 analyzer.add_project(create_test_project("project-b"));
387 analyzer.add_project(create_test_project("project-c"));
388
389 analyzer.add_dependency(ProjectDependency {
391 from: "project-a".to_string(),
392 to: "project-b".to_string(),
393 dependency_type: DependencyType::Direct,
394 version_constraint: "^0.1.0".to_string(),
395 });
396
397 analyzer.add_dependency(ProjectDependency {
398 from: "project-b".to_string(),
399 to: "project-c".to_string(),
400 dependency_type: DependencyType::Direct,
401 version_constraint: "^0.1.0".to_string(),
402 });
403
404 let transitive = analyzer.get_transitive_dependencies("project-a").unwrap();
405 assert_eq!(transitive.len(), 1);
406 assert_eq!(transitive[0].to, "project-c");
407 }
408
409 #[test]
410 fn test_circular_dependency_detection() {
411 let mut analyzer = DependencyAnalyzer::new();
412 analyzer.add_project(create_test_project("project-a"));
413 analyzer.add_project(create_test_project("project-b"));
414 analyzer.add_project(create_test_project("project-c"));
415
416 analyzer.add_dependency(ProjectDependency {
418 from: "project-a".to_string(),
419 to: "project-b".to_string(),
420 dependency_type: DependencyType::Direct,
421 version_constraint: "^0.1.0".to_string(),
422 });
423
424 analyzer.add_dependency(ProjectDependency {
425 from: "project-b".to_string(),
426 to: "project-c".to_string(),
427 dependency_type: DependencyType::Direct,
428 version_constraint: "^0.1.0".to_string(),
429 });
430
431 analyzer.add_dependency(ProjectDependency {
432 from: "project-c".to_string(),
433 to: "project-a".to_string(),
434 dependency_type: DependencyType::Direct,
435 version_constraint: "^0.1.0".to_string(),
436 });
437
438 let result = analyzer.detect_circular_dependencies();
439 assert!(result.is_err());
440 }
441
442 #[test]
443 fn test_no_circular_dependencies() {
444 let mut analyzer = DependencyAnalyzer::new();
445 analyzer.add_project(create_test_project("project-a"));
446 analyzer.add_project(create_test_project("project-b"));
447 analyzer.add_project(create_test_project("project-c"));
448
449 analyzer.add_dependency(ProjectDependency {
451 from: "project-a".to_string(),
452 to: "project-b".to_string(),
453 dependency_type: DependencyType::Direct,
454 version_constraint: "^0.1.0".to_string(),
455 });
456
457 analyzer.add_dependency(ProjectDependency {
458 from: "project-b".to_string(),
459 to: "project-c".to_string(),
460 dependency_type: DependencyType::Direct,
461 version_constraint: "^0.1.0".to_string(),
462 });
463
464 let result = analyzer.detect_circular_dependencies();
465 assert!(result.is_ok());
466 }
467
468 #[test]
469 fn test_get_upstream_dependents() {
470 let mut analyzer = DependencyAnalyzer::new();
471 analyzer.add_project(create_test_project("project-a"));
472 analyzer.add_project(create_test_project("project-b"));
473 analyzer.add_project(create_test_project("project-c"));
474
475 analyzer.add_dependency(ProjectDependency {
476 from: "project-a".to_string(),
477 to: "project-c".to_string(),
478 dependency_type: DependencyType::Direct,
479 version_constraint: "^0.1.0".to_string(),
480 });
481
482 analyzer.add_dependency(ProjectDependency {
483 from: "project-b".to_string(),
484 to: "project-c".to_string(),
485 dependency_type: DependencyType::Direct,
486 version_constraint: "^0.1.0".to_string(),
487 });
488
489 let dependents = analyzer.get_upstream_dependents("project-c");
490 assert_eq!(dependents.len(), 2);
491 assert!(dependents.contains(&"project-a".to_string()));
492 assert!(dependents.contains(&"project-b".to_string()));
493 }
494
495 #[test]
496 fn test_get_downstream_dependencies() {
497 let mut analyzer = DependencyAnalyzer::new();
498 analyzer.add_project(create_test_project("project-a"));
499 analyzer.add_project(create_test_project("project-b"));
500 analyzer.add_project(create_test_project("project-c"));
501
502 analyzer.add_dependency(ProjectDependency {
503 from: "project-a".to_string(),
504 to: "project-b".to_string(),
505 dependency_type: DependencyType::Direct,
506 version_constraint: "^0.1.0".to_string(),
507 });
508
509 analyzer.add_dependency(ProjectDependency {
510 from: "project-a".to_string(),
511 to: "project-c".to_string(),
512 dependency_type: DependencyType::Direct,
513 version_constraint: "^0.1.0".to_string(),
514 });
515
516 let deps = analyzer.get_downstream_dependencies("project-a");
517 assert_eq!(deps.len(), 2);
518 assert!(deps.contains(&"project-b".to_string()));
519 assert!(deps.contains(&"project-c".to_string()));
520 }
521
522 #[test]
523 fn test_validate_dependencies_success() {
524 let mut analyzer = DependencyAnalyzer::new();
525 analyzer.add_project(create_test_project("project-a"));
526 analyzer.add_project(create_test_project("project-b"));
527
528 analyzer.add_dependency(ProjectDependency {
529 from: "project-a".to_string(),
530 to: "project-b".to_string(),
531 dependency_type: DependencyType::Direct,
532 version_constraint: "^0.1.0".to_string(),
533 });
534
535 let result = analyzer.validate_dependencies();
536 assert!(result.is_ok());
537 }
538
539 #[test]
540 fn test_validate_dependencies_missing_from() {
541 let mut analyzer = DependencyAnalyzer::new();
542 analyzer.add_project(create_test_project("project-b"));
543
544 analyzer.add_dependency(ProjectDependency {
545 from: "project-a".to_string(),
546 to: "project-b".to_string(),
547 dependency_type: DependencyType::Direct,
548 version_constraint: "^0.1.0".to_string(),
549 });
550
551 let result = analyzer.validate_dependencies();
552 assert!(result.is_err());
553 }
554
555 #[test]
556 fn test_validate_dependencies_missing_to() {
557 let mut analyzer = DependencyAnalyzer::new();
558 analyzer.add_project(create_test_project("project-a"));
559
560 analyzer.add_dependency(ProjectDependency {
561 from: "project-a".to_string(),
562 to: "project-b".to_string(),
563 dependency_type: DependencyType::Direct,
564 version_constraint: "^0.1.0".to_string(),
565 });
566
567 let result = analyzer.validate_dependencies();
568 assert!(result.is_err());
569 }
570
571 #[test]
572 fn test_topological_sort() {
573 let mut analyzer = DependencyAnalyzer::new();
574 analyzer.add_project(create_test_project("project-a"));
575 analyzer.add_project(create_test_project("project-b"));
576 analyzer.add_project(create_test_project("project-c"));
577
578 analyzer.add_dependency(ProjectDependency {
580 from: "project-a".to_string(),
581 to: "project-b".to_string(),
582 dependency_type: DependencyType::Direct,
583 version_constraint: "^0.1.0".to_string(),
584 });
585
586 analyzer.add_dependency(ProjectDependency {
587 from: "project-b".to_string(),
588 to: "project-c".to_string(),
589 dependency_type: DependencyType::Direct,
590 version_constraint: "^0.1.0".to_string(),
591 });
592
593 let sorted = analyzer.topological_sort().unwrap();
594 assert_eq!(sorted.len(), 3);
595
596 let a_idx = sorted.iter().position(|x| x == "project-a").unwrap();
598 let b_idx = sorted.iter().position(|x| x == "project-b").unwrap();
599 let c_idx = sorted.iter().position(|x| x == "project-c").unwrap();
600
601 assert!(a_idx < b_idx);
602 assert!(b_idx < c_idx);
603 }
604
605 #[test]
606 fn test_topological_sort_with_cycle() {
607 let mut analyzer = DependencyAnalyzer::new();
608 analyzer.add_project(create_test_project("project-a"));
609 analyzer.add_project(create_test_project("project-b"));
610
611 analyzer.add_dependency(ProjectDependency {
613 from: "project-a".to_string(),
614 to: "project-b".to_string(),
615 dependency_type: DependencyType::Direct,
616 version_constraint: "^0.1.0".to_string(),
617 });
618
619 analyzer.add_dependency(ProjectDependency {
620 from: "project-b".to_string(),
621 to: "project-a".to_string(),
622 dependency_type: DependencyType::Direct,
623 version_constraint: "^0.1.0".to_string(),
624 });
625
626 let result = analyzer.topological_sort();
627 assert!(result.is_err());
628 }
629
630 #[test]
631 fn test_get_adjacency_list() {
632 let mut analyzer = DependencyAnalyzer::new();
633 analyzer.add_project(create_test_project("project-a"));
634 analyzer.add_project(create_test_project("project-b"));
635 analyzer.add_project(create_test_project("project-c"));
636
637 analyzer.add_dependency(ProjectDependency {
638 from: "project-a".to_string(),
639 to: "project-b".to_string(),
640 dependency_type: DependencyType::Direct,
641 version_constraint: "^0.1.0".to_string(),
642 });
643
644 analyzer.add_dependency(ProjectDependency {
645 from: "project-a".to_string(),
646 to: "project-c".to_string(),
647 dependency_type: DependencyType::Direct,
648 version_constraint: "^0.1.0".to_string(),
649 });
650
651 let adj_list = analyzer.get_adjacency_list();
652 assert_eq!(adj_list.len(), 3);
653 assert_eq!(adj_list.get("project-a").unwrap().len(), 2);
654 assert_eq!(adj_list.get("project-b").unwrap().len(), 0);
655 assert_eq!(adj_list.get("project-c").unwrap().len(), 0);
656 }
657
658 #[test]
659 fn test_clear() {
660 let mut analyzer = DependencyAnalyzer::new();
661 analyzer.add_project(create_test_project("project-a"));
662
663 assert_eq!(analyzer.get_projects().len(), 1);
664
665 analyzer.clear();
666
667 assert_eq!(analyzer.get_projects().len(), 0);
668 }
669}