Skip to main content

plugin_packager/
compat_matrix.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Plugin compatibility matrix and analysis
5///
6/// This module provides comprehensive compatibility analysis including:
7/// - ABI version compatibility matrix
8/// - OS/architecture support matrix
9/// - Dependency compatibility analysis
10/// - Breaking change detection and tracking
11/// - Compatibility recommendations
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// ABI version representation
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct AbiVersion {
18    major: u32,
19    minor: u32,
20}
21
22impl AbiVersion {
23    pub fn new(major: u32, minor: u32) -> Self {
24        Self { major, minor }
25    }
26
27    pub fn try_parse(s: &str) -> Option<Self> {
28        let parts: Vec<&str> = s.split('.').collect();
29        if parts.len() == 2 {
30            if let (Ok(major), Ok(minor)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
31                return Some(Self { major, minor });
32            }
33        }
34        None
35    }
36
37    pub fn to_string_val(&self) -> String {
38        format!("{}.{}", self.major, self.minor)
39    }
40
41    pub fn is_compatible_with(&self, other: &AbiVersion) -> bool {
42        // Same major version is compatible
43        self.major == other.major
44    }
45
46    pub fn is_backward_compatible(&self, older: &AbiVersion) -> bool {
47        // Major version must be the same
48        if self.major != older.major {
49            return false;
50        }
51        // Must be newer or equal
52        self.minor >= older.minor
53    }
54}
55
56/// OS/Architecture combination
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub struct PlatformArch {
59    pub platform: String,
60    pub architecture: String,
61}
62
63impl PlatformArch {
64    pub fn new(platform: &str, architecture: &str) -> Self {
65        Self {
66            platform: platform.to_string(),
67            architecture: architecture.to_string(),
68        }
69    }
70
71    pub fn as_string(&self) -> String {
72        format!("{}-{}", self.platform, self.architecture)
73    }
74
75    pub fn from_string(s: &str) -> Option<Self> {
76        let parts: Vec<&str> = s.split('-').collect();
77        if parts.len() == 2 {
78            Some(Self::new(parts[0], parts[1]))
79        } else {
80            None
81        }
82    }
83}
84
85/// Compatibility level for versions/platforms
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
87#[serde(rename_all = "PascalCase")]
88pub enum CompatibilityLevel {
89    Incompatible, // 0
90    Deprecated,   // 1
91    Compatible,   // 2
92    Recommended,  // 3
93}
94
95impl CompatibilityLevel {
96    pub fn as_str(&self) -> &'static str {
97        match self {
98            CompatibilityLevel::Incompatible => "Incompatible",
99            CompatibilityLevel::Deprecated => "Deprecated",
100            CompatibilityLevel::Compatible => "Compatible",
101            CompatibilityLevel::Recommended => "Recommended",
102        }
103    }
104
105    pub fn try_parse(s: &str) -> Option<Self> {
106        match s {
107            "Incompatible" => Some(CompatibilityLevel::Incompatible),
108            "Deprecated" => Some(CompatibilityLevel::Deprecated),
109            "Compatible" => Some(CompatibilityLevel::Compatible),
110            "Recommended" => Some(CompatibilityLevel::Recommended),
111            _ => None,
112        }
113    }
114}
115
116/// Breaking change information
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct BreakingChange {
119    pub from_version: String,
120    pub to_version: String,
121    pub description: String,
122    pub migration_guide: Option<String>,
123    pub affected_apis: Vec<String>,
124}
125
126/// ABI version compatibility entry
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct AbiCompatibilityEntry {
129    pub version: AbiVersion,
130    pub compatibility_with: HashMap<String, CompatibilityLevel>,
131    pub breaking_changes: Vec<BreakingChange>,
132    pub deprecation_notices: Vec<String>,
133}
134
135impl AbiCompatibilityEntry {
136    pub fn new(version: AbiVersion) -> Self {
137        Self {
138            version,
139            compatibility_with: HashMap::new(),
140            breaking_changes: Vec::new(),
141            deprecation_notices: Vec::new(),
142        }
143    }
144
145    pub fn is_compatible_with(&self, other_version: &str) -> bool {
146        matches!(
147            self.compatibility_with.get(other_version),
148            Some(CompatibilityLevel::Compatible) | Some(CompatibilityLevel::Recommended)
149        )
150    }
151}
152
153/// Platform support matrix entry
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct PlatformSupportEntry {
156    pub platform_arch: PlatformArch,
157    pub support_level: CompatibilityLevel,
158    pub min_version: Option<String>,
159    pub max_version: Option<String>,
160    pub notes: Option<String>,
161}
162
163/// Dependency compatibility check
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct DependencyCompatibility {
166    pub dependency_name: String,
167    pub required_version: String,
168    pub compatible_versions: Vec<String>,
169    pub incompatible_versions: Vec<String>,
170    pub notes: Option<String>,
171}
172
173/// Comprehensive compatibility matrix
174pub struct PluginCompatibilityMatrix {
175    plugin_id: String,
176    current_version: String,
177    abi_compatibility: HashMap<AbiVersion, AbiCompatibilityEntry>,
178    platform_support: HashMap<PlatformArch, PlatformSupportEntry>,
179    dependency_compatibility: HashMap<String, DependencyCompatibility>,
180}
181
182impl PluginCompatibilityMatrix {
183    /// Create a new compatibility matrix
184    pub fn new(plugin_id: &str, current_version: &str) -> Self {
185        Self {
186            plugin_id: plugin_id.to_string(),
187            current_version: current_version.to_string(),
188            abi_compatibility: HashMap::new(),
189            platform_support: HashMap::new(),
190            dependency_compatibility: HashMap::new(),
191        }
192    }
193
194    /// Add ABI compatibility information
195    pub fn add_abi_compatibility(&mut self, entry: AbiCompatibilityEntry) {
196        self.abi_compatibility.insert(entry.version.clone(), entry);
197    }
198
199    /// Add platform support information
200    pub fn add_platform_support(&mut self, entry: PlatformSupportEntry) {
201        self.platform_support
202            .insert(entry.platform_arch.clone(), entry);
203    }
204
205    /// Add dependency compatibility information
206    pub fn add_dependency_compatibility(&mut self, compat: DependencyCompatibility) {
207        self.dependency_compatibility
208            .insert(compat.dependency_name.clone(), compat);
209    }
210
211    /// Check if two ABI versions are compatible
212    pub fn check_abi_compatibility(&self, v1: &AbiVersion, v2: &AbiVersion) -> CompatibilityLevel {
213        if let Some(entry) = self.abi_compatibility.get(v1) {
214            entry
215                .compatibility_with
216                .get(&v2.to_string_val())
217                .copied()
218                .unwrap_or(CompatibilityLevel::Incompatible)
219        } else {
220            // Default: same major version is compatible
221            if v1.is_compatible_with(v2) {
222                CompatibilityLevel::Compatible
223            } else {
224                CompatibilityLevel::Incompatible
225            }
226        }
227    }
228
229    /// Check platform support
230    pub fn check_platform_support(&self, platform: &PlatformArch) -> CompatibilityLevel {
231        self.platform_support
232            .get(platform)
233            .map(|entry| entry.support_level)
234            .unwrap_or(CompatibilityLevel::Compatible)
235    }
236
237    /// Get all supported platforms
238    pub fn supported_platforms(&self) -> Vec<PlatformArch> {
239        self.platform_support
240            .values()
241            .filter(|entry| entry.support_level != CompatibilityLevel::Incompatible)
242            .map(|entry| entry.platform_arch.clone())
243            .collect()
244    }
245
246    /// Check dependency version compatibility
247    pub fn check_dependency_version(&self, dep_name: &str, version: &str) -> CompatibilityLevel {
248        if let Some(compat) = self.dependency_compatibility.get(dep_name) {
249            if compat.compatible_versions.contains(&version.to_string()) {
250                CompatibilityLevel::Compatible
251            } else if compat.incompatible_versions.contains(&version.to_string()) {
252                CompatibilityLevel::Incompatible
253            } else {
254                CompatibilityLevel::Compatible
255            }
256        } else {
257            CompatibilityLevel::Compatible
258        }
259    }
260
261    /// Find breaking changes between two versions
262    pub fn find_breaking_changes(
263        &self,
264        from_version: &str,
265        to_version: &str,
266    ) -> Vec<BreakingChange> {
267        let mut changes = Vec::new();
268        for entry in self.abi_compatibility.values() {
269            for change in &entry.breaking_changes {
270                if change.from_version == from_version && change.to_version == to_version {
271                    changes.push(change.clone());
272                }
273            }
274        }
275        changes
276    }
277
278    /// Get compatibility analysis for a specific ABI version
279    pub fn analyze_abi_version(&self, version: &AbiVersion) -> CompatibilityAnalysis {
280        let mut compatible_versions = Vec::new();
281        let mut incompatible_versions = Vec::new();
282        let mut breaking_changes_summary = Vec::new();
283
284        if let Some(entry) = self.abi_compatibility.get(version) {
285            for (ver_str, level) in &entry.compatibility_with {
286                match level {
287                    CompatibilityLevel::Compatible | CompatibilityLevel::Recommended => {
288                        compatible_versions.push(ver_str.clone());
289                    }
290                    CompatibilityLevel::Incompatible => {
291                        incompatible_versions.push(ver_str.clone());
292                    }
293                    _ => {}
294                }
295            }
296
297            for change in &entry.breaking_changes {
298                breaking_changes_summary.push(change.description.clone());
299            }
300        }
301
302        CompatibilityAnalysis {
303            version: version.to_string_val(),
304            compatible_versions,
305            incompatible_versions,
306            breaking_changes: breaking_changes_summary,
307            supported_platforms: self
308                .supported_platforms()
309                .iter()
310                .map(|p| p.as_string())
311                .collect(),
312            deprecation_status: self.abi_compatibility.get(version).and_then(|e| {
313                if e.deprecation_notices.is_empty() {
314                    None
315                } else {
316                    Some(e.deprecation_notices.clone())
317                }
318            }),
319        }
320    }
321
322    /// Get all supported ABI versions
323    pub fn all_abi_versions(&self) -> Vec<AbiVersion> {
324        self.abi_compatibility.keys().cloned().collect()
325    }
326
327    /// Generate compatibility report
328    pub fn generate_report(&self) -> CompatibilityReport {
329        CompatibilityReport {
330            plugin_id: self.plugin_id.clone(),
331            current_version: self.current_version.clone(),
332            total_abi_versions: self.abi_compatibility.len(),
333            supported_platform_count: self
334                .platform_support
335                .values()
336                .filter(|p| p.support_level != CompatibilityLevel::Incompatible)
337                .count(),
338            tracked_dependencies: self.dependency_compatibility.len(),
339        }
340    }
341}
342
343/// Compatibility analysis for a specific version
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct CompatibilityAnalysis {
346    pub version: String,
347    pub compatible_versions: Vec<String>,
348    pub incompatible_versions: Vec<String>,
349    pub breaking_changes: Vec<String>,
350    pub supported_platforms: Vec<String>,
351    pub deprecation_status: Option<Vec<String>>,
352}
353
354/// Compatibility report summary
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct CompatibilityReport {
357    pub plugin_id: String,
358    pub current_version: String,
359    pub total_abi_versions: usize,
360    pub supported_platform_count: usize,
361    pub tracked_dependencies: usize,
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_abi_version_creation() {
370        let v1 = AbiVersion::new(1, 0);
371        assert_eq!(v1.major, 1);
372        assert_eq!(v1.minor, 0);
373    }
374
375    #[test]
376    fn test_abi_version_try_parse() {
377        let v = AbiVersion::try_parse("2.1").unwrap();
378        assert_eq!(v.major, 2);
379        assert_eq!(v.minor, 1);
380    }
381
382    #[test]
383    fn test_abi_version_to_string() {
384        let v = AbiVersion::new(1, 5);
385        assert_eq!(v.to_string_val(), "1.5");
386    }
387
388    #[test]
389    fn test_abi_version_is_compatible() {
390        let v1 = AbiVersion::new(1, 0);
391        let v2 = AbiVersion::new(1, 5);
392        assert!(v1.is_compatible_with(&v2));
393    }
394
395    #[test]
396    fn test_abi_version_incompatible_major() {
397        let v1 = AbiVersion::new(1, 0);
398        let v2 = AbiVersion::new(2, 0);
399        assert!(!v1.is_compatible_with(&v2));
400    }
401
402    #[test]
403    fn test_abi_version_backward_compatible() {
404        let v_new = AbiVersion::new(1, 5);
405        let v_old = AbiVersion::new(1, 0);
406        assert!(v_new.is_backward_compatible(&v_old));
407    }
408
409    #[test]
410    fn test_abi_version_backward_incompatible() {
411        let v_old = AbiVersion::new(1, 5);
412        let v_new = AbiVersion::new(1, 0);
413        assert!(!v_new.is_backward_compatible(&v_old));
414    }
415
416    #[test]
417    fn test_platform_arch_creation() {
418        let pa = PlatformArch::new("linux", "x86_64");
419        assert_eq!(pa.platform, "linux");
420        assert_eq!(pa.architecture, "x86_64");
421    }
422
423    #[test]
424    fn test_platform_arch_as_string() {
425        let pa = PlatformArch::new("linux", "x86_64");
426        assert_eq!(pa.as_string(), "linux-x86_64");
427    }
428
429    #[test]
430    fn test_platform_arch_from_string() {
431        let pa = PlatformArch::from_string("linux-x86_64").unwrap();
432        assert_eq!(pa.platform, "linux");
433        assert_eq!(pa.architecture, "x86_64");
434    }
435
436    #[test]
437    fn test_compatibility_level_to_str() {
438        assert_eq!(CompatibilityLevel::Incompatible.as_str(), "Incompatible");
439        assert_eq!(CompatibilityLevel::Deprecated.as_str(), "Deprecated");
440        assert_eq!(CompatibilityLevel::Compatible.as_str(), "Compatible");
441        assert_eq!(CompatibilityLevel::Recommended.as_str(), "Recommended");
442    }
443
444    #[test]
445    fn test_compatibility_level_from_str() {
446        assert_eq!(
447            CompatibilityLevel::try_parse("Compatible"),
448            Some(CompatibilityLevel::Compatible)
449        );
450        assert_eq!(
451            CompatibilityLevel::try_parse("Incompatible"),
452            Some(CompatibilityLevel::Incompatible)
453        );
454    }
455
456    #[test]
457    fn test_compatibility_level_ordering() {
458        assert!(CompatibilityLevel::Incompatible < CompatibilityLevel::Deprecated);
459        assert!(CompatibilityLevel::Deprecated < CompatibilityLevel::Compatible);
460        assert!(CompatibilityLevel::Compatible < CompatibilityLevel::Recommended);
461    }
462
463    #[test]
464    fn test_abi_compatibility_entry_creation() {
465        let v = AbiVersion::new(1, 0);
466        let entry = AbiCompatibilityEntry::new(v);
467        assert_eq!(entry.version.major, 1);
468        assert_eq!(entry.breaking_changes.len(), 0);
469    }
470
471    #[test]
472    fn test_breaking_change_creation() {
473        let change = BreakingChange {
474            from_version: "1.0".to_string(),
475            to_version: "2.0".to_string(),
476            description: "API changed".to_string(),
477            migration_guide: Some("See upgrade guide".to_string()),
478            affected_apis: vec!["plugin_init".to_string()],
479        };
480        assert_eq!(change.from_version, "1.0");
481        assert_eq!(change.affected_apis.len(), 1);
482    }
483
484    #[test]
485    fn test_matrix_creation() {
486        let matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
487        assert_eq!(matrix.plugin_id, "test-plugin");
488        assert_eq!(matrix.current_version, "1.0.0");
489    }
490
491    #[test]
492    fn test_matrix_add_abi_compatibility() {
493        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
494        let entry = AbiCompatibilityEntry::new(AbiVersion::new(1, 0));
495        matrix.add_abi_compatibility(entry);
496        assert_eq!(matrix.abi_compatibility.len(), 1);
497    }
498
499    #[test]
500    fn test_matrix_add_platform_support() {
501        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
502        let pa = PlatformArch::new("linux", "x86_64");
503        let entry = PlatformSupportEntry {
504            platform_arch: pa,
505            support_level: CompatibilityLevel::Recommended,
506            min_version: None,
507            max_version: None,
508            notes: None,
509        };
510        matrix.add_platform_support(entry);
511        assert_eq!(matrix.platform_support.len(), 1);
512    }
513
514    #[test]
515    fn test_matrix_check_abi_compatibility_default() {
516        let matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
517        let v1 = AbiVersion::new(1, 0);
518        let v2 = AbiVersion::new(1, 5);
519        let level = matrix.check_abi_compatibility(&v1, &v2);
520        assert_eq!(level, CompatibilityLevel::Compatible);
521    }
522
523    #[test]
524    fn test_matrix_supported_platforms() {
525        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
526
527        let pa1 = PlatformArch::new("linux", "x86_64");
528        let entry1 = PlatformSupportEntry {
529            platform_arch: pa1,
530            support_level: CompatibilityLevel::Recommended,
531            min_version: None,
532            max_version: None,
533            notes: None,
534        };
535        matrix.add_platform_support(entry1);
536
537        let pa2 = PlatformArch::new("macos", "arm64");
538        let entry2 = PlatformSupportEntry {
539            platform_arch: pa2,
540            support_level: CompatibilityLevel::Compatible,
541            min_version: None,
542            max_version: None,
543            notes: None,
544        };
545        matrix.add_platform_support(entry2);
546
547        let platforms = matrix.supported_platforms();
548        assert_eq!(platforms.len(), 2);
549    }
550
551    #[test]
552    fn test_matrix_check_platform_support() {
553        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
554        let pa = PlatformArch::new("linux", "x86_64");
555        let entry = PlatformSupportEntry {
556            platform_arch: pa.clone(),
557            support_level: CompatibilityLevel::Recommended,
558            min_version: None,
559            max_version: None,
560            notes: None,
561        };
562        matrix.add_platform_support(entry);
563
564        let level = matrix.check_platform_support(&pa);
565        assert_eq!(level, CompatibilityLevel::Recommended);
566    }
567
568    #[test]
569    fn test_matrix_check_dependency_version() {
570        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
571        let compat = DependencyCompatibility {
572            dependency_name: "libfoo".to_string(),
573            required_version: "1.0".to_string(),
574            compatible_versions: vec!["1.0".to_string(), "1.1".to_string()],
575            incompatible_versions: vec!["2.0".to_string()],
576            notes: None,
577        };
578        matrix.add_dependency_compatibility(compat);
579
580        let level = matrix.check_dependency_version("libfoo", "1.0");
581        assert_eq!(level, CompatibilityLevel::Compatible);
582
583        let level_incompat = matrix.check_dependency_version("libfoo", "2.0");
584        assert_eq!(level_incompat, CompatibilityLevel::Incompatible);
585    }
586
587    #[test]
588    fn test_matrix_generate_report() {
589        let matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
590        let report = matrix.generate_report();
591        assert_eq!(report.plugin_id, "test-plugin");
592        assert_eq!(report.current_version, "1.0.0");
593    }
594
595    #[test]
596    fn test_compatibility_analysis_serialization() {
597        let analysis = CompatibilityAnalysis {
598            version: "1.0".to_string(),
599            compatible_versions: vec!["1.1".to_string()],
600            incompatible_versions: vec!["2.0".to_string()],
601            breaking_changes: vec![],
602            supported_platforms: vec!["linux-x86_64".to_string()],
603            deprecation_status: None,
604        };
605
606        let json = serde_json::to_string(&analysis).unwrap();
607        let deserialized: CompatibilityAnalysis = serde_json::from_str(&json).unwrap();
608
609        assert_eq!(deserialized.version, analysis.version);
610        assert_eq!(deserialized.compatible_versions.len(), 1);
611    }
612
613    #[test]
614    fn test_platform_support_entry_serialization() {
615        let pa = PlatformArch::new("linux", "x86_64");
616        let entry = PlatformSupportEntry {
617            platform_arch: pa,
618            support_level: CompatibilityLevel::Recommended,
619            min_version: Some("1.0".to_string()),
620            max_version: Some("2.0".to_string()),
621            notes: Some("Fully supported".to_string()),
622        };
623
624        let json = serde_json::to_string(&entry).unwrap();
625        let deserialized: PlatformSupportEntry = serde_json::from_str(&json).unwrap();
626
627        assert_eq!(deserialized.support_level, CompatibilityLevel::Recommended);
628    }
629
630    #[test]
631    fn test_dependency_compatibility_serialization() {
632        let compat = DependencyCompatibility {
633            dependency_name: "libfoo".to_string(),
634            required_version: "1.0".to_string(),
635            compatible_versions: vec!["1.0".to_string(), "1.1".to_string()],
636            incompatible_versions: vec!["2.0".to_string()],
637            notes: Some("Requires libfoo >= 1.0".to_string()),
638        };
639
640        let json = serde_json::to_string(&compat).unwrap();
641        let deserialized: DependencyCompatibility = serde_json::from_str(&json).unwrap();
642
643        assert_eq!(deserialized.dependency_name, "libfoo");
644        assert_eq!(deserialized.compatible_versions.len(), 2);
645    }
646
647    #[test]
648    fn test_matrix_find_breaking_changes() {
649        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
650
651        let change = BreakingChange {
652            from_version: "1.0".to_string(),
653            to_version: "2.0".to_string(),
654            description: "API changed".to_string(),
655            migration_guide: None,
656            affected_apis: vec!["plugin_init".to_string()],
657        };
658
659        let mut entry = AbiCompatibilityEntry::new(AbiVersion::new(2, 0));
660        entry.breaking_changes.push(change);
661        matrix.add_abi_compatibility(entry);
662
663        let changes = matrix.find_breaking_changes("1.0", "2.0");
664        assert_eq!(changes.len(), 1);
665        assert_eq!(changes[0].from_version, "1.0");
666    }
667
668    #[test]
669    fn test_matrix_analyze_abi_version() {
670        let mut matrix = PluginCompatibilityMatrix::new("test-plugin", "1.0.0");
671
672        let mut entry = AbiCompatibilityEntry::new(AbiVersion::new(1, 0));
673        entry
674            .compatibility_with
675            .insert("1.1".to_string(), CompatibilityLevel::Compatible);
676        entry
677            .compatibility_with
678            .insert("2.0".to_string(), CompatibilityLevel::Incompatible);
679
680        matrix.add_abi_compatibility(entry);
681
682        let analysis = matrix.analyze_abi_version(&AbiVersion::new(1, 0));
683        assert_eq!(analysis.compatible_versions.len(), 1);
684        assert_eq!(analysis.incompatible_versions.len(), 1);
685    }
686}