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}