1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PluginMetadata {
22 pub name: String,
24
25 pub version: String,
27
28 pub abi_version: String,
30
31 pub description: Option<String>,
33
34 pub authors: Option<Vec<String>>,
36
37 pub license: Option<String>,
39
40 pub keywords: Option<Vec<String>>,
42
43 pub categories: Option<Vec<String>>,
45
46 pub repository: Option<String>,
48
49 pub homepage: Option<String>,
51
52 pub documentation: Option<String>,
54
55 pub capabilities: Option<Vec<String>>,
57
58 pub requirements: Option<PluginRequirements>,
60
61 pub dependencies: Option<Vec<DependencyMetadata>>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PluginRequirements {
68 pub max_concurrency: Option<usize>,
70
71 pub min_memory_mb: Option<usize>,
73
74 pub timeout_secs: Option<u64>,
76
77 pub supports_streaming: Option<bool>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct DependencyMetadata {
84 pub name: String,
86
87 pub version: String,
89
90 pub optional: Option<bool>,
92
93 pub features: Option<Vec<String>>,
95}
96
97#[derive(Debug, Clone)]
99pub struct PluginStats {
100 pub size_bytes: u64,
102
103 pub dependency_count: usize,
105
106 pub has_documentation: bool,
108
109 pub has_changelog: bool,
111
112 pub file_count: usize,
114}
115
116impl PluginMetadata {
117 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 let authors = pkg.authors.or_else(|| pkg.author.map(|a| vec![a]));
180
181 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 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 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 pub fn dependency_count(&self) -> usize {
243 self.dependencies.as_ref().map(|d| d.len()).unwrap_or(0)
244 }
245
246 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 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 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 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 pub fn to_abi_compatible(&self) -> Result<ABICompatibleInfo> {
291 let abi_version = ABIVersion::parse(&self.abi_version)?;
293
294 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 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 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, category: PluginCategory::Utility, 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 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 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 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}