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