vanguard_plugin/
registry.rs

1use semver::Version;
2use serde_json::to_string_pretty;
3use std::collections::HashMap;
4use std::fs;
5use std::sync::Arc;
6use thiserror::Error;
7use tokio::sync::RwLock;
8
9use crate::VanguardPlugin;
10
11/// Represents the current state of a plugin version
12#[derive(Debug, Clone, PartialEq)]
13pub enum PluginState {
14    /// Plugin is active and ready to use
15    Active,
16    /// Plugin is installed but not currently active
17    Inactive,
18    /// Plugin failed to load or operate with given error message
19    Failed(String),
20}
21
22/// Errors that can occur during plugin registry operations
23#[derive(Error, Debug)]
24pub enum RegistryError {
25    /// Plugin was not found in the registry
26    #[error("Plugin not found: {0}")]
27    NotFound(String),
28
29    /// A version conflict occurred between plugins
30    #[error("Version conflict: {plugin} {version} conflicts with existing version")]
31    VersionConflict {
32        /// Name of the plugin with conflict
33        plugin: String,
34        /// Version string that caused the conflict
35        version: String,
36    },
37
38    /// The provided version string is invalid
39    #[error("Invalid version: {0}")]
40    InvalidVersion(String),
41
42    /// Attempted to register a plugin version that already exists
43    #[error("Plugin already registered: {plugin} {version}")]
44    AlreadyRegistered {
45        /// Name of the plugin that was already registered
46        plugin: String,
47        /// Version that was already registered
48        version: String,
49    },
50}
51
52/// A versioned plugin instance with its state
53#[derive(Debug)]
54struct VersionedPlugin {
55    /// Semantic version of the plugin
56    version: Version,
57    /// The plugin instance
58    plugin: Arc<dyn VanguardPlugin>,
59    /// Current state of the plugin
60    state: PluginState,
61}
62
63/// Registry for managing multiple versions of plugins
64///
65/// The registry keeps track of all installed plugin versions and their states,
66/// as well as which version is currently active for each plugin.
67#[derive(Debug)]
68pub struct PluginRegistry {
69    /// Map of plugin names to their available versions
70    plugins: RwLock<HashMap<String, Vec<VersionedPlugin>>>,
71    /// Map of plugin names to their currently active version
72    active_versions: RwLock<HashMap<String, Version>>,
73}
74
75impl Default for PluginRegistry {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl PluginRegistry {
82    /// Create a new empty plugin registry
83    pub fn new() -> Self {
84        Self {
85            plugins: RwLock::new(HashMap::new()),
86            active_versions: RwLock::new(HashMap::new()),
87        }
88    }
89
90    /// Register a new plugin version
91    pub async fn register_plugin(
92        &self,
93        plugin: Arc<dyn VanguardPlugin>,
94    ) -> Result<(), RegistryError> {
95        let name = plugin.metadata().name.clone();
96        let version_str = plugin.metadata().version.clone();
97        let version = Version::parse(&version_str)
98            .map_err(|_| RegistryError::InvalidVersion(version_str.clone()))?;
99
100        let mut plugins = self.plugins.write().await;
101
102        // Get or create the version list for this plugin
103        let versions = plugins.entry(name.clone()).or_insert_with(Vec::new);
104
105        // Check if version already exists
106        if versions.iter().any(|v| v.version == version) {
107            return Err(RegistryError::AlreadyRegistered {
108                plugin: name,
109                version: version_str,
110            });
111        }
112
113        // Add new version
114        versions.push(VersionedPlugin {
115            version: version.clone(),
116            plugin,
117            state: PluginState::Active,
118        });
119
120        // Sort versions descending
121        versions.sort_by(|a, b| b.version.cmp(&a.version));
122
123        // Always set the latest version as active
124        let mut active_versions = self.active_versions.write().await;
125        let latest_version = &versions[0].version;
126        active_versions.insert(name, latest_version.clone());
127
128        Ok(())
129    }
130
131    /// Get the currently active version of a plugin
132    pub async fn get_active_version(
133        &self,
134        name: &str,
135    ) -> Result<Arc<dyn VanguardPlugin>, RegistryError> {
136        let active_versions = self.active_versions.read().await;
137        let plugins = self.plugins.read().await;
138
139        let version = active_versions
140            .get(name)
141            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
142
143        let versions = plugins
144            .get(name)
145            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
146
147        let plugin = versions
148            .iter()
149            .find(|v| v.version == *version)
150            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
151
152        Ok(plugin.plugin.clone())
153    }
154
155    /// Get any version of a plugin (usually the latest)
156    pub async fn get_plugin(&self, name: &str) -> Option<Arc<dyn VanguardPlugin>> {
157        let plugins = self.plugins.read().await;
158        plugins
159            .get(name)
160            .and_then(|versions| versions.first().map(|v| v.plugin.clone()))
161    }
162
163    /// Activate a specific version of a plugin
164    pub async fn activate_version(
165        &self,
166        name: &str,
167        version_str: &str,
168    ) -> Result<(), RegistryError> {
169        let version = Version::parse(version_str)
170            .map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
171
172        let plugins = self.plugins.read().await;
173
174        // Verify version exists
175        let versions = plugins
176            .get(name)
177            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
178
179        if !versions.iter().any(|v| v.version == version) {
180            return Err(RegistryError::NotFound(format!("{} {}", name, version_str)));
181        }
182
183        // Update active version
184        let mut active_versions = self.active_versions.write().await;
185        active_versions.insert(name.to_string(), version);
186
187        Ok(())
188    }
189
190    /// Get the state of a specific plugin version
191    pub async fn get_plugin_state(
192        &self,
193        name: &str,
194        version_str: &str,
195    ) -> Result<PluginState, RegistryError> {
196        let version = Version::parse(version_str)
197            .map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
198
199        let plugins = self.plugins.read().await;
200
201        let versions = plugins
202            .get(name)
203            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
204
205        let plugin = versions
206            .iter()
207            .find(|v| v.version == version)
208            .ok_or_else(|| RegistryError::NotFound(format!("{} {}", name, version_str)))?;
209
210        Ok(plugin.state.clone())
211    }
212
213    /// Set the state of a specific plugin version
214    pub async fn set_plugin_state(
215        &self,
216        name: &str,
217        version_str: &str,
218        state: PluginState,
219    ) -> Result<(), RegistryError> {
220        let version = Version::parse(version_str)
221            .map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
222
223        let mut plugins = self.plugins.write().await;
224
225        let versions = plugins
226            .get_mut(name)
227            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
228
229        let plugin = versions
230            .iter_mut()
231            .find(|v| v.version == version)
232            .ok_or_else(|| RegistryError::NotFound(format!("{} {}", name, version_str)))?;
233
234        plugin.state = state;
235        Ok(())
236    }
237
238    /// Remove a specific version of a plugin
239    pub async fn remove_plugin(&self, name: &str, version_str: &str) -> Result<(), RegistryError> {
240        let version = Version::parse(version_str)
241            .map_err(|_| RegistryError::InvalidVersion(version_str.to_string()))?;
242
243        let mut plugins = self.plugins.write().await;
244
245        let versions = plugins
246            .get_mut(name)
247            .ok_or_else(|| RegistryError::NotFound(name.to_string()))?;
248
249        // Find and remove the version
250        let index = versions
251            .iter()
252            .position(|v| v.version == version)
253            .ok_or_else(|| RegistryError::NotFound(format!("{} {}", name, version_str)))?;
254
255        versions.remove(index);
256
257        // If we removed all versions, remove the plugin entirely
258        if versions.is_empty() {
259            plugins.remove(name);
260
261            // Also remove from active versions
262            let mut active_versions = self.active_versions.write().await;
263            active_versions.remove(name);
264        }
265
266        Ok(())
267    }
268
269    /// Save plugin information to the registry directory
270    pub async fn save_plugin_info(
271        &self,
272        plugin_info: &crate::PluginInfo,
273    ) -> Result<(), RegistryError> {
274        // Create registry directory if it doesn't exist
275        let home_dir = dirs::home_dir().ok_or_else(|| {
276            RegistryError::NotFound("Could not determine home directory".to_string())
277        })?;
278        let registry_dir = home_dir.join(".vanguard").join("registry");
279        fs::create_dir_all(&registry_dir).map_err(|e| {
280            RegistryError::NotFound(format!("Failed to create registry directory: {}", e))
281        })?;
282
283        // Serialize plugin info to JSON
284        let json = to_string_pretty(plugin_info).map_err(|e| {
285            RegistryError::NotFound(format!("Failed to serialize plugin info: {}", e))
286        })?;
287
288        // Write to file
289        let plugin_info_path = registry_dir.join(format!("{}.json", plugin_info.name));
290        fs::write(&plugin_info_path, json).map_err(|e| {
291            RegistryError::NotFound(format!("Failed to write plugin info file: {}", e))
292        })?;
293
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::{PluginMetadata, ValidationResult};
302    use async_trait::async_trait;
303
304    // Test plugin implementation
305    #[derive(Debug)]
306    struct TestPlugin {
307        metadata: PluginMetadata,
308    }
309
310    #[async_trait]
311    impl VanguardPlugin for TestPlugin {
312        fn metadata(&self) -> &PluginMetadata {
313            &self.metadata
314        }
315
316        async fn validate(&self) -> ValidationResult {
317            ValidationResult::Passed
318        }
319
320        async fn initialize(&self) -> Result<(), String> {
321            Ok(())
322        }
323
324        async fn cleanup(&self) -> Result<(), String> {
325            Ok(())
326        }
327    }
328
329    fn create_test_plugin(name: &str, version: &str) -> Arc<dyn VanguardPlugin> {
330        Arc::new(TestPlugin {
331            metadata: PluginMetadata {
332                name: name.to_string(),
333                version: version.to_string(),
334                description: "Test Plugin".to_string(),
335                author: "Test Author".to_string(),
336                min_vanguard_version: Some("0.1.0".to_string()),
337                max_vanguard_version: Some("2.0.0".to_string()),
338                dependencies: vec![],
339            },
340        })
341    }
342
343    #[tokio::test]
344    async fn test_register_plugin() {
345        let registry = PluginRegistry::new();
346        let plugin = create_test_plugin("test-plugin", "1.0.0");
347
348        assert!(registry.register_plugin(plugin.clone()).await.is_ok());
349
350        // Verify plugin is registered
351        let registered = registry.get_plugin("test-plugin").await;
352        assert!(registered.is_some());
353
354        // Try registering same version again
355        let duplicate = create_test_plugin("test-plugin", "1.0.0");
356        assert!(matches!(
357            registry.register_plugin(duplicate).await,
358            Err(RegistryError::AlreadyRegistered { .. })
359        ));
360    }
361
362    #[tokio::test]
363    async fn test_version_management() {
364        let registry = PluginRegistry::new();
365
366        // Register multiple versions
367        let v1 = create_test_plugin("test-plugin", "1.0.0");
368        let v2 = create_test_plugin("test-plugin", "1.1.0");
369
370        registry.register_plugin(v1).await.unwrap();
371        registry.register_plugin(v2).await.unwrap();
372
373        // Latest version should be active by default
374        let active = registry.get_active_version("test-plugin").await.unwrap();
375        assert_eq!(active.metadata().version, "1.1.0");
376
377        // Switch to older version
378        registry
379            .activate_version("test-plugin", "1.0.0")
380            .await
381            .unwrap();
382        let active = registry.get_active_version("test-plugin").await.unwrap();
383        assert_eq!(active.metadata().version, "1.0.0");
384    }
385
386    #[tokio::test]
387    async fn test_plugin_state() {
388        let registry = PluginRegistry::new();
389        let plugin = create_test_plugin("test-plugin", "1.0.0");
390
391        registry.register_plugin(plugin).await.unwrap();
392
393        // Should be active by default
394        let state = registry
395            .get_plugin_state("test-plugin", "1.0.0")
396            .await
397            .unwrap();
398        assert_eq!(state, PluginState::Active);
399
400        // Test state transition
401        registry
402            .set_plugin_state("test-plugin", "1.0.0", PluginState::Inactive)
403            .await
404            .unwrap();
405        let state = registry
406            .get_plugin_state("test-plugin", "1.0.0")
407            .await
408            .unwrap();
409        assert_eq!(state, PluginState::Inactive);
410    }
411
412    #[tokio::test]
413    async fn test_plugin_removal() {
414        let registry = PluginRegistry::new();
415        let plugin = create_test_plugin("test-plugin", "1.0.0");
416
417        registry.register_plugin(plugin).await.unwrap();
418        assert!(registry.remove_plugin("test-plugin", "1.0.0").await.is_ok());
419
420        // Verify plugin is removed
421        assert!(registry.get_plugin("test-plugin").await.is_none());
422
423        // Try removing non-existent plugin
424        assert!(matches!(
425            registry.remove_plugin("non-existent", "1.0.0").await,
426            Err(RegistryError::NotFound(_))
427        ));
428    }
429}