use super::{ApiVersion, Version};
use crate::error::CoreError;
use std::collections::HashMap;
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CompatibilityLevel {
BackwardCompatible,
MostlyCompatible,
PartiallyCompatible,
BreakingChanges,
Incompatible,
}
impl CompatibilityLevel {
pub const fn as_str(&self) -> &'static str {
match self {
CompatibilityLevel::BackwardCompatible => "backward_compatible",
CompatibilityLevel::MostlyCompatible => "mostly_compatible",
CompatibilityLevel::PartiallyCompatible => "partially_compatible",
CompatibilityLevel::BreakingChanges => "breakingchanges",
CompatibilityLevel::Incompatible => "incompatible",
}
}
pub fn requires_migration(&self) -> bool {
matches!(
self,
CompatibilityLevel::PartiallyCompatible
| CompatibilityLevel::BreakingChanges
| CompatibilityLevel::Incompatible
)
}
pub fn supports_auto_migration(&self) -> bool {
matches!(
self,
CompatibilityLevel::BackwardCompatible
| CompatibilityLevel::MostlyCompatible
| CompatibilityLevel::PartiallyCompatible
)
}
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct CompatibilityReport {
pub from_version: Version,
pub toversion: Version,
pub compatibility_level: CompatibilityLevel,
pub issues: Vec<CompatibilityIssue>,
pub breakingchanges: Vec<BreakingChange>,
pub deprecated_features: Vec<String>,
pub new_features: Vec<String>,
pub migration_recommendations: Vec<String>,
pub estimated_migration_effort: Option<u32>,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct CompatibilityIssue {
pub severity: IssueSeverity,
pub component: String,
pub description: String,
pub resolution: Option<String>,
pub impact: ImpactLevel,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueSeverity {
Info,
Warning,
Error,
Critical,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ImpactLevel {
None,
Low,
Medium,
High,
Critical,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct BreakingChange {
pub change_type: ChangeType,
pub component: String,
pub description: String,
pub migration_path: Option<String>,
pub introduced_in: Version,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeType {
ApiSignatureChange,
BehaviorChange,
FeatureRemoval,
ConfigurationChange,
DependencyChange,
DataFormatChange,
}
pub struct CompatibilityChecker {
versions: HashMap<Version, ApiVersion>,
rules: Vec<CompatibilityRule>,
}
impl CompatibilityChecker {
pub fn new() -> Self {
Self {
versions: HashMap::new(),
rules: Self::default_rules(),
}
}
pub fn register_version(&mut self, apiversion: &ApiVersion) -> Result<(), CoreError> {
self.versions
.insert(apiversion.version.clone(), apiversion.clone());
Ok(())
}
pub fn check_compatibility(
&self,
from_version: &Version,
toversion: &Version,
) -> Result<CompatibilityLevel, CoreError> {
let report = self.get_compatibility_report(from_version, toversion)?;
Ok(report.compatibility_level)
}
pub fn get_compatibility_report(
&self,
from_version: &Version,
toversion: &Version,
) -> Result<CompatibilityReport, CoreError> {
let from_api = self.versions.get(from_version).ok_or_else(|| {
CoreError::ComputationError(crate::error::ErrorContext::new(format!(
"Version {from_version} not registered"
)))
})?;
let to_api = self.versions.get(toversion).ok_or_else(|| {
CoreError::ComputationError(crate::error::ErrorContext::new(format!(
"Version {toversion} not registered"
)))
})?;
let mut report = CompatibilityReport {
from_version: from_version.clone(),
toversion: toversion.clone(),
compatibility_level: CompatibilityLevel::BackwardCompatible,
issues: Vec::new(),
breakingchanges: Vec::new(),
deprecated_features: to_api.deprecated_features.clone(),
new_features: to_api.new_features.clone(),
migration_recommendations: Vec::new(),
estimated_migration_effort: None,
};
for rule in &self.rules {
rule.apply(from_api, to_api, &mut report)?;
}
report.compatibility_level = self.determine_compatibility_level(&report);
self.generate_migration_recommendations(&mut report);
report.estimated_migration_effort = self.estimate_migration_effort(&report);
Ok(report)
}
pub fn add_rule(&mut self, rule: CompatibilityRule) {
self.rules.push(rule);
}
fn default_rules() -> Vec<CompatibilityRule> {
vec![
CompatibilityRule::SemVerRule,
CompatibilityRule::BreakingChangeRule,
CompatibilityRule::FeatureRemovalRule,
CompatibilityRule::ApiSignatureRule,
CompatibilityRule::BehaviorChangeRule,
]
}
fn determine_compatibility_level(&self, report: &CompatibilityReport) -> CompatibilityLevel {
let has_critical = report
.issues
.iter()
.any(|i| i.severity == IssueSeverity::Critical);
let haserrors = report
.issues
.iter()
.any(|i| i.severity == IssueSeverity::Error);
let has_warnings = report
.issues
.iter()
.any(|i| i.severity == IssueSeverity::Warning);
let has_breakingchanges = !report.breakingchanges.is_empty();
if has_critical {
CompatibilityLevel::Incompatible
} else if has_breakingchanges || haserrors {
if report.from_version.major() != report.toversion.major() {
CompatibilityLevel::BreakingChanges
} else {
CompatibilityLevel::PartiallyCompatible
}
} else if has_warnings {
CompatibilityLevel::MostlyCompatible
} else {
CompatibilityLevel::BackwardCompatible
}
}
fn generate_migration_recommendations(&self, report: &mut CompatibilityReport) {
for issue in &report.issues {
if let Some(ref resolution) = issue.resolution {
report
.migration_recommendations
.push(format!("{}: {}", issue.component, resolution));
}
}
for breaking_change in &report.breakingchanges {
if let Some(ref migration_path) = breaking_change.migration_path {
report
.migration_recommendations
.push(format!("{}, {}", breaking_change.component, migration_path));
}
}
if report.from_version.major() != report.toversion.major() {
report
.migration_recommendations
.push("Major version upgrade - review all API usage".to_string());
}
if !report.deprecated_features.is_empty() {
report
.migration_recommendations
.push("Update code to avoid deprecated features".to_string());
}
}
fn estimate_migration_effort(&self, report: &CompatibilityReport) -> Option<u32> {
let mut effort_hours = 0u32;
let major_diff = report
.toversion
.major()
.saturating_sub(report.from_version.major());
let minor_diff = if major_diff == 0 {
report
.toversion
.minor()
.saturating_sub(report.from_version.minor())
} else {
0
};
effort_hours += (major_diff * 40) as u32; effort_hours += (minor_diff * 8) as u32;
for issue in &report.issues {
effort_hours += match issue.impact {
ImpactLevel::None => 0,
ImpactLevel::Low => 2,
ImpactLevel::Medium => 8,
ImpactLevel::High => 24,
ImpactLevel::Critical => 80,
};
}
for breaking_change in &report.breakingchanges {
effort_hours += match breaking_change.change_type {
ChangeType::ApiSignatureChange => 16,
ChangeType::BehaviorChange => 24,
ChangeType::FeatureRemoval => 32,
ChangeType::ConfigurationChange => 8,
ChangeType::DependencyChange => 16,
ChangeType::DataFormatChange => 40,
};
}
if effort_hours > 0 {
Some(effort_hours)
} else {
None
}
}
}
impl Default for CompatibilityChecker {
fn default() -> Self {
Self::new()
}
}
pub trait CompatibilityRuleTrait {
fn apply(
&self,
from_api: &ApiVersion,
to_api: &ApiVersion,
report: &mut CompatibilityReport,
) -> Result<(), CoreError>;
}
#[derive(Debug, Clone)]
pub enum CompatibilityRule {
SemVerRule,
BreakingChangeRule,
FeatureRemovalRule,
ApiSignatureRule,
BehaviorChangeRule,
}
impl CompatibilityRuleTrait for CompatibilityRule {
fn apply(
&self,
from_api: &ApiVersion,
to_api: &ApiVersion,
report: &mut CompatibilityReport,
) -> Result<(), CoreError> {
match self {
CompatibilityRule::SemVerRule => self.apply_semver_rule(from_api, to_api, report),
CompatibilityRule::BreakingChangeRule => {
self.apply_breaking_change_rule(from_api, to_api, report)
}
CompatibilityRule::FeatureRemovalRule => {
self.apply_feature_removal_rule(from_api, to_api, report)
}
CompatibilityRule::ApiSignatureRule => {
self.apply_api_signature_rule(from_api, to_api, report)
}
CompatibilityRule::BehaviorChangeRule => {
self.apply_behavior_change_rule(from_api, to_api, report)
}
}
}
}
impl CompatibilityRule {
fn apply_semver_rule(
&self,
from_api: &ApiVersion,
to_api: &ApiVersion,
report: &mut CompatibilityReport,
) -> Result<(), CoreError> {
let from_version = &from_api.version;
let toversion = &to_api.version;
if toversion.major() > from_version.major() {
report.issues.push(CompatibilityIssue {
severity: IssueSeverity::Warning,
component: "version".to_string(),
description: "Major version upgrade detected".to_string(),
resolution: Some("Review all API usage for breaking changes".to_string()),
impact: ImpactLevel::High,
});
}
if toversion < from_version {
report.issues.push(CompatibilityIssue {
severity: IssueSeverity::Error,
component: "version".to_string(),
description: "Downgrade detected".to_string(),
resolution: Some("Downgrades are not supported".to_string()),
impact: ImpactLevel::Critical,
});
}
Ok(())
}
fn apply_breaking_change_rule(
&self,
_from_api: &ApiVersion,
to_api: &ApiVersion,
report: &mut CompatibilityReport,
) -> Result<(), CoreError> {
for breaking_change in &to_api.breakingchanges {
report.breakingchanges.push(BreakingChange {
change_type: ChangeType::BehaviorChange, component: "api".to_string(),
description: breaking_change.clone(),
migration_path: None,
introduced_in: to_api.version.clone(),
});
report.issues.push(CompatibilityIssue {
severity: IssueSeverity::Error,
component: "api".to_string(),
description: breaking_change.to_string(),
resolution: Some("Update code to handle the breaking change".to_string()),
impact: ImpactLevel::High,
});
}
Ok(())
}
fn apply_feature_removal_rule(
&self,
from_api: &ApiVersion,
to_api: &ApiVersion,
report: &mut CompatibilityReport,
) -> Result<(), CoreError> {
for feature in &from_api.features {
if !to_api.features.contains(feature) {
report.breakingchanges.push(BreakingChange {
change_type: ChangeType::FeatureRemoval,
component: feature.clone(),
description: format!("Feature '{feature}' has been removed"),
migration_path: Some("Remove usage of this feature".to_string()),
introduced_in: to_api.version.clone(),
});
report.issues.push(CompatibilityIssue {
severity: IssueSeverity::Error,
component: feature.clone(),
description: format!("Feature '{feature}' no longer available"),
resolution: Some("Remove or replace feature usage".to_string()),
impact: ImpactLevel::High,
});
}
}
Ok(())
}
fn apply_api_signature_rule(
&self,
_from_api: &ApiVersion,
_to_api: &ApiVersion,
_report: &mut CompatibilityReport,
) -> Result<(), CoreError> {
Ok(())
}
fn apply_behavior_change_rule(
&self,
_from_api: &ApiVersion,
_to_api: &ApiVersion,
_report: &mut CompatibilityReport,
) -> Result<(), CoreError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::versioning::ApiVersionBuilder;
#[test]
fn test_compatibility_levels() {
assert!(CompatibilityLevel::BackwardCompatible < CompatibilityLevel::BreakingChanges);
assert!(CompatibilityLevel::BreakingChanges.requires_migration());
assert!(CompatibilityLevel::BackwardCompatible.supports_auto_migration());
}
#[test]
fn test_compatibility_checker() {
let mut checker = CompatibilityChecker::new();
let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").expect("Operation failed"))
.feature("feature1")
.build()
.expect("Test: operation failed");
let v2 = ApiVersionBuilder::new(Version::parse("1.1.0").expect("Operation failed"))
.feature("feature1")
.feature("feature2")
.new_feature("Added feature2")
.build()
.expect("Test: operation failed");
checker.register_version(&v1).expect("Operation failed");
checker.register_version(&v2).expect("Operation failed");
let compatibility = checker
.check_compatibility(&v1.version, &v2.version)
.expect("Test: operation failed");
assert_eq!(compatibility, CompatibilityLevel::BackwardCompatible);
}
#[test]
fn test_breakingchanges() {
let mut checker = CompatibilityChecker::new();
let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").expect("Operation failed"))
.feature("feature1")
.build()
.expect("Test: operation failed");
let v2 = ApiVersionBuilder::new(Version::parse("2.0.0").expect("Operation failed"))
.breaking_change("Removed feature1")
.build()
.expect("Test: operation failed");
checker.register_version(&v1).expect("Operation failed");
checker.register_version(&v2).expect("Operation failed");
let report = checker
.get_compatibility_report(&v1.version, &v2.version)
.expect("Test: operation failed");
assert!(!report.breakingchanges.is_empty());
assert!(report.compatibility_level.requires_migration());
}
#[test]
fn test_migration_effort_estimation() {
let mut checker = CompatibilityChecker::new();
let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").expect("Operation failed"))
.build()
.expect("Test: operation failed");
let v2 = ApiVersionBuilder::new(Version::parse("2.0.0").expect("Operation failed"))
.breaking_change("Major API overhaul")
.build()
.expect("Test: operation failed");
checker.register_version(&v1).expect("Operation failed");
checker.register_version(&v2).expect("Operation failed");
let report = checker
.get_compatibility_report(&v1.version, &v2.version)
.expect("Test: operation failed");
assert!(report.estimated_migration_effort.is_some());
assert!(
report
.estimated_migration_effort
.expect("Test: operation failed")
> 0
);
}
#[test]
fn test_feature_removal_detection() {
let mut checker = CompatibilityChecker::new();
let v1 = ApiVersionBuilder::new(Version::parse("1.0.0").expect("Operation failed"))
.feature("feature1")
.feature("feature2")
.build()
.expect("Test: operation failed");
let v2 = ApiVersionBuilder::new(Version::parse("1.1.0").expect("Operation failed"))
.feature("feature1")
.build().expect("Operation failed");
checker.register_version(&v1).expect("Operation failed");
checker.register_version(&v2).expect("Operation failed");
let report = checker
.get_compatibility_report(&v1.version, &v2.version)
.expect("Test: operation failed");
assert!(!report.breakingchanges.is_empty());
let feature_removal = report
.breakingchanges
.iter()
.find(|bc| bc.change_type == ChangeType::FeatureRemoval);
assert!(feature_removal.is_some());
}
}