1use crate::error::{OrchestrationError, Result};
4use crate::models::{Project, ProjectDependency};
5use std::collections::HashMap;
6
7use super::version_validator::VersionValidator;
8
9#[derive(Debug, Clone)]
11pub struct DependencyValidator {
12 project_versions: HashMap<String, String>,
14
15 dependencies: HashMap<String, Vec<ProjectDependency>>,
17}
18
19impl DependencyValidator {
20 pub fn new() -> Self {
22 Self {
23 project_versions: HashMap::new(),
24 dependencies: HashMap::new(),
25 }
26 }
27
28 pub fn register_project(&mut self, project: &Project) {
30 self.project_versions
31 .insert(project.name.clone(), project.version.clone());
32 }
33
34 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 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 pub fn validate_single_dependency(
54 &self,
55 _from_project: &str,
56 dependency: &ProjectDependency,
57 ) -> Result<()> {
58 let target_version = self
60 .project_versions
61 .get(&dependency.to)
62 .ok_or_else(|| OrchestrationError::ProjectNotFound(dependency.to.clone()))?;
63
64 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 pub fn validate_version_update(
79 &self,
80 project_name: &str,
81 new_version: &str,
82 ) -> Result<()> {
83 let dependents = self.get_dependents(project_name);
85
86 if dependents.is_empty() {
87 return Ok(());
89 }
90
91 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 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 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 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 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 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 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 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 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 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 for dependent_name in self.get_dependents(project_name) {
204 report.dependents.push(dependent_name);
205 }
206
207 if let Err(e) = self.validate_all_dependencies() {
209 report.issues.push(e.to_string());
210 }
211
212 Ok(report)
213 }
214
215 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#[derive(Debug, Clone)]
230pub struct DependencyInfo {
231 pub target: String,
233
234 pub constraint: String,
236
237 pub satisfied: bool,
239}
240
241#[derive(Debug, Clone)]
243pub struct ValidationReport {
244 pub project: String,
246
247 pub version: String,
249
250 pub dependencies: Vec<DependencyInfo>,
252
253 pub dependents: Vec<String>,
255
256 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}