use super::*;
use mockforge_plugin_core::{PluginHealth, PluginId, PluginInstance, PluginVersion};
use std::collections::HashMap;
use std::collections::HashSet;
pub struct PluginRegistry {
plugins: HashMap<PluginId, PluginInstance>,
load_order: Vec<PluginId>,
stats: RegistryStats,
}
unsafe impl Send for PluginRegistry {}
unsafe impl Sync for PluginRegistry {}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
load_order: Vec::new(),
stats: RegistryStats::default(),
}
}
pub fn add_plugin(&mut self, plugin: PluginInstance) -> LoaderResult<()> {
let plugin_id = plugin.id.clone();
if self.plugins.contains_key(&plugin_id) {
return Err(PluginLoaderError::already_loaded(plugin_id));
}
self.validate_dependencies(&plugin)?;
self.plugins.insert(plugin_id.clone(), plugin);
self.load_order.push(plugin_id);
self.stats.total_plugins += 1;
self.stats.last_updated = chrono::Utc::now();
Ok(())
}
pub fn remove_plugin(&mut self, plugin_id: &PluginId) -> LoaderResult<PluginInstance> {
if !self.plugins.contains_key(plugin_id) {
return Err(PluginLoaderError::not_found(plugin_id.clone()));
}
self.check_reverse_dependencies(plugin_id)?;
self.load_order.retain(|id| id != plugin_id);
let plugin = self.plugins.remove(plugin_id).unwrap();
self.stats.total_plugins -= 1;
self.stats.last_updated = chrono::Utc::now();
Ok(plugin)
}
pub fn get_plugin(&self, plugin_id: &PluginId) -> Option<&PluginInstance> {
self.plugins.get(plugin_id)
}
pub fn get_plugin_mut(&mut self, plugin_id: &PluginId) -> Option<&mut PluginInstance> {
self.plugins.get_mut(plugin_id)
}
pub fn has_plugin(&self, plugin_id: &PluginId) -> bool {
self.plugins.contains_key(plugin_id)
}
pub fn list_plugins(&self) -> Vec<PluginId> {
self.plugins.keys().cloned().collect()
}
pub fn get_plugin_health(&self, plugin_id: &PluginId) -> LoaderResult<PluginHealth> {
let plugin = self
.get_plugin(plugin_id)
.ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
Ok(plugin.health.clone())
}
pub fn get_stats(&self) -> &RegistryStats {
&self.stats
}
pub fn is_version_compatible(&self, requirement: &str, version: &PluginVersion) -> bool {
if requirement.starts_with('^') {
let req_version = requirement.strip_prefix('^').unwrap();
let req_parts: Vec<&str> = req_version.split('.').collect();
let ver_parts: Vec<u32> = vec![version.major, version.minor, version.patch];
if !req_parts.is_empty() && req_parts[0].parse::<u32>().unwrap_or(0) == ver_parts[0] {
return true;
}
} else {
return requirement == version.to_string();
}
false
}
pub fn find_plugins_by_capability(&self, capability: &str) -> Vec<&PluginInstance> {
self.plugins
.values()
.filter(|plugin| plugin.manifest.capabilities.contains(&capability.to_string()))
.collect()
}
pub fn get_plugins_in_dependency_order(&self) -> Vec<&PluginInstance> {
self.load_order.iter().filter_map(|id| self.plugins.get(id)).collect()
}
fn validate_dependencies(&self, plugin: &PluginInstance) -> LoaderResult<()> {
for dep_id in plugin.manifest.dependencies.keys() {
if !self.has_plugin(dep_id) {
return Err(PluginLoaderError::dependency(format!(
"Required dependency {} not found",
dep_id.0
)));
}
if let Some(_loaded_plugin) = self.get_plugin(dep_id) {
}
}
Ok(())
}
fn check_reverse_dependencies(&self, plugin_id: &PluginId) -> LoaderResult<()> {
for (id, plugin) in &self.plugins {
if id == plugin_id {
continue; }
if plugin.manifest.dependencies.contains_key(plugin_id) {
return Err(PluginLoaderError::dependency(format!(
"Cannot remove plugin {}: required by plugin {}",
plugin_id.0, id.0
)));
}
}
Ok(())
}
pub fn get_dependency_graph(&self) -> HashMap<PluginId, Vec<PluginId>> {
let mut graph = HashMap::new();
for (plugin_id, plugin) in &self.plugins {
let mut deps = Vec::new();
for dep_id in plugin.manifest.dependencies.keys() {
if self.has_plugin(dep_id) {
deps.push(dep_id.clone());
}
}
graph.insert(plugin_id.clone(), deps);
}
graph
}
pub fn get_initialization_order(&self) -> LoaderResult<Vec<PluginId>> {
let graph = self.get_dependency_graph();
let mut visited = HashSet::new();
let mut visiting = HashSet::new();
let mut order = Vec::new();
fn visit(
plugin_id: &PluginId,
graph: &HashMap<PluginId, Vec<PluginId>>,
visited: &mut HashSet<PluginId>,
visiting: &mut HashSet<PluginId>,
order: &mut Vec<PluginId>,
) -> LoaderResult<()> {
if visited.contains(plugin_id) {
return Ok(());
}
if visiting.contains(plugin_id) {
return Err(PluginLoaderError::dependency(format!(
"Circular dependency detected involving plugin {}",
plugin_id
)));
}
visiting.insert(plugin_id.clone());
if let Some(deps) = graph.get(plugin_id) {
for dep in deps {
visit(dep, graph, visited, visiting, order)?;
}
}
visiting.remove(plugin_id);
visited.insert(plugin_id.clone());
order.push(plugin_id.clone());
Ok(())
}
for plugin_id in self.plugins.keys() {
if !visited.contains(plugin_id) {
visit(plugin_id, &graph, &mut visited, &mut visiting, &mut order)?;
}
}
Ok(order)
}
pub fn clear(&mut self) {
self.plugins.clear();
self.load_order.clear();
self.stats = RegistryStats::default();
}
pub fn health_status(&self) -> RegistryHealth {
let mut healthy_plugins = 0;
let mut unhealthy_plugins = 0;
for plugin in self.plugins.values() {
if plugin.health.healthy {
healthy_plugins += 1;
} else {
unhealthy_plugins += 1;
}
}
RegistryHealth {
total_plugins: self.plugins.len(),
healthy_plugins,
unhealthy_plugins,
last_updated: self.stats.last_updated,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RegistryStats {
pub total_plugins: usize,
pub last_updated: chrono::DateTime<chrono::Utc>,
pub total_loads: u64,
pub total_unloads: u64,
}
#[derive(Debug, Clone)]
pub struct RegistryHealth {
pub total_plugins: usize,
pub healthy_plugins: usize,
pub unhealthy_plugins: usize,
pub last_updated: chrono::DateTime<chrono::Utc>,
}
impl RegistryHealth {
pub fn is_healthy(&self) -> bool {
self.unhealthy_plugins == 0
}
pub fn health_percentage(&self) -> f64 {
if self.total_plugins == 0 {
100.0
} else {
(self.healthy_plugins as f64 / self.total_plugins as f64) * 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockforge_plugin_core::{PluginMetrics, PluginState};
#[test]
fn test_registry_creation() {
let registry = PluginRegistry::new();
assert_eq!(registry.list_plugins().len(), 0);
assert_eq!(registry.get_stats().total_plugins, 0);
}
#[test]
fn test_registry_health() {
let health = RegistryHealth {
total_plugins: 10,
healthy_plugins: 8,
unhealthy_plugins: 2,
last_updated: chrono::Utc::now(),
};
assert!(!health.is_healthy());
assert_eq!(health.health_percentage(), 80.0);
}
#[test]
fn test_empty_registry_health() {
let health = RegistryHealth {
total_plugins: 0,
healthy_plugins: 0,
unhealthy_plugins: 0,
last_updated: chrono::Utc::now(),
};
assert!(health.is_healthy());
assert_eq!(health.health_percentage(), 100.0);
}
#[test]
fn test_version_compatibility() {
let registry = PluginRegistry::new();
let v1 = PluginVersion::new(1, 0, 0);
assert!(registry.is_version_compatible("1.0.0", &v1));
assert!(registry.is_version_compatible("^1.0.0", &v1));
assert!(!registry.is_version_compatible("2.0.0", &v1));
}
#[tokio::test]
async fn test_registry_operations() {
let mut registry = PluginRegistry::new();
let plugin_id = PluginId::new("test-plugin");
let plugin_info = PluginInfo::new(
plugin_id.clone(),
PluginVersion::new(1, 0, 0),
"Test Plugin",
"A test plugin",
PluginAuthor::new("Test Author"),
);
let manifest = PluginManifest::new(plugin_info);
let plugin = PluginInstance {
id: plugin_id.clone(),
manifest,
state: PluginState::Ready,
health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
};
registry.add_plugin(plugin).unwrap();
assert_eq!(registry.list_plugins().len(), 1);
assert!(registry.has_plugin(&plugin_id));
assert!(registry.get_plugin(&plugin_id).is_some());
let removed = registry.remove_plugin(&plugin_id).unwrap();
assert_eq!(removed.id, plugin_id);
assert_eq!(registry.list_plugins().len(), 0);
assert!(!registry.has_plugin(&plugin_id));
}
#[tokio::test]
async fn test_duplicate_plugin() {
let mut registry = PluginRegistry::new();
let plugin_id = PluginId::new("test-plugin");
let plugin_info = PluginInfo::new(
plugin_id.clone(),
PluginVersion::new(1, 0, 0),
"Test Plugin",
"A test plugin",
PluginAuthor::new("Test Author"),
);
let manifest = PluginManifest::new(plugin_info.clone());
let plugin1 = PluginInstance {
id: plugin_id.clone(),
manifest: manifest.clone(),
state: PluginState::Ready,
health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
};
let plugin2 = PluginInstance {
id: plugin_id.clone(),
manifest,
state: PluginState::Ready,
health: PluginHealth::healthy("Test plugin".to_string(), PluginMetrics::default()),
};
registry.add_plugin(plugin1).unwrap();
let result = registry.add_plugin(plugin2);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PluginLoaderError::AlreadyLoaded { .. }));
}
}