ricecoder_orchestration/analyzers/
dependency_validator.rs

1//! Dependency validation and compatibility checking
2
3use crate::error::{OrchestrationError, Result};
4use crate::models::{Project, ProjectDependency};
5use std::collections::HashMap;
6
7use super::version_validator::VersionValidator;
8
9/// Validates dependency compatibility and version constraints
10#[derive(Debug, Clone)]
11pub struct DependencyValidator {
12    /// Map of project names to their versions
13    project_versions: HashMap<String, String>,
14
15    /// Map of project names to their dependencies
16    dependencies: HashMap<String, Vec<ProjectDependency>>,
17}
18
19impl DependencyValidator {
20    /// Creates a new dependency validator
21    pub fn new() -> Self {
22        Self {
23            project_versions: HashMap::new(),
24            dependencies: HashMap::new(),
25        }
26    }
27
28    /// Registers a project with its version
29    pub fn register_project(&mut self, project: &Project) {
30        self.project_versions
31            .insert(project.name.clone(), project.version.clone());
32    }
33
34    /// Registers a dependency
35    pub fn register_dependency(&mut self, dependency: ProjectDependency) {
36        self.dependencies
37            .entry(dependency.from.clone())
38            .or_default()
39            .push(dependency);
40    }
41
42    /// Validates that all dependencies have compatible versions
43    pub fn validate_all_dependencies(&self) -> Result<()> {
44        for (project_name, deps) in &self.dependencies {
45            for dep in deps {
46                self.validate_single_dependency(project_name, dep)?;
47            }
48        }
49        Ok(())
50    }
51
52    /// Validates a single dependency
53    pub fn validate_single_dependency(
54        &self,
55        _from_project: &str,
56        dependency: &ProjectDependency,
57    ) -> Result<()> {
58        // Check if the target project exists
59        let target_version = self
60            .project_versions
61            .get(&dependency.to)
62            .ok_or_else(|| OrchestrationError::ProjectNotFound(dependency.to.clone()))?;
63
64        // Validate version constraint
65        let is_compatible = VersionValidator::is_compatible(&dependency.version_constraint, target_version)?;
66        
67        if !is_compatible {
68            return Err(OrchestrationError::DependencyValidationFailed(format!(
69                "Version {} does not satisfy constraint {}",
70                target_version, dependency.version_constraint
71            )));
72        }
73
74        Ok(())
75    }
76
77    /// Validates a version update for a project
78    pub fn validate_version_update(
79        &self,
80        project_name: &str,
81        new_version: &str,
82    ) -> Result<()> {
83        // Get all projects that depend on this project
84        let dependents = self.get_dependents(project_name);
85
86        if dependents.is_empty() {
87            // No dependents, update is always valid
88            return Ok(());
89        }
90
91        // Collect all constraints from dependents
92        let mut constraints = Vec::new();
93        for dependent_name in dependents {
94            if let Some(deps) = self.dependencies.get(&dependent_name) {
95                for dep in deps {
96                    if dep.to == project_name {
97                        constraints.push(dep.version_constraint.clone());
98                    }
99                }
100            }
101        }
102
103        // Validate the new version against all constraints
104        let constraint_strs: Vec<&str> = constraints.iter().map(|s| s.as_str()).collect();
105
106        let current_version = self
107            .project_versions
108            .get(project_name)
109            .ok_or_else(|| OrchestrationError::ProjectNotFound(project_name.to_string()))?;
110
111        VersionValidator::validate_update(current_version, new_version, &constraint_strs)?;
112
113        Ok(())
114    }
115
116    /// Checks if a version update is a breaking change
117    pub fn is_breaking_change(&self, project_name: &str, new_version: &str) -> Result<bool> {
118        let current_version = self
119            .project_versions
120            .get(project_name)
121            .ok_or_else(|| OrchestrationError::ProjectNotFound(project_name.to_string()))?;
122
123        VersionValidator::is_breaking_change(current_version, new_version)
124    }
125
126    /// Gets all projects that depend on a given project
127    pub fn get_dependents(&self, project_name: &str) -> Vec<String> {
128        let mut dependents = Vec::new();
129
130        for (from, deps) in &self.dependencies {
131            for dep in deps {
132                if dep.to == project_name {
133                    dependents.push(from.clone());
134                }
135            }
136        }
137
138        dependents
139    }
140
141    /// Gets all projects that a given project depends on
142    pub fn get_dependencies(&self, project_name: &str) -> Vec<ProjectDependency> {
143        self.dependencies
144            .get(project_name)
145            .cloned()
146            .unwrap_or_default()
147    }
148
149    /// Validates that a new version doesn't break any dependents
150    pub fn validate_no_breaking_changes(&self, project_name: &str, new_version: &str) -> Result<()> {
151        if !self.is_breaking_change(project_name, new_version)? {
152            return Ok(());
153        }
154
155        // Breaking change detected - check if any dependents use exact or restrictive constraints
156        let dependents = self.get_dependents(project_name);
157
158        for dependent_name in dependents {
159            if let Some(deps) = self.dependencies.get(&dependent_name) {
160                for dep in deps {
161                    if dep.to == project_name {
162                        // Check if the constraint would reject the new version
163                        if !VersionValidator::is_compatible(&dep.version_constraint, new_version)? {
164                            return Err(OrchestrationError::DependencyValidationFailed(format!(
165                                "Breaking change in {} would break dependent project {} (constraint: {})",
166                                project_name, dependent_name, dep.version_constraint
167                            )));
168                        }
169                    }
170                }
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Gets validation report for a project
178    pub fn get_validation_report(&self, project_name: &str) -> Result<ValidationReport> {
179        let mut report = ValidationReport {
180            project: project_name.to_string(),
181            version: self
182                .project_versions
183                .get(project_name)
184                .cloned()
185                .unwrap_or_default(),
186            dependencies: Vec::new(),
187            dependents: Vec::new(),
188            issues: Vec::new(),
189        };
190
191        // Add dependencies
192        if let Some(deps) = self.dependencies.get(project_name) {
193            for dep in deps {
194                report.dependencies.push(DependencyInfo {
195                    target: dep.to.clone(),
196                    constraint: dep.version_constraint.clone(),
197                    satisfied: self.validate_single_dependency(project_name, dep).is_ok(),
198                });
199            }
200        }
201
202        // Add dependents
203        for dependent_name in self.get_dependents(project_name) {
204            report.dependents.push(dependent_name);
205        }
206
207        // Validate and collect issues
208        if let Err(e) = self.validate_all_dependencies() {
209            report.issues.push(e.to_string());
210        }
211
212        Ok(report)
213    }
214
215    /// Clears all registered data
216    pub fn clear(&mut self) {
217        self.project_versions.clear();
218        self.dependencies.clear();
219    }
220}
221
222impl Default for DependencyValidator {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228/// Information about a dependency
229#[derive(Debug, Clone)]
230pub struct DependencyInfo {
231    /// Target project name
232    pub target: String,
233
234    /// Version constraint
235    pub constraint: String,
236
237    /// Whether the constraint is satisfied
238    pub satisfied: bool,
239}
240
241/// Validation report for a project
242#[derive(Debug, Clone)]
243pub struct ValidationReport {
244    /// Project name
245    pub project: String,
246
247    /// Project version
248    pub version: String,
249
250    /// Dependencies of this project
251    pub dependencies: Vec<DependencyInfo>,
252
253    /// Projects that depend on this project
254    pub dependents: Vec<String>,
255
256    /// Validation issues
257    pub issues: Vec<String>,
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::models::{DependencyType, ProjectStatus};
264    use std::path::PathBuf;
265
266    fn create_test_project(name: &str, version: &str) -> Project {
267        Project {
268            path: PathBuf::from(format!("/path/to/{}", name)),
269            name: name.to_string(),
270            project_type: "rust".to_string(),
271            version: version.to_string(),
272            status: ProjectStatus::Healthy,
273        }
274    }
275
276    #[test]
277    fn test_register_project() {
278        let mut validator = DependencyValidator::new();
279        let project = create_test_project("project-a", "1.2.3");
280
281        validator.register_project(&project);
282
283        assert_eq!(validator.project_versions.get("project-a"), Some(&"1.2.3".to_string()));
284    }
285
286    #[test]
287    fn test_register_dependency() {
288        let mut validator = DependencyValidator::new();
289        let dep = ProjectDependency {
290            from: "project-a".to_string(),
291            to: "project-b".to_string(),
292            dependency_type: DependencyType::Direct,
293            version_constraint: "^1.0.0".to_string(),
294        };
295
296        validator.register_dependency(dep);
297
298        assert_eq!(validator.dependencies.get("project-a").unwrap().len(), 1);
299    }
300
301    #[test]
302    fn test_validate_single_dependency_success() {
303        let mut validator = DependencyValidator::new();
304        validator.register_project(&create_test_project("project-a", "1.0.0"));
305        validator.register_project(&create_test_project("project-b", "1.2.3"));
306
307        let dep = ProjectDependency {
308            from: "project-a".to_string(),
309            to: "project-b".to_string(),
310            dependency_type: DependencyType::Direct,
311            version_constraint: "^1.0.0".to_string(),
312        };
313
314        let result = validator.validate_single_dependency("project-a", &dep);
315        assert!(result.is_ok());
316    }
317
318    #[test]
319    fn test_validate_single_dependency_missing_target() {
320        let mut validator = DependencyValidator::new();
321        validator.register_project(&create_test_project("project-a", "1.0.0"));
322
323        let dep = ProjectDependency {
324            from: "project-a".to_string(),
325            to: "project-b".to_string(),
326            dependency_type: DependencyType::Direct,
327            version_constraint: "^1.0.0".to_string(),
328        };
329
330        let result = validator.validate_single_dependency("project-a", &dep);
331        assert!(result.is_err());
332    }
333
334    #[test]
335    fn test_validate_single_dependency_incompatible_version() {
336        let mut validator = DependencyValidator::new();
337        validator.register_project(&create_test_project("project-a", "1.0.0"));
338        validator.register_project(&create_test_project("project-b", "2.0.0"));
339
340        let dep = ProjectDependency {
341            from: "project-a".to_string(),
342            to: "project-b".to_string(),
343            dependency_type: DependencyType::Direct,
344            version_constraint: "^1.0.0".to_string(),
345        };
346
347        let result = validator.validate_single_dependency("project-a", &dep);
348        assert!(result.is_err());
349    }
350
351    #[test]
352    fn test_validate_all_dependencies() {
353        let mut validator = DependencyValidator::new();
354        validator.register_project(&create_test_project("project-a", "1.0.0"));
355        validator.register_project(&create_test_project("project-b", "1.2.3"));
356        validator.register_project(&create_test_project("project-c", "1.5.0"));
357
358        validator.register_dependency(ProjectDependency {
359            from: "project-a".to_string(),
360            to: "project-b".to_string(),
361            dependency_type: DependencyType::Direct,
362            version_constraint: "^1.0.0".to_string(),
363        });
364
365        validator.register_dependency(ProjectDependency {
366            from: "project-b".to_string(),
367            to: "project-c".to_string(),
368            dependency_type: DependencyType::Direct,
369            version_constraint: "^1.0.0".to_string(),
370        });
371
372        let result = validator.validate_all_dependencies();
373        assert!(result.is_ok());
374    }
375
376    #[test]
377    fn test_validate_version_update_compatible() {
378        let mut validator = DependencyValidator::new();
379        validator.register_project(&create_test_project("project-a", "1.0.0"));
380        validator.register_project(&create_test_project("project-b", "1.2.3"));
381
382        validator.register_dependency(ProjectDependency {
383            from: "project-b".to_string(),
384            to: "project-a".to_string(),
385            dependency_type: DependencyType::Direct,
386            version_constraint: "^1.0.0".to_string(),
387        });
388
389        let result = validator.validate_version_update("project-a", "1.2.4");
390        assert!(result.is_ok());
391    }
392
393    #[test]
394    fn test_validate_version_update_incompatible() {
395        let mut validator = DependencyValidator::new();
396        validator.register_project(&create_test_project("project-a", "1.0.0"));
397        validator.register_project(&create_test_project("project-b", "1.2.3"));
398
399        validator.register_dependency(ProjectDependency {
400            from: "project-b".to_string(),
401            to: "project-a".to_string(),
402            dependency_type: DependencyType::Direct,
403            version_constraint: "^1.0.0".to_string(),
404        });
405
406        let result = validator.validate_version_update("project-a", "2.0.0");
407        assert!(result.is_err());
408    }
409
410    #[test]
411    fn test_is_breaking_change() {
412        let mut validator = DependencyValidator::new();
413        validator.register_project(&create_test_project("project-a", "1.0.0"));
414
415        assert!(!validator.is_breaking_change("project-a", "1.2.3").unwrap());
416        assert!(validator.is_breaking_change("project-a", "2.0.0").unwrap());
417    }
418
419    #[test]
420    fn test_get_dependents() {
421        let mut validator = DependencyValidator::new();
422        validator.register_project(&create_test_project("project-a", "1.0.0"));
423        validator.register_project(&create_test_project("project-b", "1.0.0"));
424        validator.register_project(&create_test_project("project-c", "1.0.0"));
425
426        validator.register_dependency(ProjectDependency {
427            from: "project-b".to_string(),
428            to: "project-a".to_string(),
429            dependency_type: DependencyType::Direct,
430            version_constraint: "^1.0.0".to_string(),
431        });
432
433        validator.register_dependency(ProjectDependency {
434            from: "project-c".to_string(),
435            to: "project-a".to_string(),
436            dependency_type: DependencyType::Direct,
437            version_constraint: "^1.0.0".to_string(),
438        });
439
440        let dependents = validator.get_dependents("project-a");
441        assert_eq!(dependents.len(), 2);
442        assert!(dependents.contains(&"project-b".to_string()));
443        assert!(dependents.contains(&"project-c".to_string()));
444    }
445
446    #[test]
447    fn test_get_dependencies() {
448        let mut validator = DependencyValidator::new();
449        validator.register_project(&create_test_project("project-a", "1.0.0"));
450        validator.register_project(&create_test_project("project-b", "1.0.0"));
451
452        validator.register_dependency(ProjectDependency {
453            from: "project-a".to_string(),
454            to: "project-b".to_string(),
455            dependency_type: DependencyType::Direct,
456            version_constraint: "^1.0.0".to_string(),
457        });
458
459        let deps = validator.get_dependencies("project-a");
460        assert_eq!(deps.len(), 1);
461        assert_eq!(deps[0].to, "project-b");
462    }
463
464    #[test]
465    fn test_validate_no_breaking_changes_non_breaking() {
466        let mut validator = DependencyValidator::new();
467        validator.register_project(&create_test_project("project-a", "1.0.0"));
468        validator.register_project(&create_test_project("project-b", "1.0.0"));
469
470        validator.register_dependency(ProjectDependency {
471            from: "project-b".to_string(),
472            to: "project-a".to_string(),
473            dependency_type: DependencyType::Direct,
474            version_constraint: "^1.0.0".to_string(),
475        });
476
477        let result = validator.validate_no_breaking_changes("project-a", "1.2.3");
478        assert!(result.is_ok());
479    }
480
481    #[test]
482    fn test_validate_no_breaking_changes_breaking() {
483        let mut validator = DependencyValidator::new();
484        validator.register_project(&create_test_project("project-a", "1.0.0"));
485        validator.register_project(&create_test_project("project-b", "1.0.0"));
486
487        validator.register_dependency(ProjectDependency {
488            from: "project-b".to_string(),
489            to: "project-a".to_string(),
490            dependency_type: DependencyType::Direct,
491            version_constraint: "^1.0.0".to_string(),
492        });
493
494        let result = validator.validate_no_breaking_changes("project-a", "2.0.0");
495        assert!(result.is_err());
496    }
497
498    #[test]
499    fn test_get_validation_report() {
500        let mut validator = DependencyValidator::new();
501        validator.register_project(&create_test_project("project-a", "1.0.0"));
502        validator.register_project(&create_test_project("project-b", "1.2.3"));
503
504        validator.register_dependency(ProjectDependency {
505            from: "project-a".to_string(),
506            to: "project-b".to_string(),
507            dependency_type: DependencyType::Direct,
508            version_constraint: "^1.0.0".to_string(),
509        });
510
511        let report = validator.get_validation_report("project-a").unwrap();
512        assert_eq!(report.project, "project-a");
513        assert_eq!(report.version, "1.0.0");
514        assert_eq!(report.dependencies.len(), 1);
515    }
516
517    #[test]
518    fn test_clear() {
519        let mut validator = DependencyValidator::new();
520        validator.register_project(&create_test_project("project-a", "1.0.0"));
521
522        assert_eq!(validator.project_versions.len(), 1);
523
524        validator.clear();
525
526        assert_eq!(validator.project_versions.len(), 0);
527    }
528}