Skip to main content

dynamic_cli/plugin/
mod.rs

1//! Plugin system for `dynamic-cli`
2//!
3//! This module defines the [`Plugin`] trait, the standard extension mechanism
4//! for `dynamic-cli` applications. A plugin groups related command handlers
5//! under a single unit of deployment with explicit metadata.
6//!
7//! # Design
8//!
9//! The plugin system follows the principle established by DD-001 and DD-002:
10//! - **The YAML config is the sole source of truth** for command definitions.
11//! - **Plugins supply handlers only** — identified by their `implementation`
12//!   name, exactly as [`CliBuilder::register_handler`] does.
13//! - **The framework controls registration** — the plugin declares what it
14//!   provides via [`Plugin::handlers`]; the framework validates and registers.
15//!   The plugin never receives a `&mut CommandRegistry`.
16//!
17//! # Standard plugin
18//!
19//! [`SystemPlugin`] is provided out of the box. It supplies handlers for the
20//! common system commands (`help`, `version`, `exit` / `quit`) that every
21//! application typically needs. Users declare the corresponding commands in
22//! their YAML config and register the plugin with a single call.
23//!
24//! # Example
25//!
26//! ```
27//! use dynamic_cli::plugin::{Plugin, SystemPlugin};
28//! use dynamic_cli::executor::CommandHandler;
29//! use std::collections::HashMap;
30//!
31//! // A minimal plugin supplying one handler
32//! struct GreetPlugin;
33//!
34//! impl Plugin for GreetPlugin {
35//!     fn name(&self) -> &str { "greet" }
36//!     fn version(&self) -> &str { "0.1.0" }
37//!     fn description(&self) -> &str { "Greeting commands" }
38//!
39//!     fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
40//!         struct HelloHandler;
41//!         impl CommandHandler for HelloHandler {
42//!             fn execute(
43//!                 &self,
44//!                 _ctx: &mut dyn dynamic_cli::context::ExecutionContext,
45//!                 args: &HashMap<String, String>,
46//!             ) -> dynamic_cli::Result<()> {
47//!                 let default = "World".to_string();
48//!                 println!("Hello, {}!", args.get("name").unwrap_or(&default));
49//!                 Ok(())
50//!             }
51//!         }
52//!         vec![("greet_hello".to_string(), Box::new(HelloHandler))]
53//!     }
54//! }
55//!
56//! // Verify the trait contract
57//! let plugin = GreetPlugin;
58//! assert_eq!(plugin.name(), "greet");
59//! let handlers = plugin.handlers();
60//! assert_eq!(handlers.len(), 1);
61//! assert_eq!(handlers[0].0, "greet_hello");
62//! ```
63
64use crate::executor::CommandHandler;
65
66// Sub-modules
67pub mod system;
68
69// Sub-module for the WASM loader (feature-gated, added in #23)
70#[cfg(feature = "wasm-plugins")]
71pub mod wasm;
72
73// Re-exports for convenience
74pub use system::SystemPlugin;
75
76// ============================================================================
77// Plugin trait
78// ============================================================================
79
80/// Extension point for grouping related command handlers.
81///
82/// A plugin declares its metadata and the handlers it provides. The framework
83/// validates and registers those handlers into the [`CommandRegistry`] during
84/// [`CliBuilder::build()`]. The plugin never has direct access to the registry.
85///
86/// # Contract
87///
88/// - [`Plugin::handlers`] returns `(implementation_name, handler)` pairs.
89/// - Each `implementation_name` must match the `implementation` field of a
90///   command declared in the YAML config — exactly as with
91///   [`CliBuilder::register_handler`].
92/// - The YAML config remains the sole source of truth for command definitions.
93///   A plugin cannot inject commands that are not declared in the config.
94///
95/// # Object safety
96///
97/// This trait is intentionally object-safe (`dyn Plugin` is valid).
98/// Do not add methods with generic type parameters.
99///
100/// # Thread safety
101///
102/// Implementations must be `Send + Sync`.
103///
104/// # Example
105///
106/// ```
107/// use dynamic_cli::plugin::{Plugin, SystemPlugin};
108/// use dynamic_cli::executor::CommandHandler;
109/// use dynamic_cli::context::ExecutionContext;
110/// use std::collections::HashMap;
111///
112/// struct MyPlugin;
113///
114/// impl Plugin for MyPlugin {
115///     fn name(&self) -> &str { "my-plugin" }
116///     fn version(&self) -> &str { "1.0.0" }
117///     fn description(&self) -> &str { "My custom plugin" }
118///
119///     fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
120///         struct MyHandler;
121///         impl CommandHandler for MyHandler {
122///             fn execute(
123///                 &self,
124///                 _ctx: &mut dyn ExecutionContext,
125///                 _args: &HashMap<String, String>,
126///             ) -> dynamic_cli::Result<()> {
127///                 println!("executed");
128///                 Ok(())
129///             }
130///         }
131///         vec![("my_handler".to_string(), Box::new(MyHandler))]
132///     }
133/// }
134///
135/// // Trait object usage (object-safe)
136/// let plugin: Box<dyn Plugin> = Box::new(MyPlugin);
137/// assert_eq!(plugin.name(), "my-plugin");
138/// assert_eq!(plugin.version(), "1.0.0");
139/// assert_eq!(plugin.handlers().len(), 1);
140/// ```
141pub trait Plugin: Send + Sync {
142    /// Short identifier for this plugin (e.g. `"system"`, `"greet"`).
143    fn name(&self) -> &str;
144
145    /// Semantic version string (e.g. `"1.0.0"`).
146    fn version(&self) -> &str;
147
148    /// Human-readable description of what this plugin provides.
149    fn description(&self) -> &str;
150
151    /// Returns the handlers this plugin contributes.
152    ///
153    /// Each element is `(implementation_name, handler)` where
154    /// `implementation_name` matches the `implementation` field in the YAML
155    /// config for the corresponding command.
156    fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)>;
157}
158
159// ============================================================================
160// Tests — Plugin trait contract and fixture plugins
161// ============================================================================
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::context::ExecutionContext;
167    use crate::Result;
168    use std::any::Any;
169    use std::collections::HashMap;
170
171    // -------------------------------------------------------------------------
172    // Test fixtures (trait-level — no SystemPlugin dependency here)
173    // -------------------------------------------------------------------------
174
175    #[derive(Default)]
176    struct TestContext;
177
178    impl ExecutionContext for TestContext {
179        fn as_any(&self) -> &dyn Any {
180            self
181        }
182        fn as_any_mut(&mut self) -> &mut dyn Any {
183            self
184        }
185    }
186
187    struct EchoPlugin;
188
189    impl Plugin for EchoPlugin {
190        fn name(&self) -> &str {
191            "echo"
192        }
193        fn version(&self) -> &str {
194            "0.1.0"
195        }
196        fn description(&self) -> &str {
197            "Echoes its arguments"
198        }
199
200        fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
201            struct EchoHandler;
202            impl CommandHandler for EchoHandler {
203                fn execute(
204                    &self,
205                    _ctx: &mut dyn ExecutionContext,
206                    args: &HashMap<String, String>,
207                ) -> Result<()> {
208                    for (k, v) in args {
209                        println!("{k}={v}");
210                    }
211                    Ok(())
212                }
213            }
214            vec![("echo_handler".to_string(), Box::new(EchoHandler))]
215        }
216    }
217
218    struct MultiHandlerPlugin;
219
220    impl Plugin for MultiHandlerPlugin {
221        fn name(&self) -> &str {
222            "multi"
223        }
224        fn version(&self) -> &str {
225            "1.0.0"
226        }
227        fn description(&self) -> &str {
228            "Plugin with multiple handlers"
229        }
230
231        fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
232            struct NoopHandler;
233            impl CommandHandler for NoopHandler {
234                fn execute(
235                    &self,
236                    _: &mut dyn ExecutionContext,
237                    _: &HashMap<String, String>,
238                ) -> Result<()> {
239                    Ok(())
240                }
241            }
242            vec![
243                ("multi_alpha".to_string(), Box::new(NoopHandler)),
244                ("multi_beta".to_string(), Box::new(NoopHandler)),
245                ("multi_gamma".to_string(), Box::new(NoopHandler)),
246            ]
247        }
248    }
249
250    struct MetadataPlugin;
251
252    impl Plugin for MetadataPlugin {
253        fn name(&self) -> &str {
254            "acme-analytics"
255        }
256        fn version(&self) -> &str {
257            "3.1.4"
258        }
259        fn description(&self) -> &str {
260            "Analytics commands for Acme Corp"
261        }
262        fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
263            vec![]
264        }
265    }
266
267    // -------------------------------------------------------------------------
268    // Plugin trait — object safety
269    // -------------------------------------------------------------------------
270
271    #[test]
272    fn test_plugin_is_object_safe() {
273        // If this compiles, Plugin is dyn-compatible.
274        let _: Box<dyn Plugin> = Box::new(EchoPlugin);
275    }
276
277    #[test]
278    fn test_plugin_is_send_sync() {
279        fn assert_send_sync<T: Send + Sync>() {}
280        assert_send_sync::<EchoPlugin>();
281        assert_send_sync::<MultiHandlerPlugin>();
282        assert_send_sync::<SystemPlugin>();
283    }
284
285    // -------------------------------------------------------------------------
286    // EchoPlugin — minimal single-handler plugin
287    // -------------------------------------------------------------------------
288
289    #[test]
290    fn test_echo_plugin_metadata() {
291        let p = EchoPlugin;
292        assert_eq!(p.name(), "echo");
293        assert_eq!(p.version(), "0.1.0");
294        assert_eq!(p.description(), "Echoes its arguments");
295    }
296
297    #[test]
298    fn test_echo_plugin_handlers_count() {
299        let handlers = EchoPlugin.handlers();
300        assert_eq!(handlers.len(), 1);
301        assert_eq!(handlers[0].0, "echo_handler");
302    }
303
304    #[test]
305    fn test_echo_handler_executes() {
306        let handlers = EchoPlugin.handlers();
307        let (_, handler) = &handlers[0];
308        let mut ctx = TestContext;
309        let mut args = HashMap::new();
310        args.insert("key".to_string(), "value".to_string());
311        assert!(handler.execute(&mut ctx, &args).is_ok());
312    }
313
314    // -------------------------------------------------------------------------
315    // MultiHandlerPlugin
316    // -------------------------------------------------------------------------
317
318    #[test]
319    fn test_multi_handler_plugin_count() {
320        let handlers = MultiHandlerPlugin.handlers();
321        assert_eq!(handlers.len(), 3);
322        let names: Vec<&str> = handlers.iter().map(|(n, _)| n.as_str()).collect();
323        assert!(names.contains(&"multi_alpha"));
324        assert!(names.contains(&"multi_beta"));
325        assert!(names.contains(&"multi_gamma"));
326    }
327
328    // -------------------------------------------------------------------------
329    // MetadataPlugin
330    // -------------------------------------------------------------------------
331
332    #[test]
333    fn test_metadata_plugin_fields() {
334        let p = MetadataPlugin;
335        assert_eq!(p.name(), "acme-analytics");
336        assert_eq!(p.version(), "3.1.4");
337        assert_eq!(p.description(), "Analytics commands for Acme Corp");
338        assert_eq!(p.handlers().len(), 0);
339    }
340
341    // -------------------------------------------------------------------------
342    // Plugin as trait object — collections
343    // -------------------------------------------------------------------------
344
345    #[test]
346    fn test_plugin_trait_object_in_vec() {
347        let plugins: Vec<Box<dyn Plugin>> = vec![
348            Box::new(EchoPlugin),
349            Box::new(MultiHandlerPlugin),
350            Box::new(MetadataPlugin),
351            Box::new(SystemPlugin::new()),
352        ];
353        assert_eq!(plugins.len(), 4);
354        let names: Vec<&str> = plugins.iter().map(|p| p.name()).collect();
355        assert!(names.contains(&"echo"));
356        assert!(names.contains(&"multi"));
357        assert!(names.contains(&"acme-analytics"));
358        assert!(names.contains(&"system"));
359    }
360
361    #[test]
362    fn test_plugin_handlers_total_count() {
363        let plugins: Vec<Box<dyn Plugin>> =
364            vec![Box::new(EchoPlugin), Box::new(MultiHandlerPlugin)];
365        let total: usize = plugins.iter().map(|p| p.handlers().len()).sum();
366        assert_eq!(total, 4); // 1 + 3
367    }
368}