llm_memory_graph/plugin/
registry.rs

1//! Plugin registry and discovery system
2//!
3//! This module provides functionality for discovering, cataloging, and
4//! managing plugin metadata in a centralized registry.
5
6use super::{Plugin, PluginError, PluginMetadata};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use tracing::{debug, info, warn};
11
12/// Plugin registry entry
13#[derive(Debug, Clone)]
14pub struct PluginRegistryEntry {
15    /// Plugin metadata
16    pub metadata: PluginMetadata,
17    /// Plugin source location (if known)
18    pub source: Option<PathBuf>,
19    /// Whether the plugin is currently loaded
20    pub loaded: bool,
21    /// When the plugin was registered
22    pub registered_at: chrono::DateTime<chrono::Utc>,
23    /// Tags for categorization
24    pub tags: Vec<String>,
25}
26
27/// Plugin registry
28///
29/// Maintains a catalog of available plugins, their metadata, and discovery information.
30/// The registry is separate from the manager - it tracks what plugins are available,
31/// while the manager tracks what plugins are actively loaded and running.
32pub struct PluginRegistry {
33    entries: HashMap<String, PluginRegistryEntry>,
34    search_paths: Vec<PathBuf>,
35}
36
37impl PluginRegistry {
38    /// Create a new plugin registry
39    pub fn new() -> Self {
40        Self {
41            entries: HashMap::new(),
42            search_paths: Vec::new(),
43        }
44    }
45
46    /// Add a search path for plugin discovery
47    pub fn add_search_path(&mut self, path: impl Into<PathBuf>) {
48        let path = path.into();
49        if !self.search_paths.contains(&path) {
50            self.search_paths.push(path);
51        }
52    }
53
54    /// Register a plugin
55    ///
56    /// Adds a plugin to the registry with its metadata and optional source location.
57    pub fn register(
58        &mut self,
59        metadata: PluginMetadata,
60        source: Option<PathBuf>,
61    ) -> Result<(), PluginError> {
62        let name = metadata.name.clone();
63
64        if self.entries.contains_key(&name) {
65            return Err(PluginError::AlreadyRegistered(name));
66        }
67
68        info!("Registering plugin in registry: {}", name);
69
70        self.entries.insert(
71            name.clone(),
72            PluginRegistryEntry {
73                metadata,
74                source,
75                loaded: false,
76                registered_at: chrono::Utc::now(),
77                tags: Vec::new(),
78            },
79        );
80
81        Ok(())
82    }
83
84    /// Register a loaded plugin
85    ///
86    /// Convenience method to register a plugin that's already loaded.
87    pub fn register_plugin(&mut self, plugin: &Arc<dyn Plugin>) -> Result<(), PluginError> {
88        let metadata = plugin.metadata().clone();
89        let name = metadata.name.clone();
90
91        if self.entries.contains_key(&name) {
92            return Err(PluginError::AlreadyRegistered(name));
93        }
94
95        self.entries.insert(
96            name.clone(),
97            PluginRegistryEntry {
98                metadata,
99                source: None,
100                loaded: true,
101                registered_at: chrono::Utc::now(),
102                tags: Vec::new(),
103            },
104        );
105
106        Ok(())
107    }
108
109    /// Mark a plugin as loaded
110    pub fn mark_loaded(&mut self, name: &str) -> Result<(), PluginError> {
111        let entry = self
112            .entries
113            .get_mut(name)
114            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
115
116        entry.loaded = true;
117        Ok(())
118    }
119
120    /// Mark a plugin as unloaded
121    pub fn mark_unloaded(&mut self, name: &str) -> Result<(), PluginError> {
122        let entry = self
123            .entries
124            .get_mut(name)
125            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
126
127        entry.loaded = false;
128        Ok(())
129    }
130
131    /// Unregister a plugin
132    pub fn unregister(&mut self, name: &str) -> Result<(), PluginError> {
133        self.entries
134            .remove(name)
135            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
136
137        info!("Unregistered plugin from registry: {}", name);
138        Ok(())
139    }
140
141    /// Get a plugin entry
142    pub fn get(&self, name: &str) -> Option<&PluginRegistryEntry> {
143        self.entries.get(name)
144    }
145
146    /// Check if a plugin is registered
147    pub fn contains(&self, name: &str) -> bool {
148        self.entries.contains_key(name)
149    }
150
151    /// List all registered plugins
152    pub fn list_all(&self) -> Vec<&PluginRegistryEntry> {
153        self.entries.values().collect()
154    }
155
156    /// List loaded plugins
157    pub fn list_loaded(&self) -> Vec<&PluginRegistryEntry> {
158        self.entries.values().filter(|entry| entry.loaded).collect()
159    }
160
161    /// List unloaded plugins
162    pub fn list_unloaded(&self) -> Vec<&PluginRegistryEntry> {
163        self.entries
164            .values()
165            .filter(|entry| !entry.loaded)
166            .collect()
167    }
168
169    /// Find plugins by capability
170    pub fn find_by_capability(&self, capability: &str) -> Vec<&PluginRegistryEntry> {
171        self.entries
172            .values()
173            .filter(|entry| {
174                entry
175                    .metadata
176                    .capabilities
177                    .contains(&capability.to_string())
178            })
179            .collect()
180    }
181
182    /// Find plugins by tag
183    pub fn find_by_tag(&self, tag: &str) -> Vec<&PluginRegistryEntry> {
184        self.entries
185            .values()
186            .filter(|entry| entry.tags.contains(&tag.to_string()))
187            .collect()
188    }
189
190    /// Add a tag to a plugin
191    pub fn add_tag(&mut self, name: &str, tag: impl Into<String>) -> Result<(), PluginError> {
192        let entry = self
193            .entries
194            .get_mut(name)
195            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
196
197        let tag = tag.into();
198        if !entry.tags.contains(&tag) {
199            entry.tags.push(tag);
200        }
201
202        Ok(())
203    }
204
205    /// Remove a tag from a plugin
206    pub fn remove_tag(&mut self, name: &str, tag: &str) -> Result<(), PluginError> {
207        let entry = self
208            .entries
209            .get_mut(name)
210            .ok_or_else(|| PluginError::NotFound(name.to_string()))?;
211
212        entry.tags.retain(|t| t != tag);
213        Ok(())
214    }
215
216    /// Get registry statistics
217    pub fn stats(&self) -> PluginRegistryStats {
218        let total = self.entries.len();
219        let loaded = self.list_loaded().len();
220        let unloaded = self.list_unloaded().len();
221
222        let mut capabilities = HashMap::new();
223        for entry in self.entries.values() {
224            for capability in &entry.metadata.capabilities {
225                *capabilities.entry(capability.clone()).or_insert(0) += 1;
226            }
227        }
228
229        PluginRegistryStats {
230            total_plugins: total,
231            loaded_plugins: loaded,
232            unloaded_plugins: unloaded,
233            capabilities,
234        }
235    }
236
237    /// Clear the registry
238    pub fn clear(&mut self) {
239        self.entries.clear();
240    }
241}
242
243impl Default for PluginRegistry {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249/// Plugin registry statistics
250#[derive(Debug, Clone)]
251pub struct PluginRegistryStats {
252    /// Total number of registered plugins
253    pub total_plugins: usize,
254    /// Number of loaded plugins
255    pub loaded_plugins: usize,
256    /// Number of unloaded plugins
257    pub unloaded_plugins: usize,
258    /// Capabilities and their counts
259    pub capabilities: HashMap<String, usize>,
260}
261
262/// Plugin discovery
263///
264/// Provides functionality for discovering plugins from various sources.
265pub struct PluginDiscovery {
266    search_paths: Vec<PathBuf>,
267}
268
269impl PluginDiscovery {
270    /// Create a new plugin discovery instance
271    pub fn new() -> Self {
272        Self {
273            search_paths: Vec::new(),
274        }
275    }
276
277    /// Add a search path
278    pub fn add_path(&mut self, path: impl Into<PathBuf>) {
279        self.search_paths.push(path.into());
280    }
281
282    /// Discover plugins in all search paths
283    ///
284    /// This is a placeholder for future dynamic plugin loading.
285    /// Currently returns an empty list as plugins must be compiled in.
286    pub fn discover(&self) -> Vec<PluginMetadata> {
287        let mut discovered = Vec::new();
288
289        for path in &self.search_paths {
290            if let Ok(entries) = self.discover_in_path(path) {
291                discovered.extend(entries);
292            }
293        }
294
295        discovered
296    }
297
298    /// Discover plugins in a specific path
299    fn discover_in_path(&self, path: &Path) -> Result<Vec<PluginMetadata>, PluginError> {
300        debug!("Searching for plugins in: {}", path.display());
301
302        if !path.exists() {
303            warn!("Plugin search path does not exist: {}", path.display());
304            return Ok(Vec::new());
305        }
306
307        // TODO: Implement actual plugin discovery
308        // This would involve:
309        // 1. Scanning directories for plugin libraries
310        // 2. Reading plugin metadata files
311        // 3. Validating plugin signatures
312        // 4. Loading plugin manifests
313        warn!("Plugin discovery not yet implemented");
314
315        Ok(Vec::new())
316    }
317
318    /// Scan for plugin metadata files
319    ///
320    /// Looks for plugin.json or plugin.toml files in the search paths.
321    pub fn scan_metadata_files(&self) -> Vec<PathBuf> {
322        let mut metadata_files = Vec::new();
323
324        for path in &self.search_paths {
325            if let Ok(entries) = std::fs::read_dir(path) {
326                for entry in entries.flatten() {
327                    let path = entry.path();
328                    if path.is_file() {
329                        if let Some(filename) = path.file_name() {
330                            let filename = filename.to_string_lossy();
331                            if filename == "plugin.json" || filename == "plugin.toml" {
332                                metadata_files.push(path);
333                            }
334                        }
335                    }
336                }
337            }
338        }
339
340        metadata_files
341    }
342
343    /// Load plugin metadata from a file
344    ///
345    /// Placeholder for loading plugin metadata from JSON or TOML files.
346    pub fn load_metadata_from_file(&self, _path: &Path) -> Result<PluginMetadata, PluginError> {
347        // TODO: Implement metadata file parsing
348        Err(PluginError::General(
349            "Metadata file loading not yet implemented".to_string(),
350        ))
351    }
352}
353
354impl Default for PluginDiscovery {
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360/// Plugin catalog
361///
362/// A read-only view of available plugins with filtering and search capabilities.
363pub struct PluginCatalog {
364    entries: Vec<PluginRegistryEntry>,
365}
366
367impl PluginCatalog {
368    /// Create a catalog from a registry
369    pub fn from_registry(registry: &PluginRegistry) -> Self {
370        Self {
371            entries: registry.list_all().into_iter().cloned().collect(),
372        }
373    }
374
375    /// Get all entries
376    pub fn entries(&self) -> &[PluginRegistryEntry] {
377        &self.entries
378    }
379
380    /// Filter by capability
381    pub fn with_capability(self, capability: &str) -> Self {
382        let entries = self
383            .entries
384            .into_iter()
385            .filter(|entry| {
386                entry
387                    .metadata
388                    .capabilities
389                    .contains(&capability.to_string())
390            })
391            .collect();
392
393        Self { entries }
394    }
395
396    /// Filter by loaded status
397    pub fn loaded(self, loaded: bool) -> Self {
398        let entries = self
399            .entries
400            .into_iter()
401            .filter(|entry| entry.loaded == loaded)
402            .collect();
403
404        Self { entries }
405    }
406
407    /// Filter by tag
408    pub fn with_tag(self, tag: &str) -> Self {
409        let entries = self
410            .entries
411            .into_iter()
412            .filter(|entry| entry.tags.contains(&tag.to_string()))
413            .collect();
414
415        Self { entries }
416    }
417
418    /// Sort by name
419    pub fn sort_by_name(mut self) -> Self {
420        self.entries
421            .sort_by(|a, b| a.metadata.name.cmp(&b.metadata.name));
422        self
423    }
424
425    /// Sort by registration time
426    pub fn sort_by_time(mut self) -> Self {
427        self.entries
428            .sort_by(|a, b| a.registered_at.cmp(&b.registered_at));
429        self
430    }
431
432    /// Get the count
433    pub fn count(&self) -> usize {
434        self.entries.len()
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use crate::plugin::PluginBuilder;
442
443    #[test]
444    fn test_plugin_registry() {
445        let mut registry = PluginRegistry::new();
446        let metadata = PluginBuilder::new("test_plugin", "1.0.0")
447            .author("Test")
448            .description("Test plugin")
449            .capability("validation")
450            .build();
451
452        assert!(registry.register(metadata.clone(), None).is_ok());
453        assert!(registry.contains("test_plugin"));
454        assert_eq!(registry.list_all().len(), 1);
455    }
456
457    #[test]
458    fn test_plugin_registry_loaded_status() {
459        let mut registry = PluginRegistry::new();
460        let metadata = PluginBuilder::new("test_plugin", "1.0.0")
461            .author("Test")
462            .description("Test plugin")
463            .build();
464
465        registry.register(metadata, None).unwrap();
466        assert_eq!(registry.list_loaded().len(), 0);
467        assert_eq!(registry.list_unloaded().len(), 1);
468
469        registry.mark_loaded("test_plugin").unwrap();
470        assert_eq!(registry.list_loaded().len(), 1);
471        assert_eq!(registry.list_unloaded().len(), 0);
472    }
473
474    #[test]
475    fn test_plugin_registry_capabilities() {
476        let mut registry = PluginRegistry::new();
477        let metadata1 = PluginBuilder::new("plugin1", "1.0.0")
478            .capability("validation")
479            .build();
480        let metadata2 = PluginBuilder::new("plugin2", "1.0.0")
481            .capability("validation")
482            .capability("enrichment")
483            .build();
484
485        registry.register(metadata1, None).unwrap();
486        registry.register(metadata2, None).unwrap();
487
488        let validation_plugins = registry.find_by_capability("validation");
489        assert_eq!(validation_plugins.len(), 2);
490
491        let enrichment_plugins = registry.find_by_capability("enrichment");
492        assert_eq!(enrichment_plugins.len(), 1);
493    }
494
495    #[test]
496    fn test_plugin_catalog() {
497        let mut registry = PluginRegistry::new();
498        let metadata1 = PluginBuilder::new("plugin1", "1.0.0")
499            .capability("validation")
500            .build();
501        let metadata2 = PluginBuilder::new("plugin2", "1.0.0")
502            .capability("enrichment")
503            .build();
504
505        registry.register(metadata1, None).unwrap();
506        registry.register(metadata2, None).unwrap();
507        registry.mark_loaded("plugin1").unwrap();
508
509        let catalog = PluginCatalog::from_registry(&registry);
510        assert_eq!(catalog.count(), 2);
511
512        let loaded_catalog = catalog.loaded(true);
513        assert_eq!(loaded_catalog.count(), 1);
514    }
515}