Skip to main content

agpu/
plugin.rs

1//! Plugin system — loadable extensions for widgets and renderers.
2//!
3//! Provides [`Plugin`] trait, [`PluginManager`], and [`PluginMetadata`]
4//! for discovering and managing runtime plugin extensions.
5
6use std::collections::HashMap;
7
8/// Metadata describing a plugin.
9#[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/// Extension point that a plugin can provide.
38#[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/// A registered extension from a plugin.
49#[derive(Debug, Clone)]
50pub struct Extension {
51    pub point: ExtensionPoint,
52    pub name: String,
53    pub description: String,
54}
55
56/// Trait that all plugins must implement.
57pub trait Plugin: std::fmt::Debug {
58    /// Return metadata for this plugin.
59    fn metadata(&self) -> PluginMetadata;
60
61    /// Called when the plugin is loaded.
62    fn on_load(&mut self) -> Result<(), String>;
63
64    /// Called when the plugin is unloaded.
65    fn on_unload(&mut self) -> Result<(), String>;
66
67    /// Return the extensions this plugin provides.
68    fn extensions(&self) -> Vec<Extension>;
69
70    /// Handle a named action dispatched to this plugin.
71    fn handle_action(
72        &mut self,
73        action: &str,
74        params: &serde_json::Value,
75    ) -> Result<serde_json::Value, String>;
76}
77
78/// State of a loaded plugin.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum PluginState {
81    Registered,
82    Loaded,
83    Failed,
84    Unloaded,
85}
86
87/// Entry tracking a plugin inside the manager.
88struct 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
102/// Manages registration, lifecycle, and lookup of plugins.
103pub 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    /// Register a plugin. Does not load it yet.
115    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    /// Load a registered plugin by name.
131    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    /// Unload a loaded plugin by name.
150    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    /// Remove a plugin entirely.
162    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    /// Get the state of a plugin.
170    pub fn state(&self, name: &str) -> Option<PluginState> {
171        self.plugins.get(name).map(|e| e.state)
172    }
173
174    /// List all registered plugin names.
175    pub fn list(&self) -> Vec<String> {
176        self.plugins.keys().cloned().collect()
177    }
178
179    /// Get all extensions of a specific type across loaded plugins.
180    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    /// Dispatch an action to a specific loaded plugin.
190    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        // Not loaded
373        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}