Skip to main content

mofa_plugins/hot_reload/
registry.rs

1//! Plugin registry
2//!
3//! Manages plugin registration, versioning, and metadata
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{debug, info};
11
12use super::state::PluginState;
13
14/// Plugin version information
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct PluginVersion {
17    /// Major version
18    pub major: u32,
19    /// Minor version
20    pub minor: u32,
21    /// Patch version
22    pub patch: u32,
23    /// Pre-release tag (e.g., "alpha", "beta")
24    pub prerelease: Option<String>,
25    /// Build metadata
26    pub build: Option<String>,
27}
28
29impl PluginVersion {
30    /// Create a new version
31    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
32        Self {
33            major,
34            minor,
35            patch,
36            prerelease: None,
37            build: None,
38        }
39    }
40
41    /// Parse from string (e.g., "1.2.3-alpha+build123")
42    pub fn parse(version: &str) -> Result<Self, String> {
43        let version = version.trim();
44
45        // Split build metadata
46        let (version_pre, build) = if let Some(idx) = version.find('+') {
47            (&version[..idx], Some(version[idx + 1..].to_string()))
48        } else {
49            (version, None)
50        };
51
52        // Split prerelease
53        let (version_core, prerelease) = if let Some(idx) = version_pre.find('-') {
54            (
55                &version_pre[..idx],
56                Some(version_pre[idx + 1..].to_string()),
57            )
58        } else {
59            (version_pre, None)
60        };
61
62        // Parse core version
63        let parts: Vec<&str> = version_core.split('.').collect();
64        if parts.len() < 2 || parts.len() > 3 {
65            return Err(format!("Invalid version format: {}", version));
66        }
67
68        let major = parts[0]
69            .parse::<u32>()
70            .map_err(|_| format!("Invalid major version: {}", parts[0]))?;
71        let minor = parts[1]
72            .parse::<u32>()
73            .map_err(|_| format!("Invalid minor version: {}", parts[1]))?;
74        let patch = if parts.len() > 2 {
75            parts[2]
76                .parse::<u32>()
77                .map_err(|_| format!("Invalid patch version: {}", parts[2]))?
78        } else {
79            0
80        };
81
82        Ok(Self {
83            major,
84            minor,
85            patch,
86            prerelease,
87            build,
88        })
89    }
90
91    /// Check if this version is compatible with another (same major version)
92    pub fn is_compatible(&self, other: &PluginVersion) -> bool {
93        self.major == other.major
94    }
95
96    /// Check if this version is newer than another
97    pub fn is_newer_than(&self, other: &PluginVersion) -> bool {
98        if self.major != other.major {
99            return self.major > other.major;
100        }
101        if self.minor != other.minor {
102            return self.minor > other.minor;
103        }
104        self.patch > other.patch
105    }
106}
107
108impl std::fmt::Display for PluginVersion {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
111        if let Some(ref pre) = self.prerelease {
112            write!(f, "-{}", pre)?;
113        }
114        if let Some(ref build) = self.build {
115            write!(f, "+{}", build)?;
116        }
117        Ok(())
118    }
119}
120
121impl Default for PluginVersion {
122    fn default() -> Self {
123        Self::new(0, 0, 0)
124    }
125}
126
127/// Plugin information stored in registry
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct PluginInfo {
130    /// Plugin ID
131    pub id: String,
132    /// Plugin name
133    pub name: String,
134    /// Plugin version
135    pub version: PluginVersion,
136    /// Plugin description
137    pub description: String,
138    /// Plugin author
139    pub author: Option<String>,
140    /// Library path
141    pub library_path: Option<PathBuf>,
142    /// Current state
143    pub state: PluginState,
144    /// Load timestamp
145    pub loaded_at: Option<u64>,
146    /// Last reload timestamp
147    pub last_reload: Option<u64>,
148    /// Reload count
149    pub reload_count: u32,
150    /// Dependencies
151    pub dependencies: Vec<String>,
152    /// Capabilities/features
153    pub capabilities: Vec<String>,
154    /// Custom metadata
155    pub metadata: HashMap<String, String>,
156    /// File hash (for change detection)
157    pub file_hash: Option<String>,
158}
159
160impl PluginInfo {
161    /// Create new plugin info
162    pub fn new(id: &str, name: &str, version: PluginVersion) -> Self {
163        Self {
164            id: id.to_string(),
165            name: name.to_string(),
166            version,
167            description: String::new(),
168            author: None,
169            library_path: None,
170            state: PluginState::Unloaded,
171            loaded_at: None,
172            last_reload: None,
173            reload_count: 0,
174            dependencies: Vec::new(),
175            capabilities: Vec::new(),
176            metadata: HashMap::new(),
177            file_hash: None,
178        }
179    }
180
181    /// Set description
182    pub fn with_description(mut self, desc: &str) -> Self {
183        self.description = desc.to_string();
184        self
185    }
186
187    /// Set author
188    pub fn with_author(mut self, author: &str) -> Self {
189        self.author = Some(author.to_string());
190        self
191    }
192
193    /// Set library path
194    pub fn with_library_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
195        self.library_path = Some(path.as_ref().to_path_buf());
196        self
197    }
198
199    /// Add dependency
200    pub fn with_dependency(mut self, dep: &str) -> Self {
201        self.dependencies.push(dep.to_string());
202        self
203    }
204
205    /// Add capability
206    pub fn with_capability(mut self, cap: &str) -> Self {
207        self.capabilities.push(cap.to_string());
208        self
209    }
210
211    /// Set metadata
212    pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
213        self.metadata.insert(key.to_string(), value.to_string());
214        self
215    }
216
217    /// Mark as loaded
218    pub fn mark_loaded(&mut self) {
219        self.state = PluginState::Loaded;
220        self.loaded_at = Some(
221            std::time::SystemTime::now()
222                .duration_since(std::time::UNIX_EPOCH)
223                .unwrap_or_default()
224                .as_secs(),
225        );
226    }
227
228    /// Mark as reloaded
229    pub fn mark_reloaded(&mut self) {
230        self.state = PluginState::Loaded;
231        self.last_reload = Some(
232            std::time::SystemTime::now()
233                .duration_since(std::time::UNIX_EPOCH)
234                .unwrap_or_default()
235                .as_secs(),
236        );
237        self.reload_count += 1;
238    }
239
240    /// Check if dependencies are satisfied
241    pub fn check_dependencies(&self, available: &[String]) -> Vec<String> {
242        self.dependencies
243            .iter()
244            .filter(|dep| !available.contains(dep))
245            .cloned()
246            .collect()
247    }
248
249    /// Check if has capability
250    pub fn has_capability(&self, cap: &str) -> bool {
251        self.capabilities.iter().any(|c| c == cap)
252    }
253}
254
255/// Plugin registry for managing loaded plugins
256pub struct PluginRegistry {
257    /// Registered plugins
258    plugins: Arc<RwLock<HashMap<String, PluginInfo>>>,
259    /// Plugin path to ID mapping
260    path_to_id: Arc<RwLock<HashMap<PathBuf, String>>>,
261    /// Enable auto-registration
262    auto_register: bool,
263}
264
265impl PluginRegistry {
266    /// Create a new registry
267    pub fn new() -> Self {
268        Self {
269            plugins: Arc::new(RwLock::new(HashMap::new())),
270            path_to_id: Arc::new(RwLock::new(HashMap::new())),
271            auto_register: true,
272        }
273    }
274
275    /// Set auto-registration mode
276    pub fn with_auto_register(mut self, enabled: bool) -> Self {
277        self.auto_register = enabled;
278        self
279    }
280
281    /// Register a plugin
282    pub async fn register(&self, info: PluginInfo) -> Result<(), String> {
283        let plugin_id = info.id.clone();
284
285        info!("Registering plugin: {} v{}", info.name, info.version);
286
287        let mut plugins = self.plugins.write().await;
288
289        if plugins.contains_key(&plugin_id) {
290            return Err(format!("Plugin {} already registered", plugin_id));
291        }
292
293        // Update path mapping
294        if let Some(ref path) = info.library_path {
295            let mut path_map = self.path_to_id.write().await;
296            path_map.insert(path.clone(), plugin_id.clone());
297        }
298
299        plugins.insert(plugin_id, info);
300        Ok(())
301    }
302
303    /// Update a plugin registration
304    pub async fn update(&self, info: PluginInfo) -> Result<(), String> {
305        let plugin_id = info.id.clone();
306
307        debug!("Updating plugin registration: {}", plugin_id);
308
309        let mut plugins = self.plugins.write().await;
310
311        if !plugins.contains_key(&plugin_id) {
312            return Err(format!("Plugin {} not registered", plugin_id));
313        }
314
315        // Update path mapping if changed
316        if let Some(ref path) = info.library_path {
317            let mut path_map = self.path_to_id.write().await;
318            path_map.insert(path.clone(), plugin_id.clone());
319        }
320
321        plugins.insert(plugin_id, info);
322        Ok(())
323    }
324
325    /// Unregister a plugin
326    pub async fn unregister(&self, plugin_id: &str) -> Result<PluginInfo, String> {
327        info!("Unregistering plugin: {}", plugin_id);
328
329        let mut plugins = self.plugins.write().await;
330
331        let info = plugins
332            .remove(plugin_id)
333            .ok_or_else(|| format!("Plugin {} not found", plugin_id))?;
334
335        // Remove path mapping
336        if let Some(ref path) = info.library_path {
337            let mut path_map = self.path_to_id.write().await;
338            path_map.remove(path);
339        }
340
341        Ok(info)
342    }
343
344    /// Get plugin info
345    pub async fn get(&self, plugin_id: &str) -> Option<PluginInfo> {
346        let plugins = self.plugins.read().await;
347        plugins.get(plugin_id).cloned()
348    }
349
350    /// Get plugin by path
351    pub async fn get_by_path<P: AsRef<std::path::Path>>(&self, path: P) -> Option<PluginInfo> {
352        let path = path.as_ref().to_path_buf();
353
354        let path_map = self.path_to_id.read().await;
355        if let Some(plugin_id) = path_map.get(&path) {
356            let plugins = self.plugins.read().await;
357            return plugins.get(plugin_id).cloned();
358        }
359
360        None
361    }
362
363    /// Check if a plugin is registered
364    pub async fn contains(&self, plugin_id: &str) -> bool {
365        let plugins = self.plugins.read().await;
366        plugins.contains_key(plugin_id)
367    }
368
369    /// List all registered plugins
370    pub async fn list(&self) -> Vec<PluginInfo> {
371        let plugins = self.plugins.read().await;
372        plugins.values().cloned().collect()
373    }
374
375    /// List plugin IDs
376    pub async fn plugin_ids(&self) -> Vec<String> {
377        let plugins = self.plugins.read().await;
378        plugins.keys().cloned().collect()
379    }
380
381    /// Find plugins by capability
382    pub async fn find_by_capability(&self, capability: &str) -> Vec<PluginInfo> {
383        let plugins = self.plugins.read().await;
384        plugins
385            .values()
386            .filter(|p| p.has_capability(capability))
387            .cloned()
388            .collect()
389    }
390
391    /// Find plugins by state
392    pub async fn find_by_state(&self, state: PluginState) -> Vec<PluginInfo> {
393        let plugins = self.plugins.read().await;
394        plugins
395            .values()
396            .filter(|p| p.state == state)
397            .cloned()
398            .collect()
399    }
400
401    /// Update plugin state
402    pub async fn set_state(&self, plugin_id: &str, state: PluginState) -> Result<(), String> {
403        let mut plugins = self.plugins.write().await;
404
405        if let Some(info) = plugins.get_mut(plugin_id) {
406            debug!(
407                "Updating plugin {} state: {:?} -> {:?}",
408                plugin_id, info.state, state
409            );
410            info.state = state;
411            Ok(())
412        } else {
413            Err(format!("Plugin {} not found", plugin_id))
414        }
415    }
416
417    /// Get dependency order for loading
418    pub async fn get_load_order(&self) -> Result<Vec<String>, String> {
419        let plugins = self.plugins.read().await;
420
421        // Build dependency graph
422        let mut in_degree: HashMap<String, usize> = HashMap::new();
423        let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
424
425        for (id, info) in plugins.iter() {
426            in_degree.entry(id.clone()).or_insert(0);
427
428            for dep in &info.dependencies {
429                dependents.entry(dep.clone()).or_default().push(id.clone());
430                *in_degree.entry(id.clone()).or_insert(0) += 1;
431            }
432        }
433
434        // Topological sort (Kahn's algorithm)
435        let mut result = Vec::new();
436        let mut queue: Vec<String> = in_degree
437            .iter()
438            .filter(|(_, deg)| deg == &&0)
439            .map(|(id, _)| id.clone())
440            .collect();
441
442        while let Some(id) = queue.pop() {
443            result.push(id.clone());
444
445            if let Some(deps) = dependents.get(&id) {
446                for dep in deps {
447                    if let Some(deg) = in_degree.get_mut(dep) {
448                        *deg -= 1;
449                        if *deg == 0 {
450                            queue.push(dep.clone());
451                        }
452                    }
453                }
454            }
455        }
456
457        if result.len() != plugins.len() {
458            return Err("Circular dependency detected".to_string());
459        }
460
461        Ok(result)
462    }
463
464    /// Clear all registrations
465    pub async fn clear(&self) {
466        let mut plugins = self.plugins.write().await;
467        plugins.clear();
468
469        let mut path_map = self.path_to_id.write().await;
470        path_map.clear();
471    }
472
473    /// Get registry statistics
474    pub async fn stats(&self) -> RegistryStats {
475        let plugins = self.plugins.read().await;
476
477        let mut stats = RegistryStats::default();
478        stats.total_plugins = plugins.len();
479
480        for info in plugins.values() {
481            match info.state {
482                PluginState::Loaded | PluginState::Running => stats.loaded_plugins += 1,
483                PluginState::Failed(_) => stats.failed_plugins += 1,
484                _ => {}
485            }
486            stats.total_reloads += info.reload_count as usize;
487        }
488
489        stats
490    }
491}
492
493impl Default for PluginRegistry {
494    fn default() -> Self {
495        Self::new()
496    }
497}
498
499/// Registry statistics
500#[derive(Debug, Clone, Default)]
501pub struct RegistryStats {
502    /// Total registered plugins
503    pub total_plugins: usize,
504    /// Currently loaded plugins
505    pub loaded_plugins: usize,
506    /// Failed plugins
507    pub failed_plugins: usize,
508    /// Total reload count
509    pub total_reloads: usize,
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn test_version_parse() {
518        let v = PluginVersion::parse("1.2.3").unwrap();
519        assert_eq!(v.major, 1);
520        assert_eq!(v.minor, 2);
521        assert_eq!(v.patch, 3);
522
523        let v = PluginVersion::parse("1.2.3-alpha").unwrap();
524        assert_eq!(v.prerelease, Some("alpha".to_string()));
525
526        let v = PluginVersion::parse("1.2.3-beta+build123").unwrap();
527        assert_eq!(v.prerelease, Some("beta".to_string()));
528        assert_eq!(v.build, Some("build123".to_string()));
529    }
530
531    #[test]
532    fn test_version_comparison() {
533        let v1 = PluginVersion::new(1, 0, 0);
534        let v2 = PluginVersion::new(1, 1, 0);
535        let v3 = PluginVersion::new(2, 0, 0);
536
537        assert!(v1.is_compatible(&v2));
538        assert!(!v1.is_compatible(&v3));
539        assert!(v2.is_newer_than(&v1));
540        assert!(v3.is_newer_than(&v2));
541    }
542
543    #[test]
544    fn test_version_display() {
545        let v = PluginVersion::new(1, 2, 3);
546        assert_eq!(v.to_string(), "1.2.3");
547
548        let mut v = PluginVersion::new(1, 0, 0);
549        v.prerelease = Some("alpha".to_string());
550        assert_eq!(v.to_string(), "1.0.0-alpha");
551    }
552
553    #[test]
554    fn test_plugin_info() {
555        let info = PluginInfo::new("test", "Test Plugin", PluginVersion::new(1, 0, 0))
556            .with_description("A test plugin")
557            .with_author("Developer")
558            .with_capability("feature_a")
559            .with_capability("feature_b");
560
561        assert_eq!(info.id, "test");
562        assert!(info.has_capability("feature_a"));
563        assert!(!info.has_capability("feature_c"));
564    }
565
566    #[tokio::test]
567    async fn test_registry() {
568        let registry = PluginRegistry::new();
569
570        let info = PluginInfo::new("plugin-1", "Plugin 1", PluginVersion::new(1, 0, 0));
571        registry.register(info).await.unwrap();
572
573        assert!(registry.contains("plugin-1").await);
574        assert!(!registry.contains("plugin-2").await);
575
576        let loaded = registry.get("plugin-1").await.unwrap();
577        assert_eq!(loaded.name, "Plugin 1");
578
579        registry.unregister("plugin-1").await.unwrap();
580        assert!(!registry.contains("plugin-1").await);
581    }
582
583    #[tokio::test]
584    async fn test_registry_load_order() {
585        let registry = PluginRegistry::new();
586
587        // Plugin A has no dependencies
588        let a = PluginInfo::new("a", "A", PluginVersion::new(1, 0, 0));
589        registry.register(a).await.unwrap();
590
591        // Plugin B depends on A
592        let b = PluginInfo::new("b", "B", PluginVersion::new(1, 0, 0)).with_dependency("a");
593        registry.register(b).await.unwrap();
594
595        // Plugin C depends on B
596        let c = PluginInfo::new("c", "C", PluginVersion::new(1, 0, 0)).with_dependency("b");
597        registry.register(c).await.unwrap();
598
599        let order = registry.get_load_order().await.unwrap();
600
601        // A should come before B, B before C
602        let pos_a = order.iter().position(|x| x == "a").unwrap();
603        let pos_b = order.iter().position(|x| x == "b").unwrap();
604        let pos_c = order.iter().position(|x| x == "c").unwrap();
605
606        assert!(pos_a < pos_b);
607        assert!(pos_b < pos_c);
608    }
609}