1use super::{ApiVersion, Version};
7use crate::error::CoreError;
8use std::collections::HashMap;
9
10#[cfg(feature = "serialization")]
11use serde::{Deserialize, Serialize};
12
13#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub enum CompatibilityLevel {
17 BackwardCompatible,
19 MostlyCompatible,
21 PartiallyCompatible,
23 BreakingChanges,
25 Incompatible,
27}
28
29impl CompatibilityLevel {
30 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 pub fn requires_migration(&self) -> bool {
43 matches!(
44 self,
45 CompatibilityLevel::PartiallyCompatible
46 | CompatibilityLevel::BreakingChanges
47 | CompatibilityLevel::Incompatible
48 )
49 }
50
51 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#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
64#[derive(Debug, Clone)]
65pub struct CompatibilityReport {
66 pub from_version: Version,
68 pub toversion: Version,
70 pub compatibility_level: CompatibilityLevel,
72 pub issues: Vec<CompatibilityIssue>,
74 pub breakingchanges: Vec<BreakingChange>,
76 pub deprecated_features: Vec<String>,
78 pub new_features: Vec<String>,
80 pub migration_recommendations: Vec<String>,
82 pub estimated_migration_effort: Option<u32>,
84}
85
86#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
88#[derive(Debug, Clone)]
89pub struct CompatibilityIssue {
90 pub severity: IssueSeverity,
92 pub component: String,
94 pub description: String,
96 pub resolution: Option<String>,
98 pub impact: ImpactLevel,
100}
101
102#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
105pub enum IssueSeverity {
106 Info,
108 Warning,
110 Error,
112 Critical,
114}
115
116#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
118#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
119pub enum ImpactLevel {
120 None,
122 Low,
124 Medium,
126 High,
128 Critical,
130}
131
132#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
134#[derive(Debug, Clone)]
135pub struct BreakingChange {
136 pub change_type: ChangeType,
138 pub component: String,
140 pub description: String,
142 pub migration_path: Option<String>,
144 pub introduced_in: Version,
146}
147
148#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum ChangeType {
152 ApiSignatureChange,
154 BehaviorChange,
156 FeatureRemoval,
158 ConfigurationChange,
160 DependencyChange,
162 DataFormatChange,
164}
165
166pub struct CompatibilityChecker {
168 versions: HashMap<Version, ApiVersion>,
170 rules: Vec<CompatibilityRule>,
172}
173
174impl CompatibilityChecker {
175 pub fn new() -> Self {
177 Self {
178 versions: HashMap::new(),
179 rules: Self::default_rules(),
180 }
181 }
182
183 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 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 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 for rule in &self.rules {
231 rule.apply(from_api, to_api, &mut report)?;
232 }
233
234 report.compatibility_level = self.determine_compatibility_level(&report);
236
237 self.generate_migration_recommendations(&mut report);
239
240 report.estimated_migration_effort = self.estimate_migration_effort(&report);
242
243 Ok(report)
244 }
245
246 pub fn add_rule(&mut self, rule: CompatibilityRule) {
248 self.rules.push(rule);
249 }
250
251 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 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 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 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 fn estimate_migration_effort(&self, report: &CompatibilityReport) -> Option<u32> {
327 let mut effort_hours = 0u32;
328
329 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; effort_hours += (minor_diff * 8) as u32; 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 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
383pub trait CompatibilityRuleTrait {
385 fn apply(
387 &self,
388 from_api: &ApiVersion,
389 to_api: &ApiVersion,
390 report: &mut CompatibilityReport,
391 ) -> Result<(), CoreError>;
392}
393
394#[derive(Debug, Clone)]
396pub enum CompatibilityRule {
397 SemVerRule,
399 BreakingChangeRule,
401 FeatureRemovalRule,
403 ApiSignatureRule,
405 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 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 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, 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 fn apply_feature_removal_rule(
498 &self,
499 from_api: &ApiVersion,
500 to_api: &ApiVersion,
501 report: &mut CompatibilityReport,
502 ) -> Result<(), CoreError> {
503 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 fn apply_api_signature_rule(
529 &self,
530 _from_api: &ApiVersion,
531 _to_api: &ApiVersion,
532 _report: &mut CompatibilityReport,
533 ) -> Result<(), CoreError> {
534 Ok(())
537 }
538
539 fn apply_behavior_change_rule(
541 &self,
542 _from_api: &ApiVersion,
543 _to_api: &ApiVersion,
544 _report: &mut CompatibilityReport,
545 ) -> Result<(), CoreError> {
546 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 .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}