1use crate::error::{OrchestrationError, Result};
4use crate::models::Project;
5use crate::analyzers::{Version, VersionValidator, DependencyGraph};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Clone)]
10pub struct VersionCoordinator {
11 dependency_graph: DependencyGraph,
13
14 project_versions: HashMap<String, String>,
16
17 version_constraints: HashMap<String, Vec<String>>,
19}
20
21#[derive(Debug, Clone)]
23pub struct VersionUpdateResult {
24 pub project: String,
26
27 pub old_version: String,
29
30 pub new_version: String,
32
33 pub affected_projects: Vec<String>,
35
36 pub success: bool,
38
39 pub error: Option<String>,
41}
42
43#[derive(Debug, Clone)]
45pub struct VersionUpdatePlan {
46 pub updates: Vec<VersionUpdateStep>,
48
49 pub total_affected: usize,
51
52 pub is_valid: bool,
54
55 pub validation_errors: Vec<String>,
57}
58
59#[derive(Debug, Clone)]
61pub struct VersionUpdateStep {
62 pub project: String,
64
65 pub new_version: String,
67
68 pub dependents: Vec<String>,
70
71 pub is_breaking: bool,
73}
74
75impl VersionCoordinator {
76 pub fn new(dependency_graph: DependencyGraph) -> Self {
78 Self {
79 dependency_graph,
80 project_versions: HashMap::new(),
81 version_constraints: HashMap::new(),
82 }
83 }
84
85 pub fn register_project(&mut self, project: &Project) {
87 self.project_versions.insert(project.name.clone(), project.version.clone());
88 }
89
90 pub fn register_constraint(&mut self, project: &str, constraint: String) {
92 self.version_constraints
93 .entry(project.to_string())
94 .or_default()
95 .push(constraint);
96 }
97
98 pub fn update_version(
100 &mut self,
101 project: &str,
102 new_version: &str,
103 ) -> Result<VersionUpdateResult> {
104 Version::parse(new_version)?;
106
107 let old_version = self
109 .project_versions
110 .get(project)
111 .cloned()
112 .ok_or_else(|| OrchestrationError::ProjectNotFound(project.to_string()))?;
113
114 let _is_breaking = VersionValidator::is_breaking_change(&old_version, new_version)?;
116
117 let dependents = self.dependency_graph.get_dependents(project);
119
120 if let Some(constraints) = self.version_constraints.get(project) {
122 VersionValidator::validate_update(&old_version, new_version,
123 &constraints.iter().map(|s| s.as_str()).collect::<Vec<_>>())?;
124 }
125
126 self.project_versions.insert(project.to_string(), new_version.to_string());
128
129 Ok(VersionUpdateResult {
130 project: project.to_string(),
131 old_version,
132 new_version: new_version.to_string(),
133 affected_projects: dependents,
134 success: true,
135 error: None,
136 })
137 }
138
139 pub fn plan_version_updates(
141 &self,
142 updates: Vec<(String, String)>,
143 ) -> Result<VersionUpdatePlan> {
144 let mut plan = VersionUpdatePlan {
145 updates: Vec::new(),
146 total_affected: 0,
147 is_valid: true,
148 validation_errors: Vec::new(),
149 };
150
151 let mut affected_projects = HashSet::new();
152 let mut processed = HashSet::new();
153
154 for (project, new_version) in updates {
155 if let Err(e) = Version::parse(&new_version) {
157 plan.is_valid = false;
158 plan.validation_errors.push(format!(
159 "Invalid version for {}: {}",
160 project, e
161 ));
162 continue;
163 }
164
165 let old_version = match self.project_versions.get(&project) {
167 Some(v) => v.clone(),
168 None => {
169 plan.is_valid = false;
170 plan.validation_errors.push(format!("Project not found: {}", project));
171 continue;
172 }
173 };
174
175 let is_breaking = match VersionValidator::is_breaking_change(&old_version, &new_version) {
177 Ok(b) => b,
178 Err(e) => {
179 plan.is_valid = false;
180 plan.validation_errors.push(format!(
181 "Failed to check breaking change for {}: {}",
182 project, e
183 ));
184 continue;
185 }
186 };
187
188 let dependents = self.dependency_graph.get_dependents(&project);
190 affected_projects.extend(dependents.clone());
191
192 plan.updates.push(VersionUpdateStep {
194 project: project.clone(),
195 new_version,
196 dependents,
197 is_breaking,
198 });
199
200 processed.insert(project);
201 }
202
203 plan.total_affected = affected_projects.len();
204 Ok(plan)
205 }
206
207 pub fn get_affected_projects(&self, project: &str) -> Vec<String> {
209 self.dependency_graph.get_dependents(project)
210 }
211
212 pub fn validate_version_update(
214 &self,
215 project: &str,
216 new_version: &str,
217 ) -> Result<bool> {
218 let current_version = self
220 .project_versions
221 .get(project)
222 .ok_or_else(|| OrchestrationError::ProjectNotFound(project.to_string()))?;
223
224 let constraints = self
226 .version_constraints
227 .get(project)
228 .map(|c| c.iter().map(|s| s.as_str()).collect::<Vec<_>>())
229 .unwrap_or_default();
230
231 VersionValidator::validate_update(current_version, new_version, &constraints)
233 }
234
235 pub fn get_version(&self, project: &str) -> Option<String> {
237 self.project_versions.get(project).cloned()
238 }
239
240 pub fn get_constraints(&self, project: &str) -> Vec<String> {
242 self.version_constraints
243 .get(project)
244 .cloned()
245 .unwrap_or_default()
246 }
247
248 pub fn is_breaking_change(&self, project: &str, new_version: &str) -> Result<bool> {
250 let current_version = self
251 .project_versions
252 .get(project)
253 .ok_or_else(|| OrchestrationError::ProjectNotFound(project.to_string()))?;
254
255 VersionValidator::is_breaking_change(current_version, new_version)
256 }
257
258 pub fn dependency_graph(&self) -> &DependencyGraph {
260 &self.dependency_graph
261 }
262
263 pub fn get_all_projects(&self) -> Vec<String> {
265 self.project_versions.keys().cloned().collect()
266 }
267
268 pub fn clear(&mut self) {
270 self.project_versions.clear();
271 self.version_constraints.clear();
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 fn create_test_coordinator() -> VersionCoordinator {
280 let graph = DependencyGraph::new(false);
281 VersionCoordinator::new(graph)
282 }
283
284 #[test]
285 fn test_version_coordinator_creation() {
286 let coordinator = create_test_coordinator();
287 assert_eq!(coordinator.get_all_projects().len(), 0);
288 }
289
290 #[test]
291 fn test_register_project() {
292 let mut coordinator = create_test_coordinator();
293 let project = Project {
294 path: std::path::PathBuf::from("/path/to/project"),
295 name: "test-project".to_string(),
296 project_type: "rust".to_string(),
297 version: "1.0.0".to_string(),
298 status: crate::models::ProjectStatus::Healthy,
299 };
300
301 coordinator.register_project(&project);
302 assert_eq!(coordinator.get_version("test-project"), Some("1.0.0".to_string()));
303 }
304
305 #[test]
306 fn test_register_constraint() {
307 let mut coordinator = create_test_coordinator();
308 coordinator.register_constraint("test-project", "^1.0.0".to_string());
309
310 let constraints = coordinator.get_constraints("test-project");
311 assert_eq!(constraints.len(), 1);
312 assert_eq!(constraints[0], "^1.0.0");
313 }
314
315 #[test]
316 fn test_update_version_success() {
317 let mut coordinator = create_test_coordinator();
318 let project = Project {
319 path: std::path::PathBuf::from("/path/to/project"),
320 name: "test-project".to_string(),
321 project_type: "rust".to_string(),
322 version: "1.0.0".to_string(),
323 status: crate::models::ProjectStatus::Healthy,
324 };
325
326 coordinator.register_project(&project);
327 let result = coordinator.update_version("test-project", "1.1.0").unwrap();
328
329 assert!(result.success);
330 assert_eq!(result.old_version, "1.0.0");
331 assert_eq!(result.new_version, "1.1.0");
332 assert_eq!(coordinator.get_version("test-project"), Some("1.1.0".to_string()));
333 }
334
335 #[test]
336 fn test_update_version_invalid_format() {
337 let mut coordinator = create_test_coordinator();
338 let project = Project {
339 path: std::path::PathBuf::from("/path/to/project"),
340 name: "test-project".to_string(),
341 project_type: "rust".to_string(),
342 version: "1.0.0".to_string(),
343 status: crate::models::ProjectStatus::Healthy,
344 };
345
346 coordinator.register_project(&project);
347 let result = coordinator.update_version("test-project", "invalid");
348
349 assert!(result.is_err());
350 }
351
352 #[test]
353 fn test_update_version_not_found() {
354 let mut coordinator = create_test_coordinator();
355 let result = coordinator.update_version("nonexistent", "1.0.0");
356
357 assert!(result.is_err());
358 }
359
360 #[test]
361 fn test_validate_version_update() {
362 let mut coordinator = create_test_coordinator();
363 let project = Project {
364 path: std::path::PathBuf::from("/path/to/project"),
365 name: "test-project".to_string(),
366 project_type: "rust".to_string(),
367 version: "1.0.0".to_string(),
368 status: crate::models::ProjectStatus::Healthy,
369 };
370
371 coordinator.register_project(&project);
372 coordinator.register_constraint("test-project", "^1.0.0".to_string());
373
374 assert!(coordinator.validate_version_update("test-project", "1.1.0").unwrap());
376
377 assert!(coordinator.validate_version_update("test-project", "2.0.0").is_err());
379 }
380
381 #[test]
382 fn test_is_breaking_change() {
383 let mut coordinator = create_test_coordinator();
384 let project = Project {
385 path: std::path::PathBuf::from("/path/to/project"),
386 name: "test-project".to_string(),
387 project_type: "rust".to_string(),
388 version: "1.0.0".to_string(),
389 status: crate::models::ProjectStatus::Healthy,
390 };
391
392 coordinator.register_project(&project);
393
394 assert!(!coordinator.is_breaking_change("test-project", "1.1.0").unwrap());
396
397 assert!(coordinator.is_breaking_change("test-project", "2.0.0").unwrap());
399 }
400
401 #[test]
402 fn test_plan_version_updates() {
403 let mut coordinator = create_test_coordinator();
404 let project = Project {
405 path: std::path::PathBuf::from("/path/to/project"),
406 name: "test-project".to_string(),
407 project_type: "rust".to_string(),
408 version: "1.0.0".to_string(),
409 status: crate::models::ProjectStatus::Healthy,
410 };
411
412 coordinator.register_project(&project);
413
414 let updates = vec![("test-project".to_string(), "1.1.0".to_string())];
415 let plan = coordinator.plan_version_updates(updates).unwrap();
416
417 assert!(plan.is_valid);
418 assert_eq!(plan.updates.len(), 1);
419 assert_eq!(plan.updates[0].project, "test-project");
420 assert_eq!(plan.updates[0].new_version, "1.1.0");
421 }
422
423 #[test]
424 fn test_plan_version_updates_invalid_version() {
425 let coordinator = create_test_coordinator();
426 let updates = vec![("test-project".to_string(), "invalid".to_string())];
427 let plan = coordinator.plan_version_updates(updates).unwrap();
428
429 assert!(!plan.is_valid);
430 assert!(!plan.validation_errors.is_empty());
431 }
432
433 #[test]
434 fn test_plan_version_updates_missing_project() {
435 let coordinator = create_test_coordinator();
436 let updates = vec![("nonexistent".to_string(), "1.0.0".to_string())];
437 let plan = coordinator.plan_version_updates(updates).unwrap();
438
439 assert!(!plan.is_valid);
440 assert!(!plan.validation_errors.is_empty());
441 }
442
443 #[test]
444 fn test_get_affected_projects() {
445 let coordinator = create_test_coordinator();
446 let affected = coordinator.get_affected_projects("test-project");
447 assert_eq!(affected.len(), 0);
448 }
449
450 #[test]
451 fn test_clear() {
452 let mut coordinator = create_test_coordinator();
453 let project = Project {
454 path: std::path::PathBuf::from("/path/to/project"),
455 name: "test-project".to_string(),
456 project_type: "rust".to_string(),
457 version: "1.0.0".to_string(),
458 status: crate::models::ProjectStatus::Healthy,
459 };
460
461 coordinator.register_project(&project);
462 coordinator.register_constraint("test-project", "^1.0.0".to_string());
463
464 assert_eq!(coordinator.get_all_projects().len(), 1);
465
466 coordinator.clear();
467 assert_eq!(coordinator.get_all_projects().len(), 0);
468 assert_eq!(coordinator.get_constraints("test-project").len(), 0);
469 }
470
471 #[test]
472 fn test_multiple_projects() {
473 let mut coordinator = create_test_coordinator();
474
475 let project1 = Project {
476 path: std::path::PathBuf::from("/path/to/project1"),
477 name: "project1".to_string(),
478 project_type: "rust".to_string(),
479 version: "1.0.0".to_string(),
480 status: crate::models::ProjectStatus::Healthy,
481 };
482
483 let project2 = Project {
484 path: std::path::PathBuf::from("/path/to/project2"),
485 name: "project2".to_string(),
486 project_type: "rust".to_string(),
487 version: "2.0.0".to_string(),
488 status: crate::models::ProjectStatus::Healthy,
489 };
490
491 coordinator.register_project(&project1);
492 coordinator.register_project(&project2);
493
494 assert_eq!(coordinator.get_all_projects().len(), 2);
495 assert_eq!(coordinator.get_version("project1"), Some("1.0.0".to_string()));
496 assert_eq!(coordinator.get_version("project2"), Some("2.0.0".to_string()));
497 }
498
499 #[test]
500 fn test_version_update_step_creation() {
501 let step = VersionUpdateStep {
502 project: "test-project".to_string(),
503 new_version: "1.1.0".to_string(),
504 dependents: vec!["dependent1".to_string()],
505 is_breaking: false,
506 };
507
508 assert_eq!(step.project, "test-project");
509 assert_eq!(step.new_version, "1.1.0");
510 assert_eq!(step.dependents.len(), 1);
511 assert!(!step.is_breaking);
512 }
513
514 #[test]
515 fn test_version_update_result_creation() {
516 let result = VersionUpdateResult {
517 project: "test-project".to_string(),
518 old_version: "1.0.0".to_string(),
519 new_version: "1.1.0".to_string(),
520 affected_projects: vec!["dependent1".to_string()],
521 success: true,
522 error: None,
523 };
524
525 assert_eq!(result.project, "test-project");
526 assert_eq!(result.old_version, "1.0.0");
527 assert_eq!(result.new_version, "1.1.0");
528 assert!(result.success);
529 assert!(result.error.is_none());
530 }
531}