Skip to main content

appscale_core/
plugins.rs

1//! Plugin Marketplace — Plugin specification, registry, and discovery.
2//!
3//! Extends the `NativeModule` system with:
4//! 1. **Plugin Spec**: Metadata describing a plugin (name, version, platforms,
5//!    capabilities, dependencies).
6//! 2. **Plugin Registry**: Manages installed plugins, resolves dependencies,
7//!    and tracks lifecycle.
8//! 3. **Discovery & Compatibility**: Version compatibility checking, platform
9//!    support matrix, and search/filtering for a future marketplace UI.
10//!
11//! Plugins wrap `NativeModule`s with package-level metadata so the framework
12//! can manage them as first-class ecosystem citizens.
13
14use crate::modules::{NativeModule, ModuleRegistry, ModuleError};
15use crate::cloud::BuildTarget;
16use serde::{Serialize, Deserialize};
17use std::collections::HashMap;
18use std::sync::Arc;
19
20// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21// Errors
22// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23
24#[derive(Debug, thiserror::Error)]
25pub enum PluginError {
26    #[error("Plugin not found: {0}")]
27    NotFound(String),
28
29    #[error("Plugin already installed: {0} v{1}")]
30    AlreadyInstalled(String, String),
31
32    #[error("Incompatible plugin: {plugin} requires engine >={required}, have {current}")]
33    IncompatibleEngine { plugin: String, required: String, current: String },
34
35    #[error("Platform not supported: {plugin} does not support {platform:?}")]
36    PlatformNotSupported { plugin: String, platform: BuildTarget },
37
38    #[error("Missing dependency: {plugin} requires {dependency}")]
39    MissingDependency { plugin: String, dependency: String },
40
41    #[error("Module registration error: {0}")]
42    ModuleError(#[from] ModuleError),
43
44    #[error("Plugin error: {0}")]
45    Internal(String),
46}
47
48pub type PluginResult<T> = Result<T, PluginError>;
49
50// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51// 1. Plugin Spec
52// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53
54/// Semantic version for plugins.
55#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
56pub struct PluginVersion {
57    pub major: u32,
58    pub minor: u32,
59    pub patch: u32,
60}
61
62impl PluginVersion {
63    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
64        Self { major, minor, patch }
65    }
66
67    /// Check if this version satisfies a minimum version requirement.
68    pub fn satisfies_min(&self, min: &PluginVersion) -> bool {
69        self >= min
70    }
71}
72
73impl std::fmt::Display for PluginVersion {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
76    }
77}
78
79/// What a plugin provides to the framework.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum PluginCapability {
82    /// Provides a native module callable from JS.
83    NativeModule,
84    /// Provides UI components.
85    Components,
86    /// Provides a storage backend.
87    Storage,
88    /// Provides platform-specific functionality (camera, haptics, etc.).
89    PlatformFeature(String),
90    /// Provides theme/styling resources.
91    Theme,
92    /// Provides navigation integration.
93    Navigation,
94}
95
96/// A dependency on another plugin.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PluginDependency {
99    pub name: String,
100    pub min_version: PluginVersion,
101}
102
103/// Category for marketplace browsing.
104#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub enum PluginCategory {
106    UI,
107    Data,
108    Media,
109    Platform,
110    Analytics,
111    Auth,
112    Networking,
113    DevTools,
114    Other,
115}
116
117/// Full descriptor for a plugin package.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct PluginDescriptor {
120    pub name: String,
121    pub version: PluginVersion,
122    pub description: String,
123    pub author: String,
124    pub license: String,
125    pub homepage: Option<String>,
126    pub repository: Option<String>,
127    pub category: PluginCategory,
128    pub capabilities: Vec<PluginCapability>,
129    pub supported_platforms: Vec<BuildTarget>,
130    pub min_engine_version: PluginVersion,
131    pub dependencies: Vec<PluginDependency>,
132    pub js_package: Option<String>,
133    pub keywords: Vec<String>,
134}
135
136impl PluginDescriptor {
137    /// Check if this plugin supports the given platform.
138    pub fn supports_platform(&self, target: &BuildTarget) -> bool {
139        self.supported_platforms.contains(target)
140    }
141
142    /// Check if this plugin is compatible with the given engine version.
143    pub fn is_compatible_with_engine(&self, engine_version: &PluginVersion) -> bool {
144        engine_version.satisfies_min(&self.min_engine_version)
145    }
146
147    /// Check if all dependencies are satisfied by the given installed set.
148    pub fn check_dependencies(&self, installed: &HashMap<String, PluginVersion>) -> Vec<String> {
149        let mut missing = Vec::new();
150        for dep in &self.dependencies {
151            match installed.get(&dep.name) {
152                Some(v) if v.satisfies_min(&dep.min_version) => {}
153                _ => missing.push(dep.name.clone()),
154            }
155        }
156        missing
157    }
158}
159
160/// Validates a plugin descriptor for completeness.
161pub fn validate_descriptor(desc: &PluginDescriptor) -> PluginResult<()> {
162    if desc.name.is_empty() {
163        return Err(PluginError::Internal("Plugin name is required".into()));
164    }
165    if desc.description.is_empty() {
166        return Err(PluginError::Internal("Plugin description is required".into()));
167    }
168    if desc.supported_platforms.is_empty() {
169        return Err(PluginError::Internal("At least one supported platform is required".into()));
170    }
171    if desc.capabilities.is_empty() {
172        return Err(PluginError::Internal("At least one capability is required".into()));
173    }
174    Ok(())
175}
176
177// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
178// 2. Plugin Registry
179// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
180
181/// An installed plugin with its descriptor and optional native module.
182struct InstalledPlugin {
183    descriptor: PluginDescriptor,
184    module: Option<Arc<dyn NativeModule>>,
185    enabled: bool,
186}
187
188/// Registry managing all installed plugins.
189pub struct PluginRegistry {
190    plugins: HashMap<String, InstalledPlugin>,
191    module_registry: ModuleRegistry,
192    engine_version: PluginVersion,
193}
194
195impl PluginRegistry {
196    pub fn new(engine_version: PluginVersion) -> Self {
197        Self {
198            plugins: HashMap::new(),
199            module_registry: ModuleRegistry::new(),
200            engine_version,
201        }
202    }
203
204    /// Install a plugin. Validates compatibility and registers its native module.
205    pub fn install(
206        &mut self,
207        descriptor: PluginDescriptor,
208        module: Option<Arc<dyn NativeModule>>,
209    ) -> PluginResult<()> {
210        validate_descriptor(&descriptor)?;
211
212        // Check engine compatibility
213        if !descriptor.is_compatible_with_engine(&self.engine_version) {
214            return Err(PluginError::IncompatibleEngine {
215                plugin: descriptor.name.clone(),
216                required: descriptor.min_engine_version.to_string(),
217                current: self.engine_version.to_string(),
218            });
219        }
220
221        // Check not already installed
222        if self.plugins.contains_key(&descriptor.name) {
223            return Err(PluginError::AlreadyInstalled(
224                descriptor.name.clone(),
225                descriptor.version.to_string(),
226            ));
227        }
228
229        // Check dependencies
230        let installed_versions: HashMap<String, PluginVersion> = self.plugins.iter()
231            .map(|(k, v)| (k.clone(), v.descriptor.version.clone()))
232            .collect();
233        let missing = descriptor.check_dependencies(&installed_versions);
234        if !missing.is_empty() {
235            return Err(PluginError::MissingDependency {
236                plugin: descriptor.name.clone(),
237                dependency: missing.join(", "),
238            });
239        }
240
241        // Register native module if provided
242        if let Some(ref m) = module {
243            self.module_registry.register(m.clone())?;
244        }
245
246        let name = descriptor.name.clone();
247        self.plugins.insert(name, InstalledPlugin {
248            descriptor,
249            module,
250            enabled: true,
251        });
252
253        Ok(())
254    }
255
256    /// Uninstall a plugin by name.
257    pub fn uninstall(&mut self, name: &str) -> PluginResult<()> {
258        // Check no other plugin depends on this one before removing
259        let dependent = self.plugins.iter()
260            .find(|(_, other)| other.descriptor.dependencies.iter().any(|d| d.name == name))
261            .map(|(n, _)| n.clone());
262        if let Some(dep_name) = dependent {
263            return Err(PluginError::Internal(
264                format!("Cannot uninstall '{}': required by '{}'", name, dep_name),
265            ));
266        }
267
268        let plugin = self.plugins.remove(name)
269            .ok_or_else(|| PluginError::NotFound(name.into()))?;
270
271        // Unregister native module
272        if plugin.module.is_some() {
273            self.module_registry.unregister(name);
274        }
275
276        Ok(())
277    }
278
279    /// Enable or disable a plugin.
280    pub fn set_enabled(&mut self, name: &str, enabled: bool) -> PluginResult<()> {
281        let plugin = self.plugins.get_mut(name)
282            .ok_or_else(|| PluginError::NotFound(name.into()))?;
283        plugin.enabled = enabled;
284        Ok(())
285    }
286
287    /// Get a plugin descriptor by name.
288    pub fn get_descriptor(&self, name: &str) -> Option<&PluginDescriptor> {
289        self.plugins.get(name).map(|p| &p.descriptor)
290    }
291
292    /// Check if a plugin is installed.
293    pub fn is_installed(&self, name: &str) -> bool {
294        self.plugins.contains_key(name)
295    }
296
297    /// List all installed plugin names.
298    pub fn installed_names(&self) -> Vec<String> {
299        self.plugins.keys().cloned().collect()
300    }
301
302    /// List enabled plugins.
303    pub fn enabled_plugins(&self) -> Vec<&PluginDescriptor> {
304        self.plugins.values()
305            .filter(|p| p.enabled)
306            .map(|p| &p.descriptor)
307            .collect()
308    }
309
310    /// Number of installed plugins.
311    pub fn count(&self) -> usize {
312        self.plugins.len()
313    }
314
315    /// Get the underlying module registry (for bridge calls).
316    pub fn module_registry(&self) -> &ModuleRegistry {
317        &self.module_registry
318    }
319
320    // ── Discovery & Filtering ──
321
322    /// Search plugins by keyword (matches name, description, keywords).
323    pub fn search(&self, query: &str) -> Vec<&PluginDescriptor> {
324        let q = query.to_lowercase();
325        self.plugins.values()
326            .map(|p| &p.descriptor)
327            .filter(|d| {
328                d.name.to_lowercase().contains(&q)
329                    || d.description.to_lowercase().contains(&q)
330                    || d.keywords.iter().any(|k| k.to_lowercase().contains(&q))
331            })
332            .collect()
333    }
334
335    /// Filter plugins by category.
336    pub fn by_category(&self, category: &PluginCategory) -> Vec<&PluginDescriptor> {
337        self.plugins.values()
338            .map(|p| &p.descriptor)
339            .filter(|d| d.category == *category)
340            .collect()
341    }
342
343    /// Filter plugins by platform support.
344    pub fn by_platform(&self, target: &BuildTarget) -> Vec<&PluginDescriptor> {
345        self.plugins.values()
346            .map(|p| &p.descriptor)
347            .filter(|d| d.supports_platform(target))
348            .collect()
349    }
350
351    /// Filter plugins by capability.
352    pub fn by_capability(&self, cap: &PluginCapability) -> Vec<&PluginDescriptor> {
353        self.plugins.values()
354            .map(|p| &p.descriptor)
355            .filter(|d| d.capabilities.contains(cap))
356            .collect()
357    }
358}
359
360// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
361// Tests
362// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use crate::modules::{MethodDescriptor, ModuleArg, ModuleResult, ModuleValue};
368
369    // ── Test module ──
370    struct TestModule { name: String }
371    impl NativeModule for TestModule {
372        fn name(&self) -> &str { &self.name }
373        fn methods(&self) -> Vec<MethodDescriptor> {
374            vec![MethodDescriptor::sync("ping", "returns pong")]
375        }
376        fn invoke_sync(&self, method: &str, _args: &[ModuleArg]) -> ModuleResult {
377            match method {
378                "ping" => Ok(ModuleValue::String("pong".into())),
379                _ => Err(ModuleError::MethodNotFound {
380                    module: self.name.clone(), method: method.into(),
381                }),
382            }
383        }
384    }
385
386    fn test_descriptor(name: &str) -> PluginDescriptor {
387        PluginDescriptor {
388            name: name.into(),
389            version: PluginVersion::new(1, 0, 0),
390            description: format!("{name} plugin"),
391            author: "test".into(),
392            license: "MIT".into(),
393            homepage: None,
394            repository: None,
395            category: PluginCategory::Platform,
396            capabilities: vec![PluginCapability::NativeModule],
397            supported_platforms: vec![BuildTarget::Ios, BuildTarget::Android, BuildTarget::Web],
398            min_engine_version: PluginVersion::new(0, 1, 0),
399            dependencies: vec![],
400            js_package: None,
401            keywords: vec!["test".into()],
402        }
403    }
404
405    // ── Plugin Spec Tests ──
406
407    #[test]
408    fn test_plugin_version_comparison() {
409        let v1 = PluginVersion::new(1, 0, 0);
410        let v1_1 = PluginVersion::new(1, 1, 0);
411        let v2 = PluginVersion::new(2, 0, 0);
412
413        assert!(v1 < v1_1);
414        assert!(v1_1 < v2);
415        assert!(v1.satisfies_min(&v1));
416        assert!(v1_1.satisfies_min(&v1));
417        assert!(!v1.satisfies_min(&v2));
418    }
419
420    #[test]
421    fn test_plugin_version_display() {
422        assert_eq!(PluginVersion::new(3, 2, 1).to_string(), "3.2.1");
423    }
424
425    #[test]
426    fn test_descriptor_platform_support() {
427        let desc = test_descriptor("Camera");
428        assert!(desc.supports_platform(&BuildTarget::Ios));
429        assert!(!desc.supports_platform(&BuildTarget::Linux));
430    }
431
432    #[test]
433    fn test_descriptor_engine_compatibility() {
434        let desc = test_descriptor("Test");
435        assert!(desc.is_compatible_with_engine(&PluginVersion::new(1, 0, 0)));
436        assert!(desc.is_compatible_with_engine(&PluginVersion::new(0, 1, 0)));
437        assert!(!desc.is_compatible_with_engine(&PluginVersion::new(0, 0, 9)));
438    }
439
440    #[test]
441    fn test_descriptor_dependency_check() {
442        let mut desc = test_descriptor("Dep");
443        desc.dependencies.push(PluginDependency {
444            name: "Core".into(),
445            min_version: PluginVersion::new(1, 0, 0),
446        });
447
448        let mut installed = HashMap::new();
449        assert_eq!(desc.check_dependencies(&installed), vec!["Core"]);
450
451        installed.insert("Core".into(), PluginVersion::new(1, 0, 0));
452        assert!(desc.check_dependencies(&installed).is_empty());
453    }
454
455    #[test]
456    fn test_validate_descriptor_ok() {
457        let desc = test_descriptor("Valid");
458        assert!(validate_descriptor(&desc).is_ok());
459    }
460
461    #[test]
462    fn test_validate_descriptor_empty_name() {
463        let mut desc = test_descriptor("");
464        desc.name = String::new();
465        assert!(validate_descriptor(&desc).is_err());
466    }
467
468    #[test]
469    fn test_validate_descriptor_no_platforms() {
470        let mut desc = test_descriptor("NoPlatform");
471        desc.supported_platforms.clear();
472        assert!(validate_descriptor(&desc).is_err());
473    }
474
475    // ── Plugin Registry Tests ──
476
477    #[test]
478    fn test_install_plugin_without_module() {
479        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
480        let desc = test_descriptor("Analytics");
481        assert!(registry.install(desc, None).is_ok());
482        assert!(registry.is_installed("Analytics"));
483        assert_eq!(registry.count(), 1);
484    }
485
486    #[test]
487    fn test_install_plugin_with_module() {
488        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
489        let desc = test_descriptor("Camera");
490        let module = Arc::new(TestModule { name: "Camera".into() }) as Arc<dyn NativeModule>;
491        assert!(registry.install(desc, Some(module)).is_ok());
492        assert!(registry.module_registry().has("Camera"));
493    }
494
495    #[test]
496    fn test_install_duplicate_rejected() {
497        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
498        assert!(registry.install(test_descriptor("Dup"), None).is_ok());
499        assert!(registry.install(test_descriptor("Dup"), None).is_err());
500    }
501
502    #[test]
503    fn test_install_incompatible_engine_rejected() {
504        let mut registry = PluginRegistry::new(PluginVersion::new(0, 0, 1));
505        let mut desc = test_descriptor("New");
506        desc.min_engine_version = PluginVersion::new(2, 0, 0);
507        assert!(registry.install(desc, None).is_err());
508    }
509
510    #[test]
511    fn test_install_missing_dependency_rejected() {
512        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
513        let mut desc = test_descriptor("Child");
514        desc.dependencies.push(PluginDependency {
515            name: "Parent".into(),
516            min_version: PluginVersion::new(1, 0, 0),
517        });
518        assert!(registry.install(desc, None).is_err());
519    }
520
521    #[test]
522    fn test_install_with_satisfied_dependency() {
523        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
524        assert!(registry.install(test_descriptor("Parent"), None).is_ok());
525
526        let mut child = test_descriptor("Child");
527        child.dependencies.push(PluginDependency {
528            name: "Parent".into(),
529            min_version: PluginVersion::new(1, 0, 0),
530        });
531        assert!(registry.install(child, None).is_ok());
532    }
533
534    #[test]
535    fn test_uninstall_plugin() {
536        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
537        registry.install(test_descriptor("Removable"), None).unwrap();
538        assert!(registry.uninstall("Removable").is_ok());
539        assert!(!registry.is_installed("Removable"));
540    }
541
542    #[test]
543    fn test_uninstall_depended_on_fails() {
544        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
545        registry.install(test_descriptor("Base"), None).unwrap();
546
547        let mut child = test_descriptor("Extension");
548        child.dependencies.push(PluginDependency {
549            name: "Base".into(),
550            min_version: PluginVersion::new(1, 0, 0),
551        });
552        registry.install(child, None).unwrap();
553
554        // Can't uninstall Base — Extension depends on it
555        assert!(registry.uninstall("Base").is_err());
556        assert!(registry.is_installed("Base"));
557    }
558
559    #[test]
560    fn test_enable_disable() {
561        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
562        registry.install(test_descriptor("Toggle"), None).unwrap();
563
564        assert_eq!(registry.enabled_plugins().len(), 1);
565        registry.set_enabled("Toggle", false).unwrap();
566        assert_eq!(registry.enabled_plugins().len(), 0);
567        registry.set_enabled("Toggle", true).unwrap();
568        assert_eq!(registry.enabled_plugins().len(), 1);
569    }
570
571    // ── Discovery Tests ──
572
573    #[test]
574    fn test_search_by_name() {
575        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
576        registry.install(test_descriptor("Camera"), None).unwrap();
577        registry.install(test_descriptor("Storage"), None).unwrap();
578        registry.install(test_descriptor("CameraRoll"), None).unwrap();
579
580        let results = registry.search("camera");
581        assert_eq!(results.len(), 2);
582    }
583
584    #[test]
585    fn test_search_by_keyword() {
586        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
587        let mut desc = test_descriptor("Tracker");
588        desc.keywords.push("analytics".into());
589        registry.install(desc, None).unwrap();
590
591        let results = registry.search("analytics");
592        assert_eq!(results.len(), 1);
593        assert_eq!(results[0].name, "Tracker");
594    }
595
596    #[test]
597    fn test_filter_by_category() {
598        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
599
600        let mut ui_plugin = test_descriptor("Button");
601        ui_plugin.category = PluginCategory::UI;
602        registry.install(ui_plugin, None).unwrap();
603
604        let mut data_plugin = test_descriptor("SQLite");
605        data_plugin.category = PluginCategory::Data;
606        registry.install(data_plugin, None).unwrap();
607
608        assert_eq!(registry.by_category(&PluginCategory::UI).len(), 1);
609        assert_eq!(registry.by_category(&PluginCategory::Data).len(), 1);
610        assert_eq!(registry.by_category(&PluginCategory::Auth).len(), 0);
611    }
612
613    #[test]
614    fn test_filter_by_platform() {
615        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
616
617        let mut ios_only = test_descriptor("ARKit");
618        ios_only.supported_platforms = vec![BuildTarget::Ios];
619        registry.install(ios_only, None).unwrap();
620
621        registry.install(test_descriptor("Universal"), None).unwrap();
622
623        assert_eq!(registry.by_platform(&BuildTarget::Ios).len(), 2);
624        assert_eq!(registry.by_platform(&BuildTarget::Android).len(), 1); // Universal only
625    }
626
627    #[test]
628    fn test_filter_by_capability() {
629        let mut registry = PluginRegistry::new(PluginVersion::new(1, 0, 0));
630
631        let mut theme_plugin = test_descriptor("DarkMode");
632        theme_plugin.capabilities = vec![PluginCapability::Theme];
633        registry.install(theme_plugin, None).unwrap();
634
635        registry.install(test_descriptor("Module"), None).unwrap();
636
637        assert_eq!(registry.by_capability(&PluginCapability::Theme).len(), 1);
638        assert_eq!(registry.by_capability(&PluginCapability::NativeModule).len(), 1);
639    }
640
641    // ── Serialization ──
642
643    #[test]
644    fn test_descriptor_serialization() {
645        let desc = test_descriptor("SerTest");
646        let json = serde_json::to_string(&desc).unwrap();
647        let restored: PluginDescriptor = serde_json::from_str(&json).unwrap();
648        assert_eq!(restored.name, "SerTest");
649        assert_eq!(restored.version, PluginVersion::new(1, 0, 0));
650        assert_eq!(restored.supported_platforms.len(), 3);
651    }
652}