1use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
2
3use async_trait::async_trait;
4use semver::Version;
5use thiserror::Error;
6use tokio::sync::RwLock;
7
8use crate::{PluginMetadata, ValidationResult, VanguardPlugin};
9
10#[derive(Debug)]
12struct TestPlugin {
13 metadata: PluginMetadata,
14}
15
16#[async_trait]
17impl VanguardPlugin for TestPlugin {
18 fn metadata(&self) -> &PluginMetadata {
19 &self.metadata
20 }
21
22 async fn validate(&self) -> ValidationResult {
23 ValidationResult::Passed
24 }
25
26 async fn initialize(&self) -> Result<(), String> {
27 Ok(())
28 }
29
30 async fn cleanup(&self) -> Result<(), String> {
31 Ok(())
32 }
33}
34
35#[derive(Error, Debug)]
37pub enum LoaderError {
38 #[error("Plugin not found: {0}")]
40 NotFound(String),
41
42 #[error("Plugin already loaded: {0}")]
44 AlreadyLoaded(String),
45
46 #[error("Failed to load plugin: {0}")]
48 LoadFailed(String),
49
50 #[error("Plugin validation failed: {0}")]
52 ValidationFailed(String),
53
54 #[error("Plugin dependency error: {name} requires {dependency} {version}")]
56 DependencyError {
57 name: String,
59 dependency: String,
61 version: String,
63 },
64
65 #[error("IO error: {0}")]
67 Io(#[from] std::io::Error),
68}
69
70#[derive(Debug, Clone)]
72pub struct LoaderConfig {
73 pub plugin_dir: PathBuf,
75 pub vanguard_version: Version,
77 pub validate_on_load: bool,
79 pub check_dependencies: bool,
81}
82
83impl Default for LoaderConfig {
84 fn default() -> Self {
85 Self {
86 plugin_dir: PathBuf::from(".vanguard/plugins"),
87 vanguard_version: Version::new(0, 1, 0),
88 validate_on_load: true,
89 check_dependencies: true,
90 }
91 }
92}
93
94#[derive(Debug)]
96pub struct PluginLoader {
97 config: LoaderConfig,
99 plugins: RwLock<HashMap<String, Arc<dyn VanguardPlugin>>>,
101}
102
103impl PluginLoader {
104 pub fn new(config: LoaderConfig) -> Self {
106 Self {
107 config,
108 plugins: RwLock::new(HashMap::new()),
109 }
110 }
111
112 pub fn config(&self) -> &LoaderConfig {
114 &self.config
115 }
116
117 pub async fn load_plugin(&self, name: &str) -> Result<Arc<dyn VanguardPlugin>, LoaderError> {
119 {
121 let plugins = self.plugins.read().await;
122 if plugins.contains_key(name) {
123 return Err(LoaderError::AlreadyLoaded(name.to_string()));
124 }
125 }
126
127 let plugin_path = self.config.plugin_dir.join(format!("{}.json", name));
129 if !plugin_path.exists() {
130 return Err(LoaderError::NotFound(name.to_string()));
131 }
132
133 let content = fs::read_to_string(&plugin_path).map_err(LoaderError::Io)?;
135
136 let metadata: PluginMetadata = serde_json::from_str(&content).map_err(|e| {
137 LoaderError::ValidationFailed(format!("Invalid plugin metadata: {}", e))
138 })?;
139
140 let plugin = Arc::new(TestPlugin { metadata }) as Arc<dyn VanguardPlugin>;
143
144 if self.config.validate_on_load {
146 match plugin.validate().await {
147 ValidationResult::Passed => {}
148 ValidationResult::Failed(reason) => {
149 return Err(LoaderError::ValidationFailed(reason));
150 }
151 }
152 }
153
154 if self.config.check_dependencies {
156 self.check_dependencies(plugin.as_ref()).await?;
157 }
158
159 let mut plugins = self.plugins.write().await;
161
162 if plugins.contains_key(name) {
164 return Err(LoaderError::AlreadyLoaded(name.to_string()));
165 }
166
167 plugins.insert(name.to_string(), plugin.clone());
169 Ok(plugin)
170 }
171
172 pub async fn get_plugin(&self, name: &str) -> Option<Arc<dyn VanguardPlugin>> {
174 self.plugins.read().await.get(name).cloned()
175 }
176
177 pub async fn list_plugins(&self) -> Vec<PluginMetadata> {
179 self.plugins
180 .read()
181 .await
182 .values()
183 .map(|p| p.metadata().clone())
184 .collect()
185 }
186
187 pub async fn unload_plugin(&self, name: &str) -> Result<(), LoaderError> {
189 let mut plugins = self.plugins.write().await;
190
191 if let Some(plugin) = plugins.remove(name) {
192 if let Err(e) = plugin.cleanup().await {
194 plugins.insert(name.to_string(), plugin);
196 return Err(LoaderError::LoadFailed(format!(
197 "Failed to cleanup plugin: {}",
198 e
199 )));
200 }
201 Ok(())
202 } else {
203 Err(LoaderError::NotFound(name.to_string()))
204 }
205 }
206
207 #[allow(dead_code)] async fn check_dependencies(&self, plugin: &dyn VanguardPlugin) -> Result<(), LoaderError> {
210 let dependencies: Vec<_> = {
212 let plugins = self.plugins.read().await;
213 plugin
214 .metadata()
215 .dependencies
216 .iter()
217 .map(|dep| {
218 let loaded_version =
219 plugins.get(&dep.name).map(|p| p.metadata().version.clone());
220 (dep.clone(), loaded_version)
221 })
222 .collect()
223 };
224
225 for (dep, loaded_version) in dependencies {
227 match loaded_version {
228 Some(version) => {
229 if version != dep.version {
231 return Err(LoaderError::DependencyError {
232 name: plugin.metadata().name.clone(),
233 dependency: dep.name,
234 version: dep.version,
235 });
236 }
237 }
238 None => {
239 return Err(LoaderError::DependencyError {
240 name: plugin.metadata().name.clone(),
241 dependency: dep.name,
242 version: dep.version,
243 });
244 }
245 }
246 }
247
248 Ok(())
249 }
250
251 pub async fn discover_plugins(&self) -> Result<Vec<PluginMetadata>, LoaderError> {
253 let mut discovered = Vec::new();
254
255 if !self.config.plugin_dir.exists() {
257 fs::create_dir_all(&self.config.plugin_dir).map_err(LoaderError::Io)?;
258 return Ok(discovered);
259 }
260
261 let entries = fs::read_dir(&self.config.plugin_dir).map_err(LoaderError::Io)?;
263
264 for entry in entries {
266 let entry = entry.map_err(LoaderError::Io)?;
267 let path = entry.path();
268
269 if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
271 continue;
272 }
273
274 let content = fs::read_to_string(&path).map_err(LoaderError::Io)?;
276
277 match serde_json::from_str::<PluginMetadata>(&content) {
278 Ok(metadata) => {
279 discovered.push(metadata);
280 }
281 Err(e) => {
282 eprintln!("Failed to parse plugin metadata from {:?}: {}", path, e);
284 }
285 }
286 }
287
288 Ok(discovered)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use tempfile::TempDir;
296
297 #[allow(dead_code)]
298 fn create_test_plugin(name: &str, version: &str) -> TestPlugin {
299 TestPlugin {
300 metadata: PluginMetadata {
301 name: name.to_string(),
302 version: version.to_string(),
303 description: "Test Plugin".to_string(),
304 author: "Test Author".to_string(),
305 min_vanguard_version: Some("0.1.0".to_string()),
306 max_vanguard_version: Some("2.0.0".to_string()),
307 dependencies: vec![],
308 },
309 }
310 }
311
312 #[tokio::test]
313 async fn test_loader_config() {
314 let config = LoaderConfig {
315 plugin_dir: PathBuf::from("/test/plugins"),
316 vanguard_version: Version::new(1, 0, 0),
317 validate_on_load: true,
318 check_dependencies: true,
319 };
320
321 let loader = PluginLoader::new(config.clone());
322 assert_eq!(loader.config().plugin_dir, PathBuf::from("/test/plugins"));
323 assert_eq!(loader.config().vanguard_version, Version::new(1, 0, 0));
324 }
325
326 #[tokio::test]
327 async fn test_plugin_not_found() {
328 let loader = PluginLoader::new(LoaderConfig::default());
329 let result = loader.load_plugin("nonexistent").await;
330 assert!(matches!(result, Err(LoaderError::NotFound(_))));
331 }
332
333 #[tokio::test]
334 async fn test_list_plugins() {
335 let loader = PluginLoader::new(LoaderConfig::default());
336 let plugins = loader.list_plugins().await;
337 assert!(plugins.is_empty());
338 }
339
340 #[tokio::test]
341 async fn test_unload_nonexistent() {
342 let loader = PluginLoader::new(LoaderConfig::default());
343 let result = loader.unload_plugin("nonexistent").await;
344 assert!(matches!(result, Err(LoaderError::NotFound(_))));
345 }
346
347 #[tokio::test]
348 async fn test_discover_plugins() {
349 let temp_dir = TempDir::new().unwrap();
350 let plugin_dir = temp_dir.path().join("plugins");
351 fs::create_dir_all(&plugin_dir).unwrap();
352
353 let plugin_path = plugin_dir.join("test-plugin.json");
355 let plugin_meta = serde_json::json!({
356 "name": "test-plugin",
357 "version": "1.0.0",
358 "author": "Test Author",
359 "description": "Test Plugin",
360 "license": "MIT",
361 "min_vanguard_version": "0.1.0",
362 "max_vanguard_version": null,
363 "supported_platforms": [
364 { "os": "linux", "arch": "x86_64" }
365 ],
366 "dependencies": []
367 });
368 fs::write(&plugin_path, plugin_meta.to_string()).unwrap();
369
370 let config = LoaderConfig {
371 plugin_dir,
372 vanguard_version: Version::new(0, 1, 0),
373 validate_on_load: true,
374 check_dependencies: true,
375 };
376
377 let loader = PluginLoader::new(config);
378 let discovered = loader.discover_plugins().await.unwrap();
379
380 assert_eq!(discovered.len(), 1);
381 assert_eq!(discovered[0].name, "test-plugin");
382 }
383
384 #[tokio::test]
385 async fn test_load_plugin_validation() {
386 let temp_dir = TempDir::new().unwrap();
387 let plugin_dir = temp_dir.path().join("plugins");
388 fs::create_dir_all(&plugin_dir).unwrap();
389
390 let plugin_path = plugin_dir.join("invalid-plugin.json");
392 let plugin_meta = serde_json::json!({
393 "name": "invalid-plugin"
394 });
395 fs::write(&plugin_path, plugin_meta.to_string()).unwrap();
396
397 let config = LoaderConfig {
398 plugin_dir,
399 vanguard_version: Version::new(0, 1, 0),
400 validate_on_load: true,
401 check_dependencies: true,
402 };
403
404 let loader = PluginLoader::new(config);
405 let result = loader.load_plugin("invalid-plugin").await;
406
407 assert!(matches!(result, Err(LoaderError::ValidationFailed(_))));
408 }
409
410 #[tokio::test]
411 async fn test_load_plugin_dependencies() {
412 let temp_dir = TempDir::new().unwrap();
413 let plugin_dir = temp_dir.path().join("plugins");
414 fs::create_dir_all(&plugin_dir).unwrap();
415
416 let plugin_path = plugin_dir.join("dependent-plugin.json");
418 let plugin_meta = serde_json::json!({
419 "name": "dependent-plugin",
420 "version": "1.0.0",
421 "author": "Test Author",
422 "description": "Test Plugin",
423 "license": "MIT",
424 "min_vanguard_version": "0.1.0",
425 "max_vanguard_version": null,
426 "dependencies": [
427 {
428 "name": "base-plugin",
429 "version": "1.0.0"
430 }
431 ]
432 });
433 fs::write(&plugin_path, plugin_meta.to_string()).unwrap();
434
435 let config = LoaderConfig {
436 plugin_dir,
437 vanguard_version: Version::new(0, 1, 0),
438 validate_on_load: true,
439 check_dependencies: true,
440 };
441
442 let loader = PluginLoader::new(config);
443 let result = loader.load_plugin("dependent-plugin").await;
444
445 assert!(matches!(result, Err(LoaderError::DependencyError { .. })));
446 }
447}