Skip to main content

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