mockforge_plugin_loader/
registry.rs

1//! Plugin registry for managing loaded plugins
2//!
3//! This module provides the plugin registry that tracks loaded plugins,
4//! manages their lifecycle, and provides access to plugin instances.
5
6use super::*;
7use mockforge_plugin_core::{PluginHealth, PluginId, PluginInstance, PluginVersion};
8use std::collections::HashMap;
9use std::collections::HashSet;
10
11/// Plugin registry for managing loaded plugins
12pub struct PluginRegistry {
13    /// Registered plugins
14    plugins: HashMap<PluginId, PluginInstance>,
15    /// Plugin load order (for dependency resolution)
16    load_order: Vec<PluginId>,
17    /// Registry statistics
18    stats: RegistryStats,
19}
20
21// Implement Send + Sync for PluginRegistry
22unsafe impl Send for PluginRegistry {}
23unsafe impl Sync for PluginRegistry {}
24
25impl Default for PluginRegistry {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl PluginRegistry {
32    /// Create a new plugin registry
33    pub fn new() -> Self {
34        Self {
35            plugins: HashMap::new(),
36            load_order: Vec::new(),
37            stats: RegistryStats::default(),
38        }
39    }
40
41    /// Add a plugin to the registry
42    pub fn add_plugin(&mut self, plugin: PluginInstance) -> LoaderResult<()> {
43        let plugin_id = plugin.id.clone();
44
45        // Check if plugin already exists
46        if self.plugins.contains_key(&plugin_id) {
47            return Err(PluginLoaderError::already_loaded(plugin_id));
48        }
49
50        // Validate plugin dependencies
51        self.validate_dependencies(&plugin)?;
52
53        // Add plugin
54        self.plugins.insert(plugin_id.clone(), plugin);
55        self.load_order.push(plugin_id);
56
57        // Update statistics
58        self.stats.total_plugins += 1;
59        self.stats.last_updated = chrono::Utc::now();
60
61        Ok(())
62    }
63
64    /// Remove a plugin from the registry
65    pub fn remove_plugin(&mut self, plugin_id: &PluginId) -> LoaderResult<PluginInstance> {
66        // Check if plugin exists
67        if !self.plugins.contains_key(plugin_id) {
68            return Err(PluginLoaderError::not_found(plugin_id.clone()));
69        }
70
71        // Check if other plugins depend on this one
72        self.check_reverse_dependencies(plugin_id)?;
73
74        // Remove from load order
75        self.load_order.retain(|id| id != plugin_id);
76
77        // Remove plugin
78        let plugin = self.plugins.remove(plugin_id).unwrap();
79
80        // Update statistics
81        self.stats.total_plugins -= 1;
82        self.stats.last_updated = chrono::Utc::now();
83
84        Ok(plugin)
85    }
86
87    /// Get a plugin by ID
88    pub fn get_plugin(&self, plugin_id: &PluginId) -> Option<&PluginInstance> {
89        self.plugins.get(plugin_id)
90    }
91
92    /// Get a mutable reference to a plugin
93    pub fn get_plugin_mut(&mut self, plugin_id: &PluginId) -> Option<&mut PluginInstance> {
94        self.plugins.get_mut(plugin_id)
95    }
96
97    /// Check if a plugin is registered
98    pub fn has_plugin(&self, plugin_id: &PluginId) -> bool {
99        self.plugins.contains_key(plugin_id)
100    }
101
102    /// List all registered plugins
103    pub fn list_plugins(&self) -> Vec<PluginId> {
104        self.plugins.keys().cloned().collect()
105    }
106
107    /// Get plugin health status
108    pub fn get_plugin_health(&self, plugin_id: &PluginId) -> LoaderResult<PluginHealth> {
109        let plugin = self
110            .get_plugin(plugin_id)
111            .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
112
113        Ok(plugin.health.clone())
114    }
115
116    /// Get registry statistics
117    pub fn get_stats(&self) -> &RegistryStats {
118        &self.stats
119    }
120
121    /// Check if a version requirement is compatible with a plugin version
122    pub fn is_version_compatible(&self, requirement: &str, version: &PluginVersion) -> bool {
123        // Simple version compatibility check
124        // For now, just check exact match or caret ranges
125        if requirement.starts_with('^') {
126            // Caret range: ^1.0.0 matches 1.x.x
127            let req_version = requirement.strip_prefix('^').unwrap();
128            let req_parts: Vec<&str> = req_version.split('.').collect();
129            let ver_parts: Vec<u32> = vec![version.major, version.minor, version.patch];
130
131            if !req_parts.is_empty() && req_parts[0].parse::<u32>().unwrap_or(0) == ver_parts[0] {
132                return true;
133            }
134        } else {
135            // Exact match
136            return requirement == version.to_string();
137        }
138        false
139    }
140
141    /// Find plugins by capability
142    pub fn find_plugins_by_capability(&self, capability: &str) -> Vec<&PluginInstance> {
143        self.plugins
144            .values()
145            .filter(|plugin| plugin.manifest.capabilities.contains(&capability.to_string()))
146            .collect()
147    }
148
149    /// Get plugins in dependency order
150    pub fn get_plugins_in_dependency_order(&self) -> Vec<&PluginInstance> {
151        self.load_order.iter().filter_map(|id| self.plugins.get(id)).collect()
152    }
153
154    /// Validate plugin dependencies
155    fn validate_dependencies(&self, plugin: &PluginInstance) -> LoaderResult<()> {
156        for dep_id in plugin.manifest.dependencies.keys() {
157            // Check if dependency is loaded
158            if !self.has_plugin(dep_id) {
159                return Err(PluginLoaderError::dependency(format!(
160                    "Required dependency {} not found",
161                    dep_id.0
162                )));
163            }
164
165            // Check version compatibility (simplified for now)
166            if let Some(_loaded_plugin) = self.get_plugin(dep_id) {
167                // For now, just check that the loaded plugin exists
168                // Version compatibility checking can be added later
169            }
170        }
171
172        Ok(())
173    }
174
175    /// Check reverse dependencies (plugins that depend on the one being removed)
176    fn check_reverse_dependencies(&self, plugin_id: &PluginId) -> LoaderResult<()> {
177        for (id, plugin) in &self.plugins {
178            if id == plugin_id {
179                continue; // Skip the plugin being removed
180            }
181
182            if plugin.manifest.dependencies.contains_key(plugin_id) {
183                return Err(PluginLoaderError::dependency(format!(
184                    "Cannot remove plugin {}: required by plugin {}",
185                    plugin_id.0, id.0
186                )));
187            }
188        }
189
190        Ok(())
191    }
192
193    /// Get plugin dependency graph
194    pub fn get_dependency_graph(&self) -> HashMap<PluginId, Vec<PluginId>> {
195        let mut graph = HashMap::new();
196
197        for (plugin_id, plugin) in &self.plugins {
198            let mut deps = Vec::new();
199            for dep_id in plugin.manifest.dependencies.keys() {
200                if self.has_plugin(dep_id) {
201                    deps.push(dep_id.clone());
202                }
203            }
204            graph.insert(plugin_id.clone(), deps);
205        }
206
207        graph
208    }
209
210    /// Perform topological sort for plugin initialization
211    pub fn get_initialization_order(&self) -> LoaderResult<Vec<PluginId>> {
212        let graph = self.get_dependency_graph();
213        let mut visited = HashSet::new();
214        let mut visiting = HashSet::new();
215        let mut order = Vec::new();
216
217        fn visit(
218            plugin_id: &PluginId,
219            graph: &HashMap<PluginId, Vec<PluginId>>,
220            visited: &mut HashSet<PluginId>,
221            visiting: &mut HashSet<PluginId>,
222            order: &mut Vec<PluginId>,
223        ) -> LoaderResult<()> {
224            if visited.contains(plugin_id) {
225                return Ok(());
226            }
227
228            if visiting.contains(plugin_id) {
229                return Err(PluginLoaderError::dependency(format!(
230                    "Circular dependency detected involving plugin {}",
231                    plugin_id
232                )));
233            }
234
235            visiting.insert(plugin_id.clone());
236
237            if let Some(deps) = graph.get(plugin_id) {
238                for dep in deps {
239                    visit(dep, graph, visited, visiting, order)?;
240                }
241            }
242
243            visiting.remove(plugin_id);
244            visited.insert(plugin_id.clone());
245            order.push(plugin_id.clone());
246
247            Ok(())
248        }
249
250        for plugin_id in self.plugins.keys() {
251            if !visited.contains(plugin_id) {
252                visit(plugin_id, &graph, &mut visited, &mut visiting, &mut order)?;
253            }
254        }
255
256        Ok(order)
257    }
258
259    /// Clear all plugins from registry
260    pub fn clear(&mut self) {
261        self.plugins.clear();
262        self.load_order.clear();
263        self.stats = RegistryStats::default();
264    }
265
266    /// Get registry health status
267    pub fn health_status(&self) -> RegistryHealth {
268        let mut healthy_plugins = 0;
269        let mut unhealthy_plugins = 0;
270
271        for plugin in self.plugins.values() {
272            if plugin.health.healthy {
273                healthy_plugins += 1;
274            } else {
275                unhealthy_plugins += 1;
276            }
277        }
278
279        RegistryHealth {
280            total_plugins: self.plugins.len(),
281            healthy_plugins,
282            unhealthy_plugins,
283            last_updated: self.stats.last_updated,
284        }
285    }
286}
287
288/// Registry statistics
289#[derive(Debug, Clone, Default)]
290pub struct RegistryStats {
291    /// Total number of registered plugins
292    pub total_plugins: usize,
293    /// Last update timestamp
294    pub last_updated: chrono::DateTime<chrono::Utc>,
295    /// Total plugin loads
296    pub total_loads: u64,
297    /// Total plugin unloads
298    pub total_unloads: u64,
299}
300
301/// Registry health status
302#[derive(Debug, Clone)]
303pub struct RegistryHealth {
304    /// Total plugins
305    pub total_plugins: usize,
306    /// Healthy plugins
307    pub healthy_plugins: usize,
308    /// Unhealthy plugins
309    pub unhealthy_plugins: usize,
310    /// Last updated
311    pub last_updated: chrono::DateTime<chrono::Utc>,
312}
313
314impl RegistryHealth {
315    /// Check if registry is healthy
316    pub fn is_healthy(&self) -> bool {
317        self.unhealthy_plugins == 0
318    }
319
320    /// Get health percentage
321    pub fn health_percentage(&self) -> f64 {
322        if self.total_plugins == 0 {
323            100.0
324        } else {
325            (self.healthy_plugins as f64 / self.total_plugins as f64) * 100.0
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use mockforge_plugin_core::{PluginMetrics, PluginState};
334
335    #[test]
336    fn test_registry_creation() {
337        let registry = PluginRegistry::new();
338        assert_eq!(registry.list_plugins().len(), 0);
339        assert_eq!(registry.get_stats().total_plugins, 0);
340    }
341
342    #[test]
343    fn test_registry_health() {
344        let health = RegistryHealth {
345            total_plugins: 10,
346            healthy_plugins: 8,
347            unhealthy_plugins: 2,
348            last_updated: chrono::Utc::now(),
349        };
350
351        assert!(!health.is_healthy());
352        assert_eq!(health.health_percentage(), 80.0);
353    }
354
355    #[test]
356    fn test_empty_registry_health() {
357        let health = RegistryHealth {
358            total_plugins: 0,
359            healthy_plugins: 0,
360            unhealthy_plugins: 0,
361            last_updated: chrono::Utc::now(),
362        };
363
364        assert!(health.is_healthy());
365        assert_eq!(health.health_percentage(), 100.0);
366    }
367
368    #[test]
369    fn test_version_compatibility() {
370        let registry = PluginRegistry::new();
371
372        // Test exact version match
373        let v1 = PluginVersion::new(1, 0, 0);
374        assert!(registry.is_version_compatible("1.0.0", &v1));
375
376        // Test caret range (simplified)
377        assert!(registry.is_version_compatible("^1.0.0", &v1));
378
379        // Test non-match
380        assert!(!registry.is_version_compatible("2.0.0", &v1));
381    }
382
383    #[tokio::test]
384    async fn test_registry_operations() {
385        let mut registry = PluginRegistry::new();
386
387        // Create a test plugin
388        let plugin_id = PluginId::new("test-plugin");
389        let plugin_info = PluginInfo::new(
390            plugin_id.clone(),
391            PluginVersion::new(1, 0, 0),
392            "Test Plugin",
393            "A test plugin",
394            PluginAuthor::new("Test Author"),
395        );
396        let manifest = PluginManifest::new(plugin_info);
397
398        let plugin = PluginInstance {
399            id: plugin_id.clone(),
400            manifest,
401            state: PluginState::Ready,
402            health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
403        };
404
405        // Test adding plugin
406        registry.add_plugin(plugin).unwrap();
407        assert_eq!(registry.list_plugins().len(), 1);
408        assert!(registry.has_plugin(&plugin_id));
409
410        // Test getting plugin
411        assert!(registry.get_plugin(&plugin_id).is_some());
412
413        // Test removing plugin
414        let removed = registry.remove_plugin(&plugin_id).unwrap();
415        assert_eq!(removed.id, plugin_id);
416        assert_eq!(registry.list_plugins().len(), 0);
417        assert!(!registry.has_plugin(&plugin_id));
418    }
419
420    #[tokio::test]
421    async fn test_duplicate_plugin() {
422        let mut registry = PluginRegistry::new();
423
424        let plugin_id = PluginId::new("test-plugin");
425        let plugin_info = PluginInfo::new(
426            plugin_id.clone(),
427            PluginVersion::new(1, 0, 0),
428            "Test Plugin",
429            "A test plugin",
430            PluginAuthor::new("Test Author"),
431        );
432        let manifest = PluginManifest::new(plugin_info.clone());
433
434        let plugin1 = PluginInstance {
435            id: plugin_id.clone(),
436            manifest: manifest.clone(),
437            state: PluginState::Ready,
438            health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
439        };
440
441        let plugin2 = PluginInstance {
442            id: plugin_id.clone(),
443            manifest,
444            state: PluginState::Ready,
445            health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
446        };
447
448        // Add first plugin
449        registry.add_plugin(plugin1).unwrap();
450
451        // Try to add duplicate
452        let result = registry.add_plugin(plugin2);
453        assert!(result.is_err());
454        assert!(matches!(result.unwrap_err(), PluginLoaderError::AlreadyLoaded { .. }));
455    }
456}