1use super::{ApiVersion, Version};
7use crate::error::CoreError;
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub enum CompatibilityLevel {
15 BackwardCompatible,
17 MostlyCompatible,
19 PartiallyCompatible,
21 BreakingChanges,
23 Incompatible,
25}
26
27impl CompatibilityLevel {
28 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 pub fn requires_migration(&self) -> bool {
41 matches!(
42 self,
43 CompatibilityLevel::PartiallyCompatible
44 | CompatibilityLevel::BreakingChanges
45 | CompatibilityLevel::Incompatible
46 )
47 }
48
49 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#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct CompatibilityReport {
63 pub from_version: Version,
65 pub toversion: Version,
67 pub compatibility_level: CompatibilityLevel,
69 pub issues: Vec<CompatibilityIssue>,
71 pub breakingchanges: Vec<BreakingChange>,
73 pub deprecated_features: Vec<String>,
75 pub new_features: Vec<String>,
77 pub migration_recommendations: Vec<String>,
79 pub estimated_migration_effort: Option<u32>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CompatibilityIssue {
86 pub severity: IssueSeverity,
88 pub component: String,
90 pub description: String,
92 pub resolution: Option<String>,
94 pub impact: ImpactLevel,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
100pub enum IssueSeverity {
101 Info,
103 Warning,
105 Error,
107 Critical,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
113pub enum ImpactLevel {
114 None,
116 Low,
118 Medium,
120 High,
122 Critical,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct BreakingChange {
129 pub change_type: ChangeType,
131 pub component: String,
133 pub description: String,
135 pub migration_path: Option<String>,
137 pub introduced_in: Version,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
143pub enum ChangeType {
144 ApiSignatureChange,
146 BehaviorChange,
148 FeatureRemoval,
150 ConfigurationChange,
152 DependencyChange,
154 DataFormatChange,
156}
157
158pub struct CompatibilityChecker {
160 versions: HashMap<Version, ApiVersion>,
162 rules: Vec<CompatibilityRule>,
164}
165
166impl CompatibilityChecker {
167 pub fn new() -> Self {
169 Self {
170 versions: HashMap::new(),
171 rules: Self::default_rules(),
172 }
173 }
174
175 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 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 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 for rule in &self.rules {
223 rule.apply(from_api, to_api, &mut report)?;
224 }
225
226 report.compatibility_level = self.determine_compatibility_level(&report);
228
229 self.generate_migration_recommendations(&mut report);
231
232 report.estimated_migration_effort = self.estimate_migration_effort(&report);
234
235 Ok(report)
236 }
237
238 pub fn add_rule(&mut self, rule: CompatibilityRule) {
240 self.rules.push(rule);
241 }
242
243 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 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 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 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 fn estimate_migration_effort(&self, report: &CompatibilityReport) -> Option<u32> {
319 let mut effort_hours = 0u32;
320
321 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; effort_hours += (minor_diff * 8) as u32; 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 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
375pub trait CompatibilityRuleTrait {
377 fn apply(
379 &self,
380 from_api: &ApiVersion,
381 to_api: &ApiVersion,
382 report: &mut CompatibilityReport,
383 ) -> Result<(), CoreError>;
384}
385
386#[derive(Debug, Clone)]
388pub enum CompatibilityRule {
389 SemVerRule,
391 BreakingChangeRule,
393 FeatureRemovalRule,
395 ApiSignatureRule,
397 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 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 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, 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 fn apply_feature_removal_rule(
490 &self,
491 from_api: &ApiVersion,
492 to_api: &ApiVersion,
493 report: &mut CompatibilityReport,
494 ) -> Result<(), CoreError> {
495 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 fn apply_api_signature_rule(
521 &self,
522 _from_api: &ApiVersion,
523 _to_api: &ApiVersion,
524 _report: &mut CompatibilityReport,
525 ) -> Result<(), CoreError> {
526 Ok(())
529 }
530
531 fn apply_behavior_change_rule(
533 &self,
534 _from_api: &ApiVersion,
535 _to_api: &ApiVersion,
536 _report: &mut CompatibilityReport,
537 ) -> Result<(), CoreError> {
538 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 .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}