use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use async_trait::async_trait;
use semver::Version;
use thiserror::Error;
use tokio::sync::RwLock;
use crate::{PluginMetadata, ValidationResult, VanguardPlugin};
#[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(())
}
}
#[derive(Error, Debug)]
pub enum LoaderError {
#[error("Plugin not found: {0}")]
NotFound(String),
#[error("Plugin already loaded: {0}")]
AlreadyLoaded(String),
#[error("Failed to load plugin: {0}")]
LoadFailed(String),
#[error("Plugin validation failed: {0}")]
ValidationFailed(String),
#[error("Plugin dependency error: {name} requires {dependency} {version}")]
DependencyError {
name: String,
dependency: String,
version: String,
},
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone)]
pub struct LoaderConfig {
pub plugin_dir: PathBuf,
pub vanguard_version: Version,
pub validate_on_load: bool,
pub check_dependencies: bool,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
plugin_dir: PathBuf::from(".vanguard/plugins"),
vanguard_version: Version::new(0, 1, 0),
validate_on_load: true,
check_dependencies: true,
}
}
}
#[derive(Debug)]
pub struct PluginLoader {
config: LoaderConfig,
plugins: RwLock<HashMap<String, Arc<dyn VanguardPlugin>>>,
}
impl PluginLoader {
pub fn new(config: LoaderConfig) -> Self {
Self {
config,
plugins: RwLock::new(HashMap::new()),
}
}
pub fn config(&self) -> &LoaderConfig {
&self.config
}
pub async fn load_plugin(&self, name: &str) -> Result<Arc<dyn VanguardPlugin>, LoaderError> {
{
let plugins = self.plugins.read().await;
if plugins.contains_key(name) {
return Err(LoaderError::AlreadyLoaded(name.to_string()));
}
}
let plugin_path = self.config.plugin_dir.join(format!("{}.json", name));
if !plugin_path.exists() {
return Err(LoaderError::NotFound(name.to_string()));
}
let content = fs::read_to_string(&plugin_path).map_err(LoaderError::Io)?;
let metadata: PluginMetadata = serde_json::from_str(&content).map_err(|e| {
LoaderError::ValidationFailed(format!("Invalid plugin metadata: {}", e))
})?;
let plugin = Arc::new(TestPlugin { metadata }) as Arc<dyn VanguardPlugin>;
if self.config.validate_on_load {
match plugin.validate().await {
ValidationResult::Passed => {}
ValidationResult::Failed(reason) => {
return Err(LoaderError::ValidationFailed(reason));
}
}
}
if self.config.check_dependencies {
self.check_dependencies(plugin.as_ref()).await?;
}
let mut plugins = self.plugins.write().await;
if plugins.contains_key(name) {
return Err(LoaderError::AlreadyLoaded(name.to_string()));
}
plugins.insert(name.to_string(), plugin.clone());
Ok(plugin)
}
pub async fn get_plugin(&self, name: &str) -> Option<Arc<dyn VanguardPlugin>> {
self.plugins.read().await.get(name).cloned()
}
pub async fn list_plugins(&self) -> Vec<PluginMetadata> {
self.plugins
.read()
.await
.values()
.map(|p| p.metadata().clone())
.collect()
}
pub async fn unload_plugin(&self, name: &str) -> Result<(), LoaderError> {
let mut plugins = self.plugins.write().await;
if let Some(plugin) = plugins.remove(name) {
if let Err(e) = plugin.cleanup().await {
plugins.insert(name.to_string(), plugin);
return Err(LoaderError::LoadFailed(format!(
"Failed to cleanup plugin: {}",
e
)));
}
Ok(())
} else {
Err(LoaderError::NotFound(name.to_string()))
}
}
#[allow(dead_code)] async fn check_dependencies(&self, plugin: &dyn VanguardPlugin) -> Result<(), LoaderError> {
let dependencies: Vec<_> = {
let plugins = self.plugins.read().await;
plugin
.metadata()
.dependencies
.iter()
.map(|dep| {
let loaded_version =
plugins.get(&dep.name).map(|p| p.metadata().version.clone());
(dep.clone(), loaded_version)
})
.collect()
};
for (dep, loaded_version) in dependencies {
match loaded_version {
Some(version) => {
if version != dep.version {
return Err(LoaderError::DependencyError {
name: plugin.metadata().name.clone(),
dependency: dep.name,
version: dep.version,
});
}
}
None => {
return Err(LoaderError::DependencyError {
name: plugin.metadata().name.clone(),
dependency: dep.name,
version: dep.version,
});
}
}
}
Ok(())
}
pub async fn discover_plugins(&self) -> Result<Vec<PluginMetadata>, LoaderError> {
let mut discovered = Vec::new();
if !self.config.plugin_dir.exists() {
fs::create_dir_all(&self.config.plugin_dir).map_err(LoaderError::Io)?;
return Ok(discovered);
}
let entries = fs::read_dir(&self.config.plugin_dir).map_err(LoaderError::Io)?;
for entry in entries {
let entry = entry.map_err(LoaderError::Io)?;
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
continue;
}
let content = fs::read_to_string(&path).map_err(LoaderError::Io)?;
match serde_json::from_str::<PluginMetadata>(&content) {
Ok(metadata) => {
discovered.push(metadata);
}
Err(e) => {
eprintln!("Failed to parse plugin metadata from {:?}: {}", path, e);
}
}
}
Ok(discovered)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[allow(dead_code)]
fn create_test_plugin(name: &str, version: &str) -> TestPlugin {
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_loader_config() {
let config = LoaderConfig {
plugin_dir: PathBuf::from("/test/plugins"),
vanguard_version: Version::new(1, 0, 0),
validate_on_load: true,
check_dependencies: true,
};
let loader = PluginLoader::new(config.clone());
assert_eq!(loader.config().plugin_dir, PathBuf::from("/test/plugins"));
assert_eq!(loader.config().vanguard_version, Version::new(1, 0, 0));
}
#[tokio::test]
async fn test_plugin_not_found() {
let loader = PluginLoader::new(LoaderConfig::default());
let result = loader.load_plugin("nonexistent").await;
assert!(matches!(result, Err(LoaderError::NotFound(_))));
}
#[tokio::test]
async fn test_list_plugins() {
let loader = PluginLoader::new(LoaderConfig::default());
let plugins = loader.list_plugins().await;
assert!(plugins.is_empty());
}
#[tokio::test]
async fn test_unload_nonexistent() {
let loader = PluginLoader::new(LoaderConfig::default());
let result = loader.unload_plugin("nonexistent").await;
assert!(matches!(result, Err(LoaderError::NotFound(_))));
}
#[tokio::test]
async fn test_discover_plugins() {
let temp_dir = TempDir::new().unwrap();
let plugin_dir = temp_dir.path().join("plugins");
fs::create_dir_all(&plugin_dir).unwrap();
let plugin_path = plugin_dir.join("test-plugin.json");
let plugin_meta = serde_json::json!({
"name": "test-plugin",
"version": "1.0.0",
"author": "Test Author",
"description": "Test Plugin",
"license": "MIT",
"min_vanguard_version": "0.1.0",
"max_vanguard_version": null,
"supported_platforms": [
{ "os": "linux", "arch": "x86_64" }
],
"dependencies": []
});
fs::write(&plugin_path, plugin_meta.to_string()).unwrap();
let config = LoaderConfig {
plugin_dir,
vanguard_version: Version::new(0, 1, 0),
validate_on_load: true,
check_dependencies: true,
};
let loader = PluginLoader::new(config);
let discovered = loader.discover_plugins().await.unwrap();
assert_eq!(discovered.len(), 1);
assert_eq!(discovered[0].name, "test-plugin");
}
#[tokio::test]
async fn test_load_plugin_validation() {
let temp_dir = TempDir::new().unwrap();
let plugin_dir = temp_dir.path().join("plugins");
fs::create_dir_all(&plugin_dir).unwrap();
let plugin_path = plugin_dir.join("invalid-plugin.json");
let plugin_meta = serde_json::json!({
"name": "invalid-plugin"
});
fs::write(&plugin_path, plugin_meta.to_string()).unwrap();
let config = LoaderConfig {
plugin_dir,
vanguard_version: Version::new(0, 1, 0),
validate_on_load: true,
check_dependencies: true,
};
let loader = PluginLoader::new(config);
let result = loader.load_plugin("invalid-plugin").await;
assert!(matches!(result, Err(LoaderError::ValidationFailed(_))));
}
#[tokio::test]
async fn test_load_plugin_dependencies() {
let temp_dir = TempDir::new().unwrap();
let plugin_dir = temp_dir.path().join("plugins");
fs::create_dir_all(&plugin_dir).unwrap();
let plugin_path = plugin_dir.join("dependent-plugin.json");
let plugin_meta = serde_json::json!({
"name": "dependent-plugin",
"version": "1.0.0",
"author": "Test Author",
"description": "Test Plugin",
"license": "MIT",
"min_vanguard_version": "0.1.0",
"max_vanguard_version": null,
"dependencies": [
{
"name": "base-plugin",
"version": "1.0.0"
}
]
});
fs::write(&plugin_path, plugin_meta.to_string()).unwrap();
let config = LoaderConfig {
plugin_dir,
vanguard_version: Version::new(0, 1, 0),
validate_on_load: true,
check_dependencies: true,
};
let loader = PluginLoader::new(config);
let result = loader.load_plugin("dependent-plugin").await;
assert!(matches!(result, Err(LoaderError::DependencyError { .. })));
}
}