1use crate::modules::{NativeModule, ModuleRegistry, ModuleError};
15use crate::cloud::BuildTarget;
16use serde::{Serialize, Deserialize};
17use std::collections::HashMap;
18use std::sync::Arc;
19
20#[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#[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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum PluginCapability {
82 NativeModule,
84 Components,
86 Storage,
88 PlatformFeature(String),
90 Theme,
92 Navigation,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PluginDependency {
99 pub name: String,
100 pub min_version: PluginVersion,
101}
102
103#[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#[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 pub fn supports_platform(&self, target: &BuildTarget) -> bool {
139 self.supported_platforms.contains(target)
140 }
141
142 pub fn is_compatible_with_engine(&self, engine_version: &PluginVersion) -> bool {
144 engine_version.satisfies_min(&self.min_engine_version)
145 }
146
147 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
160pub 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
177struct InstalledPlugin {
183 descriptor: PluginDescriptor,
184 module: Option<Arc<dyn NativeModule>>,
185 enabled: bool,
186}
187
188pub 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 pub fn install(
206 &mut self,
207 descriptor: PluginDescriptor,
208 module: Option<Arc<dyn NativeModule>>,
209 ) -> PluginResult<()> {
210 validate_descriptor(&descriptor)?;
211
212 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 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 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 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 pub fn uninstall(&mut self, name: &str) -> PluginResult<()> {
258 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 if plugin.module.is_some() {
273 self.module_registry.unregister(name);
274 }
275
276 Ok(())
277 }
278
279 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 pub fn get_descriptor(&self, name: &str) -> Option<&PluginDescriptor> {
289 self.plugins.get(name).map(|p| &p.descriptor)
290 }
291
292 pub fn is_installed(&self, name: &str) -> bool {
294 self.plugins.contains_key(name)
295 }
296
297 pub fn installed_names(&self) -> Vec<String> {
299 self.plugins.keys().cloned().collect()
300 }
301
302 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 pub fn count(&self) -> usize {
312 self.plugins.len()
313 }
314
315 pub fn module_registry(&self) -> &ModuleRegistry {
317 &self.module_registry
318 }
319
320 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 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 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 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#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::modules::{MethodDescriptor, ModuleArg, ModuleResult, ModuleValue};
368
369 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 #[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 #[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 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 #[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); }
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 #[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}