Skip to main content

oxicode_sdk/middleware/
plugin.rs

1//! Plugin loader for dynamic middleware loading
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9
10use super::Middleware;
11
12/// Plugin manifest — metadata for a plugin.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PluginManifest {
15    /// Human-readable name of the plugin, also used as its lookup key.
16    pub name: String,
17    /// Version string of the plugin.
18    pub version: String,
19    /// Middleware phases this plugin participates in.
20    pub phases: Vec<String>,
21    /// Path or symbol resolved to load the plugin entry point.
22    pub entry_point: String,
23    /// Capability strings the plugin is permitted to use.
24    pub permissions: Vec<String>,
25}
26
27impl PluginManifest {
28    /// Parse a manifest from a JSON string.
29    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
30        serde_json::from_str(json)
31    }
32
33    /// Read and parse a manifest from a JSON file on disk.
34    pub fn from_file(path: &Path) -> std::io::Result<Self> {
35        let content = std::fs::read_to_string(path)?;
36        serde_json::from_str(&content)
37            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
38    }
39}
40
41/// Loads and manages dynamically-discovered middleware plugins.
42pub struct PluginLoader {
43    #[expect(dead_code)]
44    plugins_dir: PathBuf,
45    loaded: Arc<RwLock<HashMap<String, Arc<dyn Middleware>>>>,
46    manifests: Arc<RwLock<HashMap<String, PluginManifest>>>,
47}
48
49impl PluginLoader {
50    /// Create a loader rooted at the given plugins directory.
51    pub fn new(plugins_dir: impl Into<PathBuf>) -> Self {
52        Self {
53            plugins_dir: plugins_dir.into(),
54            loaded: Arc::new(RwLock::new(HashMap::new())),
55            manifests: Arc::new(RwLock::new(HashMap::new())),
56        }
57    }
58
59    /// Load a plugin from its manifest file, registering it by name.
60    pub async fn load(&self, manifest_path: &Path) -> crate::error::SdkResult<String> {
61        let manifest = PluginManifest::from_file(manifest_path).map_err(|e| {
62            crate::error::SdkError::InvalidState {
63                entity: "plugin".into(),
64                reason: format!("failed to load manifest: {}", e),
65            }
66        })?;
67        let name = manifest.name.clone();
68        let mut manifests = self.manifests.write();
69        manifests.insert(name.clone(), manifest);
70        Ok(name)
71    }
72
73    /// Return all currently loaded middlewares.
74    pub fn middlewares(&self) -> Vec<Arc<dyn Middleware>> {
75        let loaded = self.loaded.read();
76        loaded.values().cloned().collect()
77    }
78
79    /// Look up a loaded middleware by name.
80    pub fn get(&self, name: &str) -> Option<Arc<dyn Middleware>> {
81        self.loaded.read().get(name).cloned()
82    }
83
84    /// Remove the plugin with the given name; returns `true` if it was present.
85    pub fn unload(&self, name: &str) -> bool {
86        let mut loaded = self.loaded.write();
87        let removed = loaded.remove(name).is_some();
88        let mut manifests = self.manifests.write();
89        manifests.remove(name);
90        removed
91    }
92
93    /// Register an already-constructed middleware, keyed by its [`Middleware::name`].
94    pub fn register(&self, middleware: Arc<dyn Middleware>) {
95        let mut loaded = self.loaded.write();
96        loaded.insert(middleware.name().to_string(), middleware);
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::Middleware;
104    use crate::middleware::{MiddlewareContext, MiddlewarePhase, MiddlewareResult};
105    use std::future::Future;
106    use std::pin::Pin;
107
108    struct MockMiddleware {
109        name: String,
110    }
111
112    impl MockMiddleware {
113        fn new(name: &str) -> Self {
114            Self {
115                name: name.to_string(),
116            }
117        }
118    }
119
120    impl Middleware for MockMiddleware {
121        fn name(&self) -> &str {
122            &self.name
123        }
124        fn phases(&self) -> Vec<MiddlewarePhase> {
125            vec![MiddlewarePhase::BeforeTool]
126        }
127        fn handle<'a>(
128            &'a self,
129            _ctx: &'a MiddlewareContext,
130        ) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send + 'a>> {
131            Box::pin(async { MiddlewareResult::pass() })
132        }
133    }
134
135    #[test]
136    fn test_plugin_loader_register() {
137        let loader = PluginLoader::new("/tmp/plugins");
138        loader.register(Arc::new(MockMiddleware::new("test-plugin")));
139        let mws = loader.middlewares();
140        assert_eq!(mws.len(), 1);
141        assert_eq!(mws[0].name(), "test-plugin");
142    }
143
144    #[test]
145    fn test_plugin_loader_unload() {
146        let loader = PluginLoader::new("/tmp/plugins");
147        loader.register(Arc::new(MockMiddleware::new("test-plugin")));
148        assert!(loader.get("test-plugin").is_some());
149        loader.unload("test-plugin");
150        assert!(loader.get("test-plugin").is_none());
151    }
152
153    #[test]
154    fn test_plugin_manifest_parse() {
155        let json = r#"{"name":"test-plugin","version":"1.0.0","phases":["before_tool"],"entry_point":"libtest.so","permissions":[]}"#;
156        let manifest = PluginManifest::from_json(json).unwrap();
157        assert_eq!(manifest.name, "test-plugin");
158    }
159}