use semver::Version;
use serde_json::to_string_pretty;
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
use crate::VanguardPlugin;
#[derive(Debug, Clone, PartialEq)]
pub enum PluginState {
Active,
Inactive,
Failed(String),
}
#[derive(Error, Debug)]
pub enum RegistryError {
#[error("Plugin not found: {0}")]
NotFound(String),
#[error("Version conflict: {plugin} {version} conflicts with existing version")]
VersionConflict {
plugin: String,
version: String,
},
#[error("Invalid version: {0}")]
InvalidVersion(String),
#[error("Plugin already registered: {plugin} {version}")]
AlreadyRegistered {
plugin: String,
version: String,
},
}
#[derive(Debug)]
struct VersionedPlugin {
version: Version,
plugin: Arc<dyn VanguardPlugin>,
state: PluginState,
}
#[derive(Debug)]
pub struct PluginRegistry {
plugins: RwLock<HashMap<String, Vec<VersionedPlugin>>>,
active_versions: RwLock<HashMap<String, Version>>,
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: RwLock::new(HashMap::new()),
active_versions: RwLock::new(HashMap::new()),
}
}
pub async fn register_plugin(
&self,
plugin: Arc<dyn VanguardPlugin>,
) -> Result<(), RegistryError> {
let name = plugin.metadata().name.clone();
let version_str = plugin.metadata().version.clone();
let version = Version::parse(&version_str)
.map_err(|_| RegistryError::InvalidVersion(version_str.clone()))?;
let mut plugins = self.plugins.write().await;
let versions = plugins.entry(name.clone()).or_insert_with(Vec::new);
if versions.iter().any(|v| v.version == version) {
return Err(RegistryError::AlreadyRegistered {
plugin: name,
version: version_str,
});
}
versions.push(VersionedPlugin {
version: version.clone(),
plugin,
state: PluginState::Active,
});
versions.sort_by(|a, b| b.version.cmp(&a.version));
let mut active_versions = self.active_versions.write().await;
let latest_version = &versions[0].version;
active_versions.insert(name, latest_version.clone());
Ok(())
}
pub async fn get_active_version(
&self,
name: &str,
) -> Result<Arc<dyn VanguardPlugin>, RegistryError> {
let active_versions = self.active_versions.read().await;
let plugins = self.plugins.read().await;
let version = active_versions
.get(name)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
let versions = plugins
.get(name)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
let plugin = versions
.iter()
.find(|v| v.version == *version)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
Ok(plugin.plugin.clone())
}
pub async fn get_plugin(&self, name: &str) -> Option<Arc<dyn VanguardPlugin>> {
let plugins = self.plugins.read().await;
plugins
.get(name)
.and_then(|versions| versions.first().map(|v| v.plugin.clone()))
}
pub async fn activate_version(
&self,
name: &str,
version_str: &str,
) -> Result<(), RegistryError> {
let version = Version::parse(version_str)
.map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
let plugins = self.plugins.read().await;
let versions = plugins
.get(name)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
if !versions.iter().any(|v| v.version == version) {
return Err(RegistryError::NotFound(format!("{} {}", name, version_str)));
}
let mut active_versions = self.active_versions.write().await;
active_versions.insert(name.to_string(), version);
Ok(())
}
pub async fn get_plugin_state(
&self,
name: &str,
version_str: &str,
) -> Result<PluginState, RegistryError> {
let version = Version::parse(version_str)
.map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
let plugins = self.plugins.read().await;
let versions = plugins
.get(name)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
let plugin = versions
.iter()
.find(|v| v.version == version)
.ok_or_else(|| RegistryError::NotFound(format!("{} {}", name, version_str)))?;
Ok(plugin.state.clone())
}
pub async fn set_plugin_state(
&self,
name: &str,
version_str: &str,
state: PluginState,
) -> Result<(), RegistryError> {
let version = Version::parse(version_str)
.map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
let mut plugins = self.plugins.write().await;
let versions = plugins
.get_mut(name)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
let plugin = versions
.iter_mut()
.find(|v| v.version == version)
.ok_or_else(|| RegistryError::NotFound(format!("{} {}", name, version_str)))?;
plugin.state = state;
Ok(())
}
pub async fn remove_plugin(&self, name: &str, version_str: &str) -> Result<(), RegistryError> {
let version = Version::parse(version_str)
.map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
let mut plugins = self.plugins.write().await;
let versions = plugins
.get_mut(name)
.ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
let index = versions
.iter()
.position(|v| v.version == version)
.ok_or_else(|| RegistryError::NotFound(format!("{} {}", name, version_str)))?;
versions.remove(index);
if versions.is_empty() {
plugins.remove(name);
let mut active_versions = self.active_versions.write().await;
active_versions.remove(name);
}
Ok(())
}
pub async fn save_plugin_info(
&self,
plugin_info: &crate::PluginInfo,
) -> Result<(), RegistryError> {
let home_dir = dirs::home_dir().ok_or_else(|| {
RegistryError::NotFound("Could not determine home directory".to_string())
})?;
let registry_dir = home_dir.join(".vanguard").join("registry");
fs::create_dir_all(®istry_dir).map_err(|e| {
RegistryError::NotFound(format!("Failed to create registry directory: {}", e))
})?;
let json = to_string_pretty(plugin_info).map_err(|e| {
RegistryError::NotFound(format!("Failed to serialize plugin info: {}", e))
})?;
let plugin_info_path = registry_dir.join(format!("{}.json", plugin_info.name));
fs::write(&plugin_info_path, json).map_err(|e| {
RegistryError::NotFound(format!("Failed to write plugin info file: {}", e))
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PluginMetadata, ValidationResult};
use async_trait::async_trait;
#[derive(Debug)]
struct TestPlugin {
metadata: PluginMetadata,
}
#[async_trait]
impl VanguardPlugin for TestPlugin {
fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
async fn validate(&self) -> ValidationResult {
ValidationResult::Passed
}
async fn initialize(&self) -> Result<(), String> {
Ok(())
}
async fn cleanup(&self) -> Result<(), String> {
Ok(())
}
}
fn create_test_plugin(name: &str, version: &str) -> Arc<dyn VanguardPlugin> {
Arc::new(TestPlugin {
metadata: PluginMetadata {
name: name.to_string(),
version: version.to_string(),
description: "Test Plugin".to_string(),
author: "Test Author".to_string(),
min_vanguard_version: Some("0.1.0".to_string()),
max_vanguard_version: Some("2.0.0".to_string()),
dependencies: vec![],
},
})
}
#[tokio::test]
async fn test_register_plugin() {
let registry = PluginRegistry::new();
let plugin = create_test_plugin("test-plugin", "1.0.0");
assert!(registry.register_plugin(plugin.clone()).await.is_ok());
let registered = registry.get_plugin("test-plugin").await;
assert!(registered.is_some());
let duplicate = create_test_plugin("test-plugin", "1.0.0");
assert!(matches!(
registry.register_plugin(duplicate).await,
Err(RegistryError::AlreadyRegistered { .. })
));
}
#[tokio::test]
async fn test_version_management() {
let registry = PluginRegistry::new();
let v1 = create_test_plugin("test-plugin", "1.0.0");
let v2 = create_test_plugin("test-plugin", "1.1.0");
registry.register_plugin(v1).await.unwrap();
registry.register_plugin(v2).await.unwrap();
let active = registry.get_active_version("test-plugin").await.unwrap();
assert_eq!(active.metadata().version, "1.1.0");
registry
.activate_version("test-plugin", "1.0.0")
.await
.unwrap();
let active = registry.get_active_version("test-plugin").await.unwrap();
assert_eq!(active.metadata().version, "1.0.0");
}
#[tokio::test]
async fn test_plugin_state() {
let registry = PluginRegistry::new();
let plugin = create_test_plugin("test-plugin", "1.0.0");
registry.register_plugin(plugin).await.unwrap();
let state = registry
.get_plugin_state("test-plugin", "1.0.0")
.await
.unwrap();
assert_eq!(state, PluginState::Active);
registry
.set_plugin_state("test-plugin", "1.0.0", PluginState::Inactive)
.await
.unwrap();
let state = registry
.get_plugin_state("test-plugin", "1.0.0")
.await
.unwrap();
assert_eq!(state, PluginState::Inactive);
}
#[tokio::test]
async fn test_plugin_removal() {
let registry = PluginRegistry::new();
let plugin = create_test_plugin("test-plugin", "1.0.0");
registry.register_plugin(plugin).await.unwrap();
assert!(registry.remove_plugin("test-plugin", "1.0.0").await.is_ok());
assert!(registry.get_plugin("test-plugin").await.is_none());
assert!(matches!(
registry.remove_plugin("non-existent", "1.0.0").await,
Err(RegistryError::NotFound(_))
));
}
}