ricecoder_orchestration/managers/
version_coordinator.rs

1//! Version coordination across dependent projects
2
3use crate::error::{OrchestrationError, Result};
4use crate::models::Project;
5use crate::analyzers::{Version, VersionValidator, DependencyGraph};
6use std::collections::{HashMap, HashSet};
7
8/// Coordinates version updates across dependent projects
9#[derive(Debug, Clone)]
10pub struct VersionCoordinator {
11    /// Dependency graph for tracking project relationships
12    dependency_graph: DependencyGraph,
13
14    /// Current versions of all projects
15    project_versions: HashMap<String, String>,
16
17    /// Version constraints for each project
18    version_constraints: HashMap<String, Vec<String>>,
19}
20
21/// Result of a version update operation
22#[derive(Debug, Clone)]
23pub struct VersionUpdateResult {
24    /// Project that was updated
25    pub project: String,
26
27    /// Old version
28    pub old_version: String,
29
30    /// New version
31    pub new_version: String,
32
33    /// Projects that need to be updated as a result
34    pub affected_projects: Vec<String>,
35
36    /// Whether the update was successful
37    pub success: bool,
38
39    /// Error message if update failed
40    pub error: Option<String>,
41}
42
43/// Plan for coordinating version updates
44#[derive(Debug, Clone)]
45pub struct VersionUpdatePlan {
46    /// Updates to be applied in order
47    pub updates: Vec<VersionUpdateStep>,
48
49    /// Total number of projects affected
50    pub total_affected: usize,
51
52    /// Whether the plan is valid
53    pub is_valid: bool,
54
55    /// Validation errors if any
56    pub validation_errors: Vec<String>,
57}
58
59/// A single step in a version update plan
60#[derive(Debug, Clone)]
61pub struct VersionUpdateStep {
62    /// Project to update
63    pub project: String,
64
65    /// New version to apply
66    pub new_version: String,
67
68    /// Projects that depend on this project
69    pub dependents: Vec<String>,
70
71    /// Whether this is a breaking change
72    pub is_breaking: bool,
73}
74
75impl VersionCoordinator {
76    /// Creates a new version coordinator
77    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    /// Registers a project with its current version
86    pub fn register_project(&mut self, project: &Project) {
87        self.project_versions.insert(project.name.clone(), project.version.clone());
88    }
89
90    /// Registers a dependency constraint
91    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    /// Updates a project version and propagates to dependents
99    pub fn update_version(
100        &mut self,
101        project: &str,
102        new_version: &str,
103    ) -> Result<VersionUpdateResult> {
104        // Validate the new version format
105        Version::parse(new_version)?;
106
107        // Get the old version
108        let old_version = self
109            .project_versions
110            .get(project)
111            .cloned()
112            .ok_or_else(|| OrchestrationError::ProjectNotFound(project.to_string()))?;
113
114        // Check if this is a breaking change
115        let _is_breaking = VersionValidator::is_breaking_change(&old_version, new_version)?;
116
117        // Get all projects that depend on this one
118        let dependents = self.dependency_graph.get_dependents(project);
119
120        // Validate that the new version is compatible with all dependent constraints
121        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        // Update the version
127        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    /// Creates a plan for coordinating version updates across projects
140    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            // Validate version format
156            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            // Get current version
166            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            // Check if breaking change
176            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            // Get dependents
189            let dependents = self.dependency_graph.get_dependents(&project);
190            affected_projects.extend(dependents.clone());
191
192            // Create update step
193            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    /// Gets all projects that need to be updated due to a change in the given project
208    pub fn get_affected_projects(&self, project: &str) -> Vec<String> {
209        self.dependency_graph.get_dependents(project)
210    }
211
212    /// Validates that a version update maintains all constraints
213    pub fn validate_version_update(
214        &self,
215        project: &str,
216        new_version: &str,
217    ) -> Result<bool> {
218        // Get current version
219        let current_version = self
220            .project_versions
221            .get(project)
222            .ok_or_else(|| OrchestrationError::ProjectNotFound(project.to_string()))?;
223
224        // Get all constraints for this project
225        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        // Validate the update
232        VersionValidator::validate_update(current_version, new_version, &constraints)
233    }
234
235    /// Gets the current version of a project
236    pub fn get_version(&self, project: &str) -> Option<String> {
237        self.project_versions.get(project).cloned()
238    }
239
240    /// Gets all version constraints for a project
241    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    /// Checks if a version update would be a breaking change
249    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    /// Gets the dependency graph
259    pub fn dependency_graph(&self) -> &DependencyGraph {
260        &self.dependency_graph
261    }
262
263    /// Gets all registered projects
264    pub fn get_all_projects(&self) -> Vec<String> {
265        self.project_versions.keys().cloned().collect()
266    }
267
268    /// Clears all registered projects and constraints
269    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        // Valid update
375        assert!(coordinator.validate_version_update("test-project", "1.1.0").unwrap());
376
377        // Invalid update (breaks constraint)
378        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        // Minor version change is not breaking
395        assert!(!coordinator.is_breaking_change("test-project", "1.1.0").unwrap());
396
397        // Major version change is breaking
398        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}