1#[cfg(feature = "wasm-plugins")]
7pub mod host;
8#[cfg(feature = "wasm-plugins")]
9pub mod loader;
10#[cfg(feature = "wasm-plugins")]
11pub mod sandbox;
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PluginManifest {
19 pub id: String,
21 pub name: String,
23 pub version: String,
25 pub description: String,
27 pub author: String,
29 pub min_commander_version: Option<String>,
31 pub permissions: PluginPermissions,
33 pub config: HashMap<String, String>,
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct PluginPermissions {
40 pub read_vars: bool,
42 pub set_vars: bool,
44 pub http: bool,
46 pub log: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct PluginResult {
53 pub success: bool,
55 pub output: String,
57 pub error: Option<String>,
59}
60
61pub trait Plugin: Send + Sync {
63 fn manifest(&self) -> &PluginManifest;
65
66 fn initialize(&mut self, config: &HashMap<String, String>) -> anyhow::Result<()>;
68
69 fn execute(&mut self, input: &str) -> anyhow::Result<PluginResult>;
71
72 fn shutdown(&mut self) -> anyhow::Result<()>;
74}
75
76pub struct PluginRegistry {
78 plugins: HashMap<String, Box<dyn Plugin>>,
79}
80
81impl PluginRegistry {
82 pub fn new() -> Self {
84 Self {
85 plugins: HashMap::new(),
86 }
87 }
88
89 pub fn register(&mut self, plugin: Box<dyn Plugin>) -> anyhow::Result<()> {
91 let id = plugin.manifest().id.clone();
92 if self.plugins.contains_key(&id) {
93 anyhow::bail!("Plugin '{}' is already registered", id);
94 }
95 self.plugins.insert(id, plugin);
96 Ok(())
97 }
98
99 pub fn unregister(&mut self, id: &str) -> anyhow::Result<()> {
101 if let Some(mut plugin) = self.plugins.remove(id) {
102 plugin.shutdown()?;
103 }
104 Ok(())
105 }
106
107 pub fn get(&self, id: &str) -> Option<&dyn Plugin> {
109 self.plugins.get(id).map(|p| p.as_ref())
110 }
111
112 pub fn get_mut(&mut self, id: &str) -> Option<&mut (dyn Plugin + 'static)> {
114 self.plugins.get_mut(id).map(|p| &mut **p)
115 }
116
117 pub fn list(&self) -> Vec<&PluginManifest> {
119 self.plugins.values().map(|p| p.manifest()).collect()
120 }
121
122 pub fn execute(&mut self, id: &str, input: &str) -> anyhow::Result<PluginResult> {
124 let plugin = self
125 .plugins
126 .get_mut(id)
127 .ok_or_else(|| anyhow::anyhow!("Plugin '{}' not found", id))?;
128 plugin.execute(input)
129 }
130}
131
132impl Default for PluginRegistry {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 struct MockPlugin {
143 manifest: PluginManifest,
144 initialized: bool,
145 }
146
147 impl MockPlugin {
148 fn new(id: &str) -> Self {
149 Self {
150 manifest: PluginManifest {
151 id: id.to_string(),
152 name: format!("Mock {}", id),
153 version: "1.0.0".to_string(),
154 description: "A mock plugin".to_string(),
155 author: "test".to_string(),
156 min_commander_version: None,
157 permissions: PluginPermissions::default(),
158 config: HashMap::new(),
159 },
160 initialized: false,
161 }
162 }
163 }
164
165 impl Plugin for MockPlugin {
166 fn manifest(&self) -> &PluginManifest {
167 &self.manifest
168 }
169
170 fn initialize(&mut self, _config: &HashMap<String, String>) -> anyhow::Result<()> {
171 self.initialized = true;
172 Ok(())
173 }
174
175 fn execute(&mut self, input: &str) -> anyhow::Result<PluginResult> {
176 Ok(PluginResult {
177 success: true,
178 output: format!("Processed: {}", input),
179 error: None,
180 })
181 }
182
183 fn shutdown(&mut self) -> anyhow::Result<()> {
184 self.initialized = false;
185 Ok(())
186 }
187 }
188
189 #[test]
190 fn test_plugin_manifest_serialization() {
191 let manifest = PluginManifest {
192 id: "test-plugin".into(),
193 name: "Test Plugin".into(),
194 version: "1.0.0".into(),
195 description: "A test".into(),
196 author: "tester".into(),
197 min_commander_version: Some("0.1.0".into()),
198 permissions: PluginPermissions {
199 read_vars: true,
200 set_vars: false,
201 http: true,
202 log: true,
203 },
204 config: HashMap::new(),
205 };
206 let json = serde_json::to_string(&manifest).unwrap();
207 let deserialized: PluginManifest = serde_json::from_str(&json).unwrap();
208 assert_eq!(deserialized.id, "test-plugin");
209 assert!(deserialized.permissions.read_vars);
210 assert!(!deserialized.permissions.set_vars);
211 }
212
213 #[test]
214 fn test_registry_register_and_list() {
215 let mut registry = PluginRegistry::new();
216 registry.register(Box::new(MockPlugin::new("p1"))).unwrap();
217 registry.register(Box::new(MockPlugin::new("p2"))).unwrap();
218
219 let list = registry.list();
220 assert_eq!(list.len(), 2);
221 }
222
223 #[test]
224 fn test_registry_duplicate_register() {
225 let mut registry = PluginRegistry::new();
226 registry.register(Box::new(MockPlugin::new("p1"))).unwrap();
227 assert!(registry.register(Box::new(MockPlugin::new("p1"))).is_err());
228 }
229
230 #[test]
231 fn test_registry_execute() {
232 let mut registry = PluginRegistry::new();
233 registry.register(Box::new(MockPlugin::new("p1"))).unwrap();
234
235 let result = registry.execute("p1", "hello").unwrap();
236 assert!(result.success);
237 assert_eq!(result.output, "Processed: hello");
238 }
239
240 #[test]
241 fn test_registry_execute_not_found() {
242 let mut registry = PluginRegistry::new();
243 assert!(registry.execute("nonexistent", "test").is_err());
244 }
245
246 #[test]
247 fn test_registry_unregister() {
248 let mut registry = PluginRegistry::new();
249 registry.register(Box::new(MockPlugin::new("p1"))).unwrap();
250 assert!(registry.get("p1").is_some());
251
252 registry.unregister("p1").unwrap();
253 assert!(registry.get("p1").is_none());
254 }
255}