use super::{Plugin, PluginError, PluginMetadata};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct PluginRegistryEntry {
pub metadata: PluginMetadata,
pub source: Option<PathBuf>,
pub loaded: bool,
pub registered_at: chrono::DateTime<chrono::Utc>,
pub tags: Vec<String>,
}
pub struct PluginRegistry {
entries: HashMap<String, PluginRegistryEntry>,
search_paths: Vec<PathBuf>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
search_paths: Vec::new(),
}
}
pub fn add_search_path(&mut self, path: impl Into<PathBuf>) {
let path = path.into();
if !self.search_paths.contains(&path) {
self.search_paths.push(path);
}
}
pub fn register(
&mut self,
metadata: PluginMetadata,
source: Option<PathBuf>,
) -> Result<(), PluginError> {
let name = metadata.name.clone();
if self.entries.contains_key(&name) {
return Err(PluginError::AlreadyRegistered(name));
}
info!("Registering plugin in registry: {}", name);
self.entries.insert(
name.clone(),
PluginRegistryEntry {
metadata,
source,
loaded: false,
registered_at: chrono::Utc::now(),
tags: Vec::new(),
},
);
Ok(())
}
pub fn register_plugin(&mut self, plugin: &Arc<dyn Plugin>) -> Result<(), PluginError> {
let metadata = plugin.metadata().clone();
let name = metadata.name.clone();
if self.entries.contains_key(&name) {
return Err(PluginError::AlreadyRegistered(name));
}
self.entries.insert(
name.clone(),
PluginRegistryEntry {
metadata,
source: None,
loaded: true,
registered_at: chrono::Utc::now(),
tags: Vec::new(),
},
);
Ok(())
}
pub fn mark_loaded(&mut self, name: &str) -> Result<(), PluginError> {
let entry = self
.entries
.get_mut(name)
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
entry.loaded = true;
Ok(())
}
pub fn mark_unloaded(&mut self, name: &str) -> Result<(), PluginError> {
let entry = self
.entries
.get_mut(name)
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
entry.loaded = false;
Ok(())
}
pub fn unregister(&mut self, name: &str) -> Result<(), PluginError> {
self.entries
.remove(name)
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
info!("Unregistered plugin from registry: {}", name);
Ok(())
}
pub fn get(&self, name: &str) -> Option<&PluginRegistryEntry> {
self.entries.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
pub fn list_all(&self) -> Vec<&PluginRegistryEntry> {
self.entries.values().collect()
}
pub fn list_loaded(&self) -> Vec<&PluginRegistryEntry> {
self.entries.values().filter(|entry| entry.loaded).collect()
}
pub fn list_unloaded(&self) -> Vec<&PluginRegistryEntry> {
self.entries
.values()
.filter(|entry| !entry.loaded)
.collect()
}
pub fn find_by_capability(&self, capability: &str) -> Vec<&PluginRegistryEntry> {
self.entries
.values()
.filter(|entry| {
entry
.metadata
.capabilities
.contains(&capability.to_string())
})
.collect()
}
pub fn find_by_tag(&self, tag: &str) -> Vec<&PluginRegistryEntry> {
self.entries
.values()
.filter(|entry| entry.tags.contains(&tag.to_string()))
.collect()
}
pub fn add_tag(&mut self, name: &str, tag: impl Into<String>) -> Result<(), PluginError> {
let entry = self
.entries
.get_mut(name)
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
let tag = tag.into();
if !entry.tags.contains(&tag) {
entry.tags.push(tag);
}
Ok(())
}
pub fn remove_tag(&mut self, name: &str, tag: &str) -> Result<(), PluginError> {
let entry = self
.entries
.get_mut(name)
.ok_or_else(|| PluginError::NotFound(name.to_string()))?;
entry.tags.retain(|t| t != tag);
Ok(())
}
pub fn stats(&self) -> PluginRegistryStats {
let total = self.entries.len();
let loaded = self.list_loaded().len();
let unloaded = self.list_unloaded().len();
let mut capabilities = HashMap::new();
for entry in self.entries.values() {
for capability in &entry.metadata.capabilities {
*capabilities.entry(capability.clone()).or_insert(0) += 1;
}
}
PluginRegistryStats {
total_plugins: total,
loaded_plugins: loaded,
unloaded_plugins: unloaded,
capabilities,
}
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct PluginRegistryStats {
pub total_plugins: usize,
pub loaded_plugins: usize,
pub unloaded_plugins: usize,
pub capabilities: HashMap<String, usize>,
}
pub struct PluginDiscovery {
search_paths: Vec<PathBuf>,
}
impl PluginDiscovery {
pub fn new() -> Self {
Self {
search_paths: Vec::new(),
}
}
pub fn add_path(&mut self, path: impl Into<PathBuf>) {
self.search_paths.push(path.into());
}
pub fn discover(&self) -> Vec<PluginMetadata> {
let mut discovered = Vec::new();
for path in &self.search_paths {
if let Ok(entries) = self.discover_in_path(path) {
discovered.extend(entries);
}
}
discovered
}
fn discover_in_path(&self, path: &Path) -> Result<Vec<PluginMetadata>, PluginError> {
debug!("Searching for plugins in: {}", path.display());
if !path.exists() {
warn!("Plugin search path does not exist: {}", path.display());
return Ok(Vec::new());
}
warn!("Plugin discovery not yet implemented");
Ok(Vec::new())
}
pub fn scan_metadata_files(&self) -> Vec<PathBuf> {
let mut metadata_files = Vec::new();
for path in &self.search_paths {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(filename) = path.file_name() {
let filename = filename.to_string_lossy();
if filename == "plugin.json" || filename == "plugin.toml" {
metadata_files.push(path);
}
}
}
}
}
}
metadata_files
}
pub fn load_metadata_from_file(&self, _path: &Path) -> Result<PluginMetadata, PluginError> {
Err(PluginError::General(
"Metadata file loading not yet implemented".to_string(),
))
}
}
impl Default for PluginDiscovery {
fn default() -> Self {
Self::new()
}
}
pub struct PluginCatalog {
entries: Vec<PluginRegistryEntry>,
}
impl PluginCatalog {
pub fn from_registry(registry: &PluginRegistry) -> Self {
Self {
entries: registry.list_all().into_iter().cloned().collect(),
}
}
pub fn entries(&self) -> &[PluginRegistryEntry] {
&self.entries
}
pub fn with_capability(self, capability: &str) -> Self {
let entries = self
.entries
.into_iter()
.filter(|entry| {
entry
.metadata
.capabilities
.contains(&capability.to_string())
})
.collect();
Self { entries }
}
pub fn loaded(self, loaded: bool) -> Self {
let entries = self
.entries
.into_iter()
.filter(|entry| entry.loaded == loaded)
.collect();
Self { entries }
}
pub fn with_tag(self, tag: &str) -> Self {
let entries = self
.entries
.into_iter()
.filter(|entry| entry.tags.contains(&tag.to_string()))
.collect();
Self { entries }
}
pub fn sort_by_name(mut self) -> Self {
self.entries
.sort_by(|a, b| a.metadata.name.cmp(&b.metadata.name));
self
}
pub fn sort_by_time(mut self) -> Self {
self.entries
.sort_by(|a, b| a.registered_at.cmp(&b.registered_at));
self
}
pub fn count(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::PluginBuilder;
#[test]
fn test_plugin_registry() {
let mut registry = PluginRegistry::new();
let metadata = PluginBuilder::new("test_plugin", "1.0.0")
.author("Test")
.description("Test plugin")
.capability("validation")
.build();
assert!(registry.register(metadata.clone(), None).is_ok());
assert!(registry.contains("test_plugin"));
assert_eq!(registry.list_all().len(), 1);
}
#[test]
fn test_plugin_registry_loaded_status() {
let mut registry = PluginRegistry::new();
let metadata = PluginBuilder::new("test_plugin", "1.0.0")
.author("Test")
.description("Test plugin")
.build();
registry.register(metadata, None).unwrap();
assert_eq!(registry.list_loaded().len(), 0);
assert_eq!(registry.list_unloaded().len(), 1);
registry.mark_loaded("test_plugin").unwrap();
assert_eq!(registry.list_loaded().len(), 1);
assert_eq!(registry.list_unloaded().len(), 0);
}
#[test]
fn test_plugin_registry_capabilities() {
let mut registry = PluginRegistry::new();
let metadata1 = PluginBuilder::new("plugin1", "1.0.0")
.capability("validation")
.build();
let metadata2 = PluginBuilder::new("plugin2", "1.0.0")
.capability("validation")
.capability("enrichment")
.build();
registry.register(metadata1, None).unwrap();
registry.register(metadata2, None).unwrap();
let validation_plugins = registry.find_by_capability("validation");
assert_eq!(validation_plugins.len(), 2);
let enrichment_plugins = registry.find_by_capability("enrichment");
assert_eq!(enrichment_plugins.len(), 1);
}
#[test]
fn test_plugin_catalog() {
let mut registry = PluginRegistry::new();
let metadata1 = PluginBuilder::new("plugin1", "1.0.0")
.capability("validation")
.build();
let metadata2 = PluginBuilder::new("plugin2", "1.0.0")
.capability("enrichment")
.build();
registry.register(metadata1, None).unwrap();
registry.register(metadata2, None).unwrap();
registry.mark_loaded("plugin1").unwrap();
let catalog = PluginCatalog::from_registry(®istry);
assert_eq!(catalog.count(), 2);
let loaded_catalog = catalog.loaded(true);
assert_eq!(loaded_catalog.count(), 1);
}
}