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