1use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct PluginMetadata {
11 pub name: String,
12 pub version: String,
13 pub description: String,
14 pub author: String,
15}
16
17impl PluginMetadata {
18 pub fn new(
19 name: impl Into<String>,
20 version: impl Into<String>,
21 description: impl Into<String>,
22 ) -> Self {
23 Self {
24 name: name.into(),
25 version: version.into(),
26 description: description.into(),
27 author: String::new(),
28 }
29 }
30
31 pub fn author(mut self, author: impl Into<String>) -> Self {
32 self.author = author.into();
33 self
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub enum ExtensionPoint {
40 Widget,
41 Renderer,
42 Layout,
43 Theme,
44 InputHandler,
45 Custom(String),
46}
47
48#[derive(Debug, Clone)]
50pub struct Extension {
51 pub point: ExtensionPoint,
52 pub name: String,
53 pub description: String,
54}
55
56pub trait Plugin: std::fmt::Debug {
58 fn metadata(&self) -> PluginMetadata;
60
61 fn on_load(&mut self) -> Result<(), String>;
63
64 fn on_unload(&mut self) -> Result<(), String>;
66
67 fn extensions(&self) -> Vec<Extension>;
69
70 fn handle_action(
72 &mut self,
73 action: &str,
74 params: &serde_json::Value,
75 ) -> Result<serde_json::Value, String>;
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum PluginState {
81 Registered,
82 Loaded,
83 Failed,
84 Unloaded,
85}
86
87struct PluginEntry {
89 plugin: Box<dyn Plugin>,
90 state: PluginState,
91}
92
93impl std::fmt::Debug for PluginEntry {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("PluginEntry")
96 .field("metadata", &self.plugin.metadata())
97 .field("state", &self.state)
98 .finish()
99 }
100}
101
102pub struct PluginManager {
104 plugins: HashMap<String, PluginEntry>,
105}
106
107impl PluginManager {
108 pub fn new() -> Self {
109 Self {
110 plugins: HashMap::new(),
111 }
112 }
113
114 pub fn register(&mut self, plugin: Box<dyn Plugin>) -> Result<(), String> {
116 let meta = plugin.metadata();
117 if self.plugins.contains_key(&meta.name) {
118 return Err(format!("Plugin '{}' already registered", meta.name));
119 }
120 self.plugins.insert(
121 meta.name.clone(),
122 PluginEntry {
123 plugin,
124 state: PluginState::Registered,
125 },
126 );
127 Ok(())
128 }
129
130 pub fn load(&mut self, name: &str) -> Result<(), String> {
132 let entry = self
133 .plugins
134 .get_mut(name)
135 .ok_or_else(|| format!("Plugin '{name}' not found"))?;
136
137 match entry.plugin.on_load() {
138 Ok(()) => {
139 entry.state = PluginState::Loaded;
140 Ok(())
141 }
142 Err(e) => {
143 entry.state = PluginState::Failed;
144 Err(e)
145 }
146 }
147 }
148
149 pub fn unload(&mut self, name: &str) -> Result<(), String> {
151 let entry = self
152 .plugins
153 .get_mut(name)
154 .ok_or_else(|| format!("Plugin '{name}' not found"))?;
155
156 entry.plugin.on_unload()?;
157 entry.state = PluginState::Unloaded;
158 Ok(())
159 }
160
161 pub fn remove(&mut self, name: &str) -> Result<(), String> {
163 if self.plugins.remove(name).is_none() {
164 return Err(format!("Plugin '{name}' not found"));
165 }
166 Ok(())
167 }
168
169 pub fn state(&self, name: &str) -> Option<PluginState> {
171 self.plugins.get(name).map(|e| e.state)
172 }
173
174 pub fn list(&self) -> Vec<String> {
176 self.plugins.keys().cloned().collect()
177 }
178
179 pub fn extensions_for(&self, point: &ExtensionPoint) -> Vec<Extension> {
181 self.plugins
182 .values()
183 .filter(|e| e.state == PluginState::Loaded)
184 .flat_map(|e| e.plugin.extensions())
185 .filter(|ext| &ext.point == point)
186 .collect()
187 }
188
189 pub fn dispatch(
191 &mut self,
192 plugin_name: &str,
193 action: &str,
194 params: &serde_json::Value,
195 ) -> Result<serde_json::Value, String> {
196 let entry = self
197 .plugins
198 .get_mut(plugin_name)
199 .ok_or_else(|| format!("Plugin '{plugin_name}' not found"))?;
200
201 if entry.state != PluginState::Loaded {
202 return Err(format!("Plugin '{plugin_name}' is not loaded"));
203 }
204
205 entry.plugin.handle_action(action, params)
206 }
207
208 pub fn count(&self) -> usize {
209 self.plugins.len()
210 }
211}
212
213impl Default for PluginManager {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use serde_json::json;
223
224 #[derive(Debug)]
225 struct TestPlugin {
226 loaded: bool,
227 }
228
229 impl TestPlugin {
230 fn new() -> Self {
231 Self { loaded: false }
232 }
233 }
234
235 impl Plugin for TestPlugin {
236 fn metadata(&self) -> PluginMetadata {
237 PluginMetadata::new("test-plugin", "1.0.0", "A test plugin")
238 }
239
240 fn on_load(&mut self) -> Result<(), String> {
241 self.loaded = true;
242 Ok(())
243 }
244
245 fn on_unload(&mut self) -> Result<(), String> {
246 self.loaded = false;
247 Ok(())
248 }
249
250 fn extensions(&self) -> Vec<Extension> {
251 vec![Extension {
252 point: ExtensionPoint::Widget,
253 name: "custom-widget".into(),
254 description: "A custom widget".into(),
255 }]
256 }
257
258 fn handle_action(
259 &mut self,
260 action: &str,
261 _params: &serde_json::Value,
262 ) -> Result<serde_json::Value, String> {
263 match action {
264 "ping" => Ok(json!("pong")),
265 _ => Err(format!("Unknown action: {action}")),
266 }
267 }
268 }
269
270 #[derive(Debug)]
271 struct FailPlugin;
272
273 impl Plugin for FailPlugin {
274 fn metadata(&self) -> PluginMetadata {
275 PluginMetadata::new("fail-plugin", "0.1.0", "Always fails to load")
276 }
277 fn on_load(&mut self) -> Result<(), String> {
278 Err("load error".into())
279 }
280 fn on_unload(&mut self) -> Result<(), String> {
281 Ok(())
282 }
283 fn extensions(&self) -> Vec<Extension> {
284 vec![]
285 }
286 fn handle_action(
287 &mut self,
288 _action: &str,
289 _params: &serde_json::Value,
290 ) -> Result<serde_json::Value, String> {
291 Err("not loaded".into())
292 }
293 }
294
295 #[test]
296 fn register_plugin() {
297 let mut mgr = PluginManager::new();
298 assert!(mgr.register(Box::new(TestPlugin::new())).is_ok());
299 assert_eq!(mgr.count(), 1);
300 }
301
302 #[test]
303 fn duplicate_register_fails() {
304 let mut mgr = PluginManager::new();
305 mgr.register(Box::new(TestPlugin::new())).unwrap();
306 assert!(mgr.register(Box::new(TestPlugin::new())).is_err());
307 }
308
309 #[test]
310 fn load_plugin() {
311 let mut mgr = PluginManager::new();
312 mgr.register(Box::new(TestPlugin::new())).unwrap();
313 mgr.load("test-plugin").unwrap();
314 assert_eq!(mgr.state("test-plugin"), Some(PluginState::Loaded));
315 }
316
317 #[test]
318 fn load_failure() {
319 let mut mgr = PluginManager::new();
320 mgr.register(Box::new(FailPlugin)).unwrap();
321 assert!(mgr.load("fail-plugin").is_err());
322 assert_eq!(mgr.state("fail-plugin"), Some(PluginState::Failed));
323 }
324
325 #[test]
326 fn unload_plugin() {
327 let mut mgr = PluginManager::new();
328 mgr.register(Box::new(TestPlugin::new())).unwrap();
329 mgr.load("test-plugin").unwrap();
330 mgr.unload("test-plugin").unwrap();
331 assert_eq!(mgr.state("test-plugin"), Some(PluginState::Unloaded));
332 }
333
334 #[test]
335 fn remove_plugin() {
336 let mut mgr = PluginManager::new();
337 mgr.register(Box::new(TestPlugin::new())).unwrap();
338 mgr.remove("test-plugin").unwrap();
339 assert_eq!(mgr.count(), 0);
340 }
341
342 #[test]
343 fn dispatch_action() {
344 let mut mgr = PluginManager::new();
345 mgr.register(Box::new(TestPlugin::new())).unwrap();
346 mgr.load("test-plugin").unwrap();
347 let result = mgr.dispatch("test-plugin", "ping", &json!(null)).unwrap();
348 assert_eq!(result, json!("pong"));
349 }
350
351 #[test]
352 fn dispatch_to_unloaded_fails() {
353 let mut mgr = PluginManager::new();
354 mgr.register(Box::new(TestPlugin::new())).unwrap();
355 assert!(mgr.dispatch("test-plugin", "ping", &json!(null)).is_err());
356 }
357
358 #[test]
359 fn extensions_listing() {
360 let mut mgr = PluginManager::new();
361 mgr.register(Box::new(TestPlugin::new())).unwrap();
362 mgr.load("test-plugin").unwrap();
363 let exts = mgr.extensions_for(&ExtensionPoint::Widget);
364 assert_eq!(exts.len(), 1);
365 assert_eq!(exts[0].name, "custom-widget");
366 }
367
368 #[test]
369 fn extensions_only_loaded() {
370 let mut mgr = PluginManager::new();
371 mgr.register(Box::new(TestPlugin::new())).unwrap();
372 let exts = mgr.extensions_for(&ExtensionPoint::Widget);
374 assert_eq!(exts.len(), 0);
375 }
376
377 #[test]
378 fn plugin_metadata_builder() {
379 let meta = PluginMetadata::new("my-plugin", "2.0.0", "My plugin").author("Test Author");
380 assert_eq!(meta.author, "Test Author");
381 }
382}