Skip to main content

plugin_packager/
metadata.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Plugin metadata extraction utilities
5//!
6//! This module provides functions for extracting and working with plugin metadata,
7//! including parsing manifests, extracting dependencies, and analyzing plugins.
8
9use crate::abi_compat::{
10    ABICompatibleInfo, ABIValidationResult, ABIValidator, ABIVersion,
11    CapabilityInfo as ABICapability, DependencyInfo as ABIDependency, MaturityLevel,
12    PluginCategory, ResourceRequirements,
13};
14use anyhow::Context;
15use serde::{Deserialize, Serialize};
16
17type Result<T> = anyhow::Result<T>;
18
19/// Rich metadata extracted from a plugin
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PluginMetadata {
22    /// Plugin name (unique identifier)
23    pub name: String,
24
25    /// Semantic version (e.g., "1.2.3")
26    pub version: String,
27
28    /// ABI version (e.g., "2.0")
29    pub abi_version: String,
30
31    /// Human-readable description
32    pub description: Option<String>,
33
34    /// Plugin author(s)
35    pub authors: Option<Vec<String>>,
36
37    /// License identifier (SPDX)
38    pub license: Option<String>,
39
40    /// Plugin keywords for searching
41    pub keywords: Option<Vec<String>>,
42
43    /// Categories/tags
44    pub categories: Option<Vec<String>>,
45
46    /// Repository URL
47    pub repository: Option<String>,
48
49    /// Homepage URL
50    pub homepage: Option<String>,
51
52    /// Documentation URL
53    pub documentation: Option<String>,
54
55    /// Plugin capabilities
56    pub capabilities: Option<Vec<String>>,
57
58    /// Plugin requirements
59    pub requirements: Option<PluginRequirements>,
60
61    /// Dependencies
62    pub dependencies: Option<Vec<DependencyMetadata>>,
63}
64
65/// Plugin requirements metadata
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PluginRequirements {
68    /// Maximum concurrency for requests
69    pub max_concurrency: Option<usize>,
70
71    /// Minimum memory required (in MB)
72    pub min_memory_mb: Option<usize>,
73
74    /// Timeout for operations (in seconds)
75    pub timeout_secs: Option<u64>,
76
77    /// Whether plugin handles streaming
78    pub supports_streaming: Option<bool>,
79}
80
81/// Dependency metadata
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct DependencyMetadata {
84    /// Dependency name
85    pub name: String,
86
87    /// Version requirement
88    pub version: String,
89
90    /// Whether this dependency is optional
91    pub optional: Option<bool>,
92
93    /// Features from dependency
94    pub features: Option<Vec<String>>,
95}
96
97/// Plugin statistics
98#[derive(Debug, Clone)]
99pub struct PluginStats {
100    /// Total size in bytes
101    pub size_bytes: u64,
102
103    /// Number of dependencies
104    pub dependency_count: usize,
105
106    /// Whether plugin has documentation
107    pub has_documentation: bool,
108
109    /// Whether plugin has changelog
110    pub has_changelog: bool,
111
112    /// Number of files in plugin
113    pub file_count: usize,
114}
115
116impl PluginMetadata {
117    /// Extract basic metadata from a manifest
118    pub fn from_manifest(manifest_content: &str) -> Result<Self> {
119        #[derive(Deserialize)]
120        struct ManifestPackage {
121            name: String,
122            version: String,
123            abi_version: String,
124            description: Option<String>,
125            authors: Option<Vec<String>>,
126            author: Option<String>,
127            license: Option<String>,
128            keywords: Option<Vec<String>>,
129            categories: Option<Vec<String>>,
130            repository: Option<String>,
131            homepage: Option<String>,
132            documentation: Option<String>,
133        }
134
135        #[derive(Deserialize)]
136        struct Capabilities {
137            handles_requests: Option<bool>,
138            provides_health_checks: Option<bool>,
139            supports_streaming: Option<bool>,
140            custom: Option<Vec<String>>,
141        }
142
143        #[derive(Deserialize)]
144        struct Requirements {
145            max_concurrency: Option<usize>,
146            min_memory_mb: Option<usize>,
147            timeout_secs: Option<u64>,
148            supports_streaming: Option<bool>,
149        }
150
151        #[derive(Deserialize)]
152        struct Dependency {
153            name: String,
154            version: String,
155            optional: Option<bool>,
156            features: Option<Vec<String>>,
157        }
158
159        #[derive(Deserialize)]
160        struct FullManifest {
161            #[serde(default)]
162            package: Option<ManifestPackage>,
163            #[serde(default)]
164            capabilities: Option<Capabilities>,
165            #[serde(default)]
166            requirements: Option<Requirements>,
167            #[serde(default)]
168            dependencies: Option<Vec<Dependency>>,
169        }
170
171        let manifest: FullManifest =
172            toml::from_str(manifest_content).context("parsing plugin manifest")?;
173
174        let pkg = manifest
175            .package
176            .context("manifest must have [package] section")?;
177
178        // Handle author(s)
179        let authors = pkg.authors.or_else(|| pkg.author.map(|a| vec![a]));
180
181        // Extract capabilities
182        let capabilities = manifest.capabilities.and_then(|c| {
183            let mut caps = Vec::new();
184            if c.handles_requests.unwrap_or(false) {
185                caps.push("handles_requests".to_string());
186            }
187            if c.provides_health_checks.unwrap_or(false) {
188                caps.push("provides_health_checks".to_string());
189            }
190            if c.supports_streaming.unwrap_or(false) {
191                caps.push("supports_streaming".to_string());
192            }
193            if let Some(custom) = c.custom {
194                caps.extend(custom);
195            }
196            if !caps.is_empty() {
197                Some(caps)
198            } else {
199                None
200            }
201        });
202
203        // Extract requirements
204        let requirements = manifest.requirements.map(|r| PluginRequirements {
205            max_concurrency: r.max_concurrency,
206            min_memory_mb: r.min_memory_mb,
207            timeout_secs: r.timeout_secs,
208            supports_streaming: r.supports_streaming,
209        });
210
211        // Extract dependencies
212        let dependencies = manifest.dependencies.map(|deps| {
213            deps.into_iter()
214                .map(|d| DependencyMetadata {
215                    name: d.name,
216                    version: d.version,
217                    optional: d.optional,
218                    features: d.features,
219                })
220                .collect()
221        });
222
223        Ok(PluginMetadata {
224            name: pkg.name,
225            version: pkg.version,
226            abi_version: pkg.abi_version,
227            description: pkg.description,
228            authors,
229            license: pkg.license,
230            keywords: pkg.keywords,
231            categories: pkg.categories,
232            repository: pkg.repository,
233            homepage: pkg.homepage,
234            documentation: pkg.documentation,
235            capabilities,
236            requirements,
237            dependencies,
238        })
239    }
240
241    /// Get total dependency count (including optional)
242    pub fn dependency_count(&self) -> usize {
243        self.dependencies.as_ref().map(|d| d.len()).unwrap_or(0)
244    }
245
246    /// Get required dependency count (excluding optional)
247    pub fn required_dependency_count(&self) -> usize {
248        self.dependencies
249            .as_ref()
250            .map(|d| {
251                d.iter()
252                    .filter(|dep| !dep.optional.unwrap_or(false))
253                    .count()
254            })
255            .unwrap_or(0)
256    }
257
258    /// Get optional dependency count
259    pub fn optional_dependency_count(&self) -> usize {
260        self.dependencies
261            .as_ref()
262            .map(|d| d.iter().filter(|dep| dep.optional.unwrap_or(false)).count())
263            .unwrap_or(0)
264    }
265
266    /// Check if plugin has all required metadata
267    pub fn is_valid(&self) -> bool {
268        !self.name.trim().is_empty()
269            && !self.version.trim().is_empty()
270            && !self.abi_version.trim().is_empty()
271    }
272
273    /// Get a human-readable summary
274    pub fn summary(&self) -> String {
275        format!(
276            "{} v{} (ABI: {})\n  {}\n  Dependencies: {} (required: {}, optional: {})",
277            self.name,
278            self.version,
279            self.abi_version,
280            self.description
281                .as_ref()
282                .unwrap_or(&"No description".to_string()),
283            self.dependency_count(),
284            self.required_dependency_count(),
285            self.optional_dependency_count()
286        )
287    }
288
289    /// Convert to ABI v2.0 compatible info for registry integration
290    pub fn to_abi_compatible(&self) -> Result<ABICompatibleInfo> {
291        // Parse ABI version
292        let abi_version = ABIVersion::parse(&self.abi_version)?;
293
294        // Convert capabilities
295        let capabilities = self
296            .capabilities
297            .as_ref()
298            .map(|caps| {
299                caps.iter()
300                    .map(|cap| ABICapability {
301                        name: cap.clone(),
302                        description: None,
303                        required_permission: None,
304                    })
305                    .collect()
306            })
307            .unwrap_or_default();
308
309        // Convert dependencies
310        let dependencies = self
311            .dependencies
312            .as_ref()
313            .map(|deps| {
314                deps.iter()
315                    .map(|dep| ABIDependency {
316                        name: dep.name.clone(),
317                        version_range: dep.version.clone(),
318                        required: !dep.optional.unwrap_or(false),
319                        service_type: None,
320                    })
321                    .collect()
322            })
323            .unwrap_or_default();
324
325        // Extract resource requirements from PluginRequirements
326        let resources = if let Some(reqs) = &self.requirements {
327            ResourceRequirements {
328                min_cpu_cores: 1,
329                max_cpu_cores: reqs.max_concurrency.unwrap_or(4) as u32,
330                min_memory_mb: reqs.min_memory_mb.unwrap_or(256) as u32,
331                max_memory_mb: (reqs.min_memory_mb.unwrap_or(256) * 2) as u32,
332                min_disk_mb: 100,
333                max_disk_mb: 1024,
334                requires_gpu: false,
335            }
336        } else {
337            ResourceRequirements::default()
338        };
339
340        Ok(ABICompatibleInfo {
341            name: self.name.clone(),
342            version: self.version.clone(),
343            abi_version,
344            skylet_version_min: None,
345            skylet_version_max: None,
346            maturity_level: MaturityLevel::Alpha, // Default to alpha until specified
347            category: PluginCategory::Utility,    // Default category
348            author: self.authors.as_ref().and_then(|a| a.first().cloned()),
349            license: self.license.clone(),
350            description: self.description.clone(),
351            capabilities,
352            dependencies,
353            resources,
354        })
355    }
356
357    /// Validate ABI v2.0 compatibility
358    pub fn validate_abi_compatibility(&self) -> Result<ABIValidationResult> {
359        let abi_info = self.to_abi_compatible()?;
360        Ok(ABIValidator::validate(&abi_info))
361    }
362
363    /// Check if plugin meets minimum ABI v2.0 requirements
364    pub fn is_abi_v2_compatible(&self) -> Result<bool> {
365        let abi_version = ABIVersion::parse(&self.abi_version)?;
366        Ok(matches!(abi_version, ABIVersion::V2 | ABIVersion::V3))
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn test_extract_metadata_basic() -> Result<()> {
376        let manifest = r#"
377[package]
378name = "test-plugin"
379version = "1.0.0"
380abi_version = "2.0"
381description = "A test plugin"
382license = "MIT"
383"#;
384
385        let metadata = PluginMetadata::from_manifest(manifest)?;
386        assert_eq!(metadata.name, "test-plugin");
387        assert_eq!(metadata.version, "1.0.0");
388        assert_eq!(metadata.abi_version, "2.0");
389        assert_eq!(metadata.description, Some("A test plugin".to_string()));
390
391        Ok(())
392    }
393
394    #[test]
395    fn test_extract_metadata_with_dependencies() -> Result<()> {
396        let manifest = r#"
397[package]
398name = "consumer-plugin"
399version = "1.0.0"
400abi_version = "2.0"
401
402[[dependencies]]
403name = "base-plugin"
404version = "^1.0.0"
405optional = false
406
407[[dependencies]]
408name = "optional-plugin"
409version = ">=1.0.0"
410optional = true
411"#;
412
413        let metadata = PluginMetadata::from_manifest(manifest)?;
414        assert_eq!(metadata.dependency_count(), 2);
415        assert_eq!(metadata.required_dependency_count(), 1);
416        assert_eq!(metadata.optional_dependency_count(), 1);
417
418        Ok(())
419    }
420
421    #[test]
422    fn test_extract_metadata_with_capabilities() -> Result<()> {
423        let manifest = r#"
424[package]
425name = "advanced-plugin"
426version = "1.0.0"
427abi_version = "2.0"
428
429[capabilities]
430handles_requests = true
431provides_health_checks = true
432custom = ["custom_feature"]
433
434[requirements]
435max_concurrency = 10
436min_memory_mb = 256
437timeout_secs = 30
438"#;
439
440        let metadata = PluginMetadata::from_manifest(manifest)?;
441        assert!(metadata.capabilities.is_some());
442        assert_eq!(metadata.capabilities.unwrap().len(), 3);
443        assert!(metadata.requirements.is_some());
444        let reqs = metadata.requirements.unwrap();
445        assert_eq!(reqs.max_concurrency, Some(10));
446        assert_eq!(reqs.min_memory_mb, Some(256));
447
448        Ok(())
449    }
450
451    #[test]
452    fn test_metadata_summary() -> Result<()> {
453        let manifest = r#"
454[package]
455name = "summary-test"
456version = "2.0.0"
457abi_version = "2.0"
458description = "A summary test plugin"
459"#;
460
461        let metadata = PluginMetadata::from_manifest(manifest)?;
462        let summary = metadata.summary();
463        assert!(summary.contains("summary-test"));
464        assert!(summary.contains("v2.0.0"));
465        assert!(summary.contains("A summary test plugin"));
466
467        Ok(())
468    }
469
470    #[test]
471    fn test_metadata_validation() -> Result<()> {
472        let manifest = r#"
473[package]
474name = "valid-plugin"
475version = "1.0.0"
476abi_version = "2.0"
477"#;
478
479        let metadata = PluginMetadata::from_manifest(manifest)?;
480        assert!(metadata.is_valid());
481
482        // Test invalid metadata
483        let invalid = PluginMetadata {
484            name: "".to_string(),
485            version: "1.0.0".to_string(),
486            abi_version: "2.0".to_string(),
487            description: None,
488            authors: None,
489            license: None,
490            keywords: None,
491            categories: None,
492            repository: None,
493            homepage: None,
494            documentation: None,
495            capabilities: None,
496            requirements: None,
497            dependencies: None,
498        };
499        assert!(!invalid.is_valid());
500
501        Ok(())
502    }
503}