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