scirs2_core/versioning/
compatibility.rs

1//! # Backward Compatibility Checking
2//!
3//! Comprehensive backward compatibility checking and enforcement system
4//! for API evolution management in production environments.
5
6use super::{ApiVersion, Version};
7use crate::error::CoreError;
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12/// Compatibility levels between API versions
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub enum CompatibilityLevel {
15    /// Fully backward compatible
16    BackwardCompatible,
17    /// Compatible with minor changes
18    MostlyCompatible,
19    /// Some breaking changes but migration possible
20    PartiallyCompatible,
21    /// Major breaking changes requiring significant migration
22    BreakingChanges,
23    /// Incompatible - no migration path
24    Incompatible,
25}
26
27impl CompatibilityLevel {
28    /// Get the string representation
29    pub const fn as_str(&self) -> &'static str {
30        match self {
31            CompatibilityLevel::BackwardCompatible => "backward_compatible",
32            CompatibilityLevel::MostlyCompatible => "mostly_compatible",
33            CompatibilityLevel::PartiallyCompatible => "partially_compatible",
34            CompatibilityLevel::BreakingChanges => "breakingchanges",
35            CompatibilityLevel::Incompatible => "incompatible",
36        }
37    }
38
39    /// Check if migration is recommended
40    pub fn requires_migration(&self) -> bool {
41        matches!(
42            self,
43            CompatibilityLevel::PartiallyCompatible
44                | CompatibilityLevel::BreakingChanges
45                | CompatibilityLevel::Incompatible
46        )
47    }
48
49    /// Check if automatic migration is possible
50    pub fn supports_auto_migration(&self) -> bool {
51        matches!(
52            self,
53            CompatibilityLevel::BackwardCompatible
54                | CompatibilityLevel::MostlyCompatible
55                | CompatibilityLevel::PartiallyCompatible
56        )
57    }
58}
59
60/// Detailed compatibility report
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct CompatibilityReport {
63    /// Source version
64    pub from_version: Version,
65    /// Target version
66    pub toversion: Version,
67    /// Overall compatibility level
68    pub compatibility_level: CompatibilityLevel,
69    /// Detailed compatibility issues
70    pub issues: Vec<CompatibilityIssue>,
71    /// Breaking changes detected
72    pub breakingchanges: Vec<BreakingChange>,
73    /// Deprecated features that will be removed
74    pub deprecated_features: Vec<String>,
75    /// New features added
76    pub new_features: Vec<String>,
77    /// Migration recommendations
78    pub migration_recommendations: Vec<String>,
79    /// Estimated migration effort (in developer hours)
80    pub estimated_migration_effort: Option<u32>,
81}
82
83/// Specific compatibility issue
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CompatibilityIssue {
86    /// Issue severity
87    pub severity: IssueSeverity,
88    /// Component affected
89    pub component: String,
90    /// Issue description
91    pub description: String,
92    /// Suggested resolution
93    pub resolution: Option<String>,
94    /// Impact assessment
95    pub impact: ImpactLevel,
96}
97
98/// Issue severity levels
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
100pub enum IssueSeverity {
101    /// Informational - no action required
102    Info,
103    /// Warning - action recommended
104    Warning,
105    /// Error - action required
106    Error,
107    /// Critical - blocking issue
108    Critical,
109}
110
111/// Impact level of compatibility issues
112#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
113pub enum ImpactLevel {
114    /// No user impact
115    None,
116    /// Minor impact - easily resolved
117    Low,
118    /// Moderate impact - requires changes
119    Medium,
120    /// High impact - significant changes required
121    High,
122    /// Critical impact - may block migration
123    Critical,
124}
125
126/// Breaking change information
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct BreakingChange {
129    /// Change type
130    pub change_type: ChangeType,
131    /// Component affected
132    pub component: String,
133    /// Description of the change
134    pub description: String,
135    /// Migration path
136    pub migration_path: Option<String>,
137    /// Version where change was introduced
138    pub introduced_in: Version,
139}
140
141/// Types of breaking changes
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
143pub enum ChangeType {
144    /// API signature changed
145    ApiSignatureChange,
146    /// Behavior change
147    BehaviorChange,
148    /// Removed feature
149    FeatureRemoval,
150    /// Configuration change
151    ConfigurationChange,
152    /// Dependency change
153    DependencyChange,
154    /// Data format change
155    DataFormatChange,
156}
157
158/// Compatibility checker implementation
159pub struct CompatibilityChecker {
160    /// Registered versions and their metadata
161    versions: HashMap<Version, ApiVersion>,
162    /// Compatibility rules
163    rules: Vec<CompatibilityRule>,
164}
165
166impl CompatibilityChecker {
167    /// Create a new compatibility checker
168    pub fn new() -> Self {
169        Self {
170            versions: HashMap::new(),
171            rules: Self::default_rules(),
172        }
173    }
174
175    /// Register a version for compatibility checking
176    pub fn register_version(&mut self, apiversion: &ApiVersion) -> Result<(), CoreError> {
177        self.versions
178            .insert(apiversion.version.clone(), apiversion.clone());
179        Ok(())
180    }
181
182    /// Check compatibility between two versions
183    pub fn check_compatibility(
184        &self,
185        from_version: &Version,
186        toversion: &Version,
187    ) -> Result<CompatibilityLevel, CoreError> {
188        let report = self.get_compatibility_report(from_version, toversion)?;
189        Ok(report.compatibility_level)
190    }
191
192    /// Get detailed compatibility report
193    pub fn get_compatibility_report(
194        &self,
195        from_version: &Version,
196        toversion: &Version,
197    ) -> Result<CompatibilityReport, CoreError> {
198        let from_api = self.versions.get(from_version).ok_or_else(|| {
199            CoreError::ComputationError(crate::error::ErrorContext::new(format!(
200                "Version {from_version} not registered"
201            )))
202        })?;
203        let to_api = self.versions.get(toversion).ok_or_else(|| {
204            CoreError::ComputationError(crate::error::ErrorContext::new(format!(
205                "Version {toversion} not registered"
206            )))
207        })?;
208
209        let mut report = CompatibilityReport {
210            from_version: from_version.clone(),
211            toversion: toversion.clone(),
212            compatibility_level: CompatibilityLevel::BackwardCompatible,
213            issues: Vec::new(),
214            breakingchanges: Vec::new(),
215            deprecated_features: to_api.deprecated_features.clone(),
216            new_features: to_api.new_features.clone(),
217            migration_recommendations: Vec::new(),
218            estimated_migration_effort: None,
219        };
220
221        // Apply compatibility rules
222        for rule in &self.rules {
223            rule.apply(from_api, to_api, &mut report)?;
224        }
225
226        // Determine overall compatibility level
227        report.compatibility_level = self.determine_compatibility_level(&report);
228
229        // Generate migration recommendations
230        self.generate_migration_recommendations(&mut report);
231
232        // Estimate migration effort
233        report.estimated_migration_effort = self.estimate_migration_effort(&report);
234
235        Ok(report)
236    }
237
238    /// Add a custom compatibility rule
239    pub fn add_rule(&mut self, rule: CompatibilityRule) {
240        self.rules.push(rule);
241    }
242
243    /// Create default compatibility rules
244    fn default_rules() -> Vec<CompatibilityRule> {
245        vec![
246            CompatibilityRule::SemVerRule,
247            CompatibilityRule::BreakingChangeRule,
248            CompatibilityRule::FeatureRemovalRule,
249            CompatibilityRule::ApiSignatureRule,
250            CompatibilityRule::BehaviorChangeRule,
251        ]
252    }
253
254    /// Determine overall compatibility level based on issues
255    fn determine_compatibility_level(&self, report: &CompatibilityReport) -> CompatibilityLevel {
256        let has_critical = report
257            .issues
258            .iter()
259            .any(|i| i.severity == IssueSeverity::Critical);
260        let haserrors = report
261            .issues
262            .iter()
263            .any(|i| i.severity == IssueSeverity::Error);
264        let has_warnings = report
265            .issues
266            .iter()
267            .any(|i| i.severity == IssueSeverity::Warning);
268        let has_breakingchanges = !report.breakingchanges.is_empty();
269
270        if has_critical {
271            CompatibilityLevel::Incompatible
272        } else if has_breakingchanges || haserrors {
273            if report.from_version.major() != report.toversion.major() {
274                CompatibilityLevel::BreakingChanges
275            } else {
276                CompatibilityLevel::PartiallyCompatible
277            }
278        } else if has_warnings {
279            CompatibilityLevel::MostlyCompatible
280        } else {
281            CompatibilityLevel::BackwardCompatible
282        }
283    }
284
285    /// Generate migration recommendations
286    fn generate_migration_recommendations(&self, report: &mut CompatibilityReport) {
287        for issue in &report.issues {
288            if let Some(ref resolution) = issue.resolution {
289                report
290                    .migration_recommendations
291                    .push(format!("{}: {}", issue.component, resolution));
292            }
293        }
294
295        for breaking_change in &report.breakingchanges {
296            if let Some(ref migration_path) = breaking_change.migration_path {
297                report
298                    .migration_recommendations
299                    .push(format!("{}, {}", breaking_change.component, migration_path));
300            }
301        }
302
303        // Add version-specific recommendations
304        if report.from_version.major() != report.toversion.major() {
305            report
306                .migration_recommendations
307                .push("Major version upgrade - review all API usage".to_string());
308        }
309
310        if !report.deprecated_features.is_empty() {
311            report
312                .migration_recommendations
313                .push("Update code to avoid deprecated features".to_string());
314        }
315    }
316
317    /// Estimate migration effort in developer hours
318    fn estimate_migration_effort(&self, report: &CompatibilityReport) -> Option<u32> {
319        let mut effort_hours = 0u32;
320
321        // Base effort for version differences
322        let major_diff = report
323            .toversion
324            .major()
325            .saturating_sub(report.from_version.major());
326        let minor_diff = if major_diff == 0 {
327            report
328                .toversion
329                .minor()
330                .saturating_sub(report.from_version.minor())
331        } else {
332            0
333        };
334
335        effort_hours += (major_diff * 40) as u32; // 40 hours per major version
336        effort_hours += (minor_diff * 8) as u32; // 8 hours per minor version
337
338        // Add effort for specific issues
339        for issue in &report.issues {
340            effort_hours += match issue.impact {
341                ImpactLevel::None => 0,
342                ImpactLevel::Low => 2,
343                ImpactLevel::Medium => 8,
344                ImpactLevel::High => 24,
345                ImpactLevel::Critical => 80,
346            };
347        }
348
349        // Add effort for breaking changes
350        for breaking_change in &report.breakingchanges {
351            effort_hours += match breaking_change.change_type {
352                ChangeType::ApiSignatureChange => 16,
353                ChangeType::BehaviorChange => 24,
354                ChangeType::FeatureRemoval => 32,
355                ChangeType::ConfigurationChange => 8,
356                ChangeType::DependencyChange => 16,
357                ChangeType::DataFormatChange => 40,
358            };
359        }
360
361        if effort_hours > 0 {
362            Some(effort_hours)
363        } else {
364            None
365        }
366    }
367}
368
369impl Default for CompatibilityChecker {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375/// Compatibility rule trait
376pub trait CompatibilityRuleTrait {
377    /// Apply the rule to generate compatibility issues
378    fn apply(
379        &self,
380        from_api: &ApiVersion,
381        to_api: &ApiVersion,
382        report: &mut CompatibilityReport,
383    ) -> Result<(), CoreError>;
384}
385
386/// Built-in compatibility rules
387#[derive(Debug, Clone)]
388pub enum CompatibilityRule {
389    /// Semantic versioning rule
390    SemVerRule,
391    /// Breaking change detection rule
392    BreakingChangeRule,
393    /// Feature removal rule
394    FeatureRemovalRule,
395    /// API signature change rule
396    ApiSignatureRule,
397    /// Behavior change rule
398    BehaviorChangeRule,
399}
400
401impl CompatibilityRuleTrait for CompatibilityRule {
402    fn apply(
403        &self,
404        from_api: &ApiVersion,
405        to_api: &ApiVersion,
406        report: &mut CompatibilityReport,
407    ) -> Result<(), CoreError> {
408        match self {
409            CompatibilityRule::SemVerRule => self.apply_semver_rule(from_api, to_api, report),
410            CompatibilityRule::BreakingChangeRule => {
411                self.apply_breaking_change_rule(from_api, to_api, report)
412            }
413            CompatibilityRule::FeatureRemovalRule => {
414                self.apply_feature_removal_rule(from_api, to_api, report)
415            }
416            CompatibilityRule::ApiSignatureRule => {
417                self.apply_api_signature_rule(from_api, to_api, report)
418            }
419            CompatibilityRule::BehaviorChangeRule => {
420                self.apply_behavior_change_rule(from_api, to_api, report)
421            }
422        }
423    }
424}
425
426impl CompatibilityRule {
427    /// Apply semantic versioning rule
428    fn apply_semver_rule(
429        &self,
430        from_api: &ApiVersion,
431        to_api: &ApiVersion,
432        report: &mut CompatibilityReport,
433    ) -> Result<(), CoreError> {
434        let from_version = &from_api.version;
435        let toversion = &to_api.version;
436
437        if toversion.major() > from_version.major() {
438            report.issues.push(CompatibilityIssue {
439                severity: IssueSeverity::Warning,
440                component: "version".to_string(),
441                description: "Major version upgrade detected".to_string(),
442                resolution: Some("Review all API usage for breaking changes".to_string()),
443                impact: ImpactLevel::High,
444            });
445        }
446
447        if toversion < from_version {
448            report.issues.push(CompatibilityIssue {
449                severity: IssueSeverity::Error,
450                component: "version".to_string(),
451                description: "Downgrade detected".to_string(),
452                resolution: Some("Downgrades are not supported".to_string()),
453                impact: ImpactLevel::Critical,
454            });
455        }
456
457        Ok(())
458    }
459
460    /// Apply breaking change rule
461    fn apply_breaking_change_rule(
462        &self,
463        _from_api: &ApiVersion,
464        to_api: &ApiVersion,
465        report: &mut CompatibilityReport,
466    ) -> Result<(), CoreError> {
467        for breaking_change in &to_api.breakingchanges {
468            report.breakingchanges.push(BreakingChange {
469                change_type: ChangeType::BehaviorChange, // Default type
470                component: "api".to_string(),
471                description: breaking_change.clone(),
472                migration_path: None,
473                introduced_in: to_api.version.clone(),
474            });
475
476            report.issues.push(CompatibilityIssue {
477                severity: IssueSeverity::Error,
478                component: "api".to_string(),
479                description: breaking_change.to_string(),
480                resolution: Some("Update code to handle the breaking change".to_string()),
481                impact: ImpactLevel::High,
482            });
483        }
484
485        Ok(())
486    }
487
488    /// Apply feature removal rule
489    fn apply_feature_removal_rule(
490        &self,
491        from_api: &ApiVersion,
492        to_api: &ApiVersion,
493        report: &mut CompatibilityReport,
494    ) -> Result<(), CoreError> {
495        // Check for features that existed in from_api but not in to_api
496        for feature in &from_api.features {
497            if !to_api.features.contains(feature) {
498                report.breakingchanges.push(BreakingChange {
499                    change_type: ChangeType::FeatureRemoval,
500                    component: feature.clone(),
501                    description: format!("Feature '{feature}' has been removed"),
502                    migration_path: Some("Remove usage of this feature".to_string()),
503                    introduced_in: to_api.version.clone(),
504                });
505
506                report.issues.push(CompatibilityIssue {
507                    severity: IssueSeverity::Error,
508                    component: feature.clone(),
509                    description: format!("Feature '{feature}' no longer available"),
510                    resolution: Some("Remove or replace feature usage".to_string()),
511                    impact: ImpactLevel::High,
512                });
513            }
514        }
515
516        Ok(())
517    }
518
519    /// Apply API signature rule
520    fn apply_api_signature_rule(
521        &self,
522        _from_api: &ApiVersion,
523        _to_api: &ApiVersion,
524        _report: &mut CompatibilityReport,
525    ) -> Result<(), CoreError> {
526        // This would typically analyze actual API signatures
527        // For now, it's a placeholder
528        Ok(())
529    }
530
531    /// Apply behavior change rule
532    fn apply_behavior_change_rule(
533        &self,
534        _from_api: &ApiVersion,
535        _to_api: &ApiVersion,
536        _report: &mut CompatibilityReport,
537    ) -> Result<(), CoreError> {
538        // This would typically analyze behavior changes
539        // For now, it's a placeholder
540        Ok(())
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::versioning::ApiVersionBuilder;
548
549    #[test]
550    fn test_compatibility_levels() {
551        assert!(CompatibilityLevel::BackwardCompatible < CompatibilityLevel::BreakingChanges);
552        assert!(CompatibilityLevel::BreakingChanges.requires_migration());
553        assert!(CompatibilityLevel::BackwardCompatible.supports_auto_migration());
554    }
555
556    #[test]
557    fn test_compatibility_checker() {
558        let mut checker = CompatibilityChecker::new();
559
560        let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").unwrap())
561            .feature("feature1")
562            .build()
563            .unwrap();
564        let v2 = ApiVersionBuilder::new(Version::parse("1.1.0").unwrap())
565            .feature("feature1")
566            .feature("feature2")
567            .new_feature("Added feature2")
568            .build()
569            .unwrap();
570
571        checker.register_version(&v1).unwrap();
572        checker.register_version(&v2).unwrap();
573
574        let compatibility = checker
575            .check_compatibility(&v1.version, &v2.version)
576            .unwrap();
577        assert_eq!(compatibility, CompatibilityLevel::BackwardCompatible);
578    }
579
580    #[test]
581    fn test_breakingchanges() {
582        let mut checker = CompatibilityChecker::new();
583
584        let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").unwrap())
585            .feature("feature1")
586            .build()
587            .unwrap();
588        let v2 = ApiVersionBuilder::new(Version::parse("2.0.0").unwrap())
589            .breaking_change("Removed feature1")
590            .build()
591            .unwrap();
592
593        checker.register_version(&v1).unwrap();
594        checker.register_version(&v2).unwrap();
595
596        let report = checker
597            .get_compatibility_report(&v1.version, &v2.version)
598            .unwrap();
599        assert!(!report.breakingchanges.is_empty());
600        assert!(report.compatibility_level.requires_migration());
601    }
602
603    #[test]
604    fn test_migration_effort_estimation() {
605        let mut checker = CompatibilityChecker::new();
606
607        let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").unwrap())
608            .build()
609            .unwrap();
610        let v2 = ApiVersionBuilder::new(Version::parse("2.0.0").unwrap())
611            .breaking_change("Major API overhaul")
612            .build()
613            .unwrap();
614
615        checker.register_version(&v1).unwrap();
616        checker.register_version(&v2).unwrap();
617
618        let report = checker
619            .get_compatibility_report(&v1.version, &v2.version)
620            .unwrap();
621        assert!(report.estimated_migration_effort.is_some());
622        assert!(report.estimated_migration_effort.unwrap() > 0);
623    }
624
625    #[test]
626    fn test_feature_removal_detection() {
627        let mut checker = CompatibilityChecker::new();
628
629        let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").unwrap())
630            .feature("feature1")
631            .feature("feature2")
632            .build()
633            .unwrap();
634        let v2 = ApiVersionBuilder::new(Version::parse("1.1.0").unwrap())
635            .feature("feature1")
636            // feature2 removed
637            .build().unwrap();
638
639        checker.register_version(&v1).unwrap();
640        checker.register_version(&v2).unwrap();
641
642        let report = checker
643            .get_compatibility_report(&v1.version, &v2.version)
644            .unwrap();
645        assert!(!report.breakingchanges.is_empty());
646
647        let feature_removal = report
648            .breakingchanges
649            .iter()
650            .find(|bc| bc.change_type == ChangeType::FeatureRemoval);
651        assert!(feature_removal.is_some());
652    }
653}