Skip to main content

plugin_packager/
abi_compat.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! ABI v2.0 compatibility layer
5//!
6//! This module provides compatibility between plugin-packager and the skylet-abi,
7//! ensuring plugins packaged here are compatible with the plugin lifecycle
8//! management system.
9
10use serde::{Deserialize, Serialize};
11
12/// ABI v2.0 specification levels
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ABIVersion {
15    V1,
16    V2,
17    V3,
18}
19
20impl ABIVersion {
21    /// Parse ABI version from string (e.g., "2.0")
22    pub fn parse(version_str: &str) -> anyhow::Result<Self> {
23        match version_str {
24            "1.0" | "1" => Ok(ABIVersion::V1),
25            "2.0" | "2" => Ok(ABIVersion::V2),
26            "3.0" | "3" => Ok(ABIVersion::V3),
27            _ => anyhow::bail!("Unknown ABI version: {}", version_str),
28        }
29    }
30
31    /// Get as string representation
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            ABIVersion::V1 => "1.0",
35            ABIVersion::V2 => "2.0",
36            ABIVersion::V3 => "3.0",
37        }
38    }
39
40    /// Check if this version is compatible with another
41    pub fn is_compatible_with(&self, other: &ABIVersion) -> bool {
42        // For now, only exact matches are compatible
43        // This can be refined as we understand compatibility requirements
44        self == other
45    }
46}
47
48/// Plugin maturity level for plugin classification
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum MaturityLevel {
51    Alpha,
52    Beta,
53    ReleaseCandidate,
54    Stable,
55    Deprecated,
56}
57
58impl MaturityLevel {
59    /// Parse maturity level from string
60    pub fn parse(level_str: &str) -> anyhow::Result<Self> {
61        match level_str.to_lowercase().as_str() {
62            "alpha" => Ok(MaturityLevel::Alpha),
63            "beta" => Ok(MaturityLevel::Beta),
64            "rc" | "releasecandidate" => Ok(MaturityLevel::ReleaseCandidate),
65            "stable" | "production" => Ok(MaturityLevel::Stable),
66            "deprecated" => Ok(MaturityLevel::Deprecated),
67            _ => anyhow::bail!("Unknown maturity level: {}", level_str),
68        }
69    }
70
71    /// Get as string representation
72    pub fn as_str(&self) -> &'static str {
73        match self {
74            MaturityLevel::Alpha => "alpha",
75            MaturityLevel::Beta => "beta",
76            MaturityLevel::ReleaseCandidate => "rc",
77            MaturityLevel::Stable => "stable",
78            MaturityLevel::Deprecated => "deprecated",
79        }
80    }
81}
82
83/// Plugin category for plugin classification
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum PluginCategory {
86    Utility,
87    Database,
88    Network,
89    Storage,
90    Security,
91    Monitoring,
92    Payment,
93    Integration,
94    Development,
95    Other,
96}
97
98impl PluginCategory {
99    /// Parse category from string
100    pub fn parse(category_str: &str) -> anyhow::Result<Self> {
101        match category_str.to_lowercase().as_str() {
102            "utility" => Ok(PluginCategory::Utility),
103            "database" => Ok(PluginCategory::Database),
104            "network" => Ok(PluginCategory::Network),
105            "storage" => Ok(PluginCategory::Storage),
106            "security" => Ok(PluginCategory::Security),
107            "monitoring" => Ok(PluginCategory::Monitoring),
108            "payment" => Ok(PluginCategory::Payment),
109            "integration" => Ok(PluginCategory::Integration),
110            "development" => Ok(PluginCategory::Development),
111            "other" => Ok(PluginCategory::Other),
112            _ => anyhow::bail!("Unknown plugin category: {}", category_str),
113        }
114    }
115
116    /// Get as string representation
117    pub fn as_str(&self) -> &'static str {
118        match self {
119            PluginCategory::Utility => "utility",
120            PluginCategory::Database => "database",
121            PluginCategory::Network => "network",
122            PluginCategory::Storage => "storage",
123            PluginCategory::Security => "security",
124            PluginCategory::Monitoring => "monitoring",
125            PluginCategory::Payment => "payment",
126            PluginCategory::Integration => "integration",
127            PluginCategory::Development => "development",
128            PluginCategory::Other => "other",
129        }
130    }
131}
132
133/// ABI v2.0 compatible plugin information
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ABICompatibleInfo {
136    /// Plugin name
137    pub name: String,
138
139    /// Semantic version (e.g., "1.2.3")
140    pub version: String,
141
142    /// ABI version required
143    pub abi_version: ABIVersion,
144
145    /// Minimum Skylet version required
146    pub skylet_version_min: Option<String>,
147
148    /// Maximum Skylet version supported
149    pub skylet_version_max: Option<String>,
150
151    /// Plugin maturity level
152    pub maturity_level: MaturityLevel,
153
154    /// Plugin category
155    pub category: PluginCategory,
156
157    /// Author/organization
158    pub author: Option<String>,
159
160    /// License (SPDX identifier, e.g., "MIT", "Apache-2.0")
161    pub license: Option<String>,
162
163    /// Long description
164    pub description: Option<String>,
165
166    /// Plugin-provided capabilities (function exports)
167    pub capabilities: Vec<CapabilityInfo>,
168
169    /// Plugin dependencies
170    pub dependencies: Vec<DependencyInfo>,
171
172    /// Required resource specifications
173    pub resources: ResourceRequirements,
174}
175
176/// Plugin capability information
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct CapabilityInfo {
179    /// Capability name (e.g., "http_request", "file_read")
180    pub name: String,
181
182    /// Capability description
183    pub description: Option<String>,
184
185    /// Required permission level (e.g., "user", "system", "root")
186    pub required_permission: Option<String>,
187}
188
189/// Plugin dependency information
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct DependencyInfo {
192    /// Dependency name
193    pub name: String,
194
195    /// Semantic version range (e.g., ">=1.0.0,<2.0.0")
196    pub version_range: String,
197
198    /// Whether this dependency is required
199    pub required: bool,
200
201    /// Service type/category for the dependency
202    pub service_type: Option<String>,
203}
204
205/// Resource requirements specification
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ResourceRequirements {
208    /// Minimum CPU cores
209    pub min_cpu_cores: u32,
210
211    /// Maximum CPU cores
212    pub max_cpu_cores: u32,
213
214    /// Minimum memory in MB
215    pub min_memory_mb: u32,
216
217    /// Maximum memory in MB
218    pub max_memory_mb: u32,
219
220    /// Minimum disk space in MB
221    pub min_disk_mb: u32,
222
223    /// Maximum disk space in MB
224    pub max_disk_mb: u32,
225
226    /// Whether GPU is required
227    pub requires_gpu: bool,
228}
229
230impl Default for ResourceRequirements {
231    fn default() -> Self {
232        Self {
233            min_cpu_cores: 1,
234            max_cpu_cores: 4,
235            min_memory_mb: 256,
236            max_memory_mb: 1024,
237            min_disk_mb: 100,
238            max_disk_mb: 1024,
239            requires_gpu: false,
240        }
241    }
242}
243
244/// ABI v2.0 validation result
245#[derive(Debug, Clone)]
246pub struct ABIValidationResult {
247    /// Whether the plugin is ABI v2.0 compatible
248    pub is_compatible: bool,
249
250    /// Validation warnings
251    pub warnings: Vec<String>,
252
253    /// Validation errors
254    pub errors: Vec<String>,
255
256    /// Recommended fixes
257    pub recommendations: Vec<String>,
258}
259
260impl ABIValidationResult {
261    /// Create new validation result
262    pub fn new(is_compatible: bool) -> Self {
263        Self {
264            is_compatible,
265            warnings: Vec::new(),
266            errors: Vec::new(),
267            recommendations: Vec::new(),
268        }
269    }
270
271    /// Add warning
272    pub fn add_warning(&mut self, message: String) {
273        self.warnings.push(message);
274    }
275
276    /// Add error
277    pub fn add_error(&mut self, message: String) {
278        self.errors.push(message);
279        self.is_compatible = false;
280    }
281
282    /// Add recommendation
283    pub fn add_recommendation(&mut self, message: String) {
284        self.recommendations.push(message);
285    }
286
287    /// Check if validation passed
288    pub fn passed(&self) -> bool {
289        self.is_compatible && self.errors.is_empty()
290    }
291}
292
293/// ABI compatibility validator
294pub struct ABIValidator;
295
296impl ABIValidator {
297    /// Validate ABI v2.0 compatibility
298    pub fn validate(info: &ABICompatibleInfo) -> ABIValidationResult {
299        let mut result = ABIValidationResult::new(true);
300
301        // Validate ABI version
302        if info.abi_version == ABIVersion::V1 {
303            result.add_warning(
304                "ABI v1.0 is deprecated. Consider upgrading to v2.0 for new features.".to_string(),
305            );
306        } else if info.abi_version == ABIVersion::V3 {
307            result.add_warning(
308                "ABI v3.0 is experimental. Some compatibility issues may occur.".to_string(),
309            );
310        }
311
312        // Validate maturity level
313        if info.maturity_level == MaturityLevel::Alpha {
314            result.add_warning("Plugin is marked as alpha. It may not be stable.".to_string());
315        } else if info.maturity_level == MaturityLevel::Deprecated {
316            result.add_error("Plugin is marked as deprecated.".to_string());
317        }
318
319        // Validate capabilities
320        if info.capabilities.is_empty() {
321            result.add_recommendation(
322                "Plugin declares no capabilities. Add at least one capability for discoverability."
323                    .to_string(),
324            );
325        }
326
327        // Validate resource requirements
328        if info.resources.min_cpu_cores > info.resources.max_cpu_cores {
329            result
330                .add_error("Resource requirement error: min_cpu_cores > max_cpu_cores".to_string());
331        }
332        if info.resources.min_memory_mb > info.resources.max_memory_mb {
333            result
334                .add_error("Resource requirement error: min_memory_mb > max_memory_mb".to_string());
335        }
336        if info.resources.min_disk_mb > info.resources.max_disk_mb {
337            result.add_error("Resource requirement error: min_disk_mb > max_disk_mb".to_string());
338        }
339
340        // Validate dependencies
341        for dep in &info.dependencies {
342            if dep.name.is_empty() {
343                result.add_error("Dependency with empty name found.".to_string());
344            }
345            if dep.version_range.is_empty() {
346                result.add_error(format!(
347                    "Dependency '{}' has empty version range.",
348                    dep.name
349                ));
350            }
351        }
352
353        // Validate license
354        if info.license.is_none() {
355            result.add_recommendation(
356                "No license specified. Add a license for registry publishing.".to_string(),
357            );
358        }
359
360        // Validate author
361        if info.author.is_none() {
362            result.add_warning("No author specified.".to_string());
363        }
364
365        result
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_abi_version_parse() {
375        assert_eq!(ABIVersion::parse("1.0").unwrap(), ABIVersion::V1);
376        assert_eq!(ABIVersion::parse("2.0").unwrap(), ABIVersion::V2);
377        assert_eq!(ABIVersion::parse("3.0").unwrap(), ABIVersion::V3);
378        assert_eq!(ABIVersion::parse("2").unwrap(), ABIVersion::V2);
379    }
380
381    #[test]
382    fn test_abi_version_as_str() {
383        assert_eq!(ABIVersion::V1.as_str(), "1.0");
384        assert_eq!(ABIVersion::V2.as_str(), "2.0");
385        assert_eq!(ABIVersion::V3.as_str(), "3.0");
386    }
387
388    #[test]
389    fn test_abi_version_compatibility() {
390        assert!(ABIVersion::V2.is_compatible_with(&ABIVersion::V2));
391        assert!(!ABIVersion::V2.is_compatible_with(&ABIVersion::V1));
392    }
393
394    #[test]
395    fn test_maturity_level_parse() {
396        assert_eq!(MaturityLevel::parse("alpha").unwrap(), MaturityLevel::Alpha);
397        assert_eq!(
398            MaturityLevel::parse("stable").unwrap(),
399            MaturityLevel::Stable
400        );
401        assert_eq!(
402            MaturityLevel::parse("production").unwrap(),
403            MaturityLevel::Stable
404        );
405    }
406
407    #[test]
408    fn test_plugin_category_parse() {
409        assert_eq!(
410            PluginCategory::parse("database").unwrap(),
411            PluginCategory::Database
412        );
413        assert_eq!(
414            PluginCategory::parse("security").unwrap(),
415            PluginCategory::Security
416        );
417    }
418
419    #[test]
420    fn test_resource_requirements_default() {
421        let res = ResourceRequirements::default();
422        assert_eq!(res.min_cpu_cores, 1);
423        assert_eq!(res.min_memory_mb, 256);
424    }
425
426    #[test]
427    fn test_abi_validation_basic() {
428        let info = ABICompatibleInfo {
429            name: "test-plugin".to_string(),
430            version: "1.0.0".to_string(),
431            abi_version: ABIVersion::V2,
432            skylet_version_min: None,
433            skylet_version_max: None,
434            maturity_level: MaturityLevel::Stable,
435            category: PluginCategory::Utility,
436            author: Some("Test Author".to_string()),
437            license: Some("MIT".to_string()),
438            description: Some("Test plugin".to_string()),
439            capabilities: vec![],
440            dependencies: vec![],
441            resources: ResourceRequirements::default(),
442        };
443
444        let result = ABIValidator::validate(&info);
445        assert!(result.is_compatible);
446        // Note: result.passed() checks for no errors AND compatibility
447        // This result has warnings/recommendations but no errors
448        assert!(
449            !result.errors.is_empty()
450                || !result.warnings.is_empty()
451                || !result.recommendations.is_empty()
452        );
453    }
454
455    #[test]
456    fn test_abi_validation_errors() {
457        let mut resources = ResourceRequirements::default();
458        resources.min_cpu_cores = 10;
459        resources.max_cpu_cores = 2; // Invalid: min > max
460
461        let info = ABICompatibleInfo {
462            name: "test-plugin".to_string(),
463            version: "1.0.0".to_string(),
464            abi_version: ABIVersion::V2,
465            skylet_version_min: None,
466            skylet_version_max: None,
467            maturity_level: MaturityLevel::Stable,
468            category: PluginCategory::Utility,
469            author: None,
470            license: None,
471            description: None,
472            capabilities: vec![],
473            dependencies: vec![],
474            resources,
475        };
476
477        let result = ABIValidator::validate(&info);
478        assert!(!result.is_compatible);
479        assert!(!result.errors.is_empty());
480    }
481
482    #[test]
483    fn test_abi_validation_deprecated() {
484        let info = ABICompatibleInfo {
485            name: "test-plugin".to_string(),
486            version: "1.0.0".to_string(),
487            abi_version: ABIVersion::V2,
488            skylet_version_min: None,
489            skylet_version_max: None,
490            maturity_level: MaturityLevel::Deprecated,
491            category: PluginCategory::Utility,
492            author: None,
493            license: None,
494            description: None,
495            capabilities: vec![],
496            dependencies: vec![],
497            resources: ResourceRequirements::default(),
498        };
499
500        let result = ABIValidator::validate(&info);
501        assert!(!result.is_compatible);
502        assert!(result.errors.iter().any(|e| e.contains("deprecated")));
503    }
504}