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