Skip to main content

dynamic_cli/plugin/builtin/
help.rs

1//! Standalone `HelpPlugin`.
2//!
3//! Contributes the same `system_help` handler as [`SystemPlugin`], reusing
4//! [`SystemHelpHandler`]'s logic internally — no duplicated logic (#44 /
5//! DD-025).
6//!
7//! [`SystemPlugin`]: crate::plugin::system::SystemPlugin
8//! [`SystemHelpHandler`]: crate::plugin::system::SystemHelpHandler
9
10use crate::config::schema::CommandsConfig;
11use crate::executor::CommandHandler;
12use crate::plugin::system::SystemHelpHandler;
13use crate::plugin::Plugin;
14
15/// Standalone builtin providing only the `help` command.
16///
17/// Use this instead of [`SystemPlugin`][crate::builtin::system::SystemPlugin]
18/// when an application wants `help` without also registering `version` and
19/// `exit`.
20///
21/// # YAML config
22///
23/// ```yaml
24/// commands:
25///   - name: help
26///     implementation: system_help
27///     description: "Show help"
28///     aliases: ["h", "?"]
29///     required: false
30///     arguments: []
31///     options: []
32/// ```
33///
34/// # Example
35///
36/// ```
37/// use dynamic_cli::plugin::{HelpPlugin, Plugin};
38///
39/// let builtin = HelpPlugin::new();
40/// assert_eq!(builtin.name(), "help");
41///
42/// let handlers = builtin.handlers();
43/// assert_eq!(handlers.len(), 1);
44/// assert_eq!(handlers[0].0, "system_help");
45/// ```
46pub struct HelpPlugin {
47    /// Application config, needed to render help content.
48    config: Option<CommandsConfig>,
49}
50
51impl HelpPlugin {
52    /// Create a new `HelpPlugin` with no config attached.
53    ///
54    /// # Example
55    ///
56    /// ```
57    /// use dynamic_cli::plugin::{HelpPlugin, Plugin};
58    ///
59    /// let builtin = HelpPlugin::new();
60    /// assert_eq!(builtin.name(), "help");
61    /// ```
62    pub fn new() -> Self {
63        Self { config: None }
64    }
65
66    /// Attach a config so the handler can render app/command help.
67    ///
68    /// Call this yourself before
69    /// [`register_plugin`][crate::builder::CliBuilder::register_plugin] —
70    /// `CliBuilder::build()` does not attach config to plugins on its own.
71    ///
72    /// # Example
73    ///
74    /// ```
75    /// use dynamic_cli::plugin::{HelpPlugin, Plugin};
76    /// use dynamic_cli::config::schema::{CommandsConfig, Metadata};
77    ///
78    /// let config = CommandsConfig {
79    ///     metadata: Metadata {
80    ///         version: "1.0.0".to_string(),
81    ///         prompt: "myapp".to_string(),
82    ///         prompt_suffix: " > ".to_string(),
83    ///     },
84    ///     commands: vec![],
85    ///     global_options: vec![],
86    /// };
87    ///
88    /// let builtin = HelpPlugin::new().with_config(config);
89    /// assert_eq!(builtin.name(), "help");
90    /// ```
91    pub fn with_config(mut self, config: CommandsConfig) -> Self {
92        self.config = Some(config);
93        self
94    }
95}
96
97impl Default for HelpPlugin {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl Plugin for HelpPlugin {
104    fn name(&self) -> &str {
105        "help"
106    }
107
108    fn version(&self) -> &str {
109        env!("CARGO_PKG_VERSION")
110    }
111
112    fn description(&self) -> &str {
113        "Standalone help command (split out of SystemPlugin, #44)"
114    }
115
116    fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
117        vec![(
118            "system_help".to_string(),
119            Box::new(SystemHelpHandler::new(self.config.clone())),
120        )]
121    }
122}
123
124// ============================================================================
125// Tests
126// ============================================================================
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::config::schema::{CommandsConfig, Metadata};
132    use crate::context::ExecutionContext;
133    use crate::parser::ParsedArgs;
134    use std::any::Any;
135    use std::collections::HashMap;
136
137    #[derive(Default)]
138    struct TestContext;
139
140    impl ExecutionContext for TestContext {
141        fn as_any(&self) -> &dyn Any {
142            self
143        }
144        fn as_any_mut(&mut self) -> &mut dyn Any {
145            self
146        }
147    }
148
149    fn test_config() -> CommandsConfig {
150        CommandsConfig {
151            metadata: Metadata {
152                version: "2.0.0".to_string(),
153                prompt: "testapp".to_string(),
154                prompt_suffix: " > ".to_string(),
155            },
156            commands: vec![],
157            global_options: vec![],
158        }
159    }
160
161    #[test]
162    fn test_help_plugin_metadata() {
163        let p = HelpPlugin::new();
164        assert_eq!(p.name(), "help");
165        assert!(!p.version().is_empty());
166        assert!(!p.description().is_empty());
167    }
168
169    #[test]
170    fn test_help_plugin_default() {
171        let p = HelpPlugin::default();
172        assert_eq!(p.name(), "help");
173    }
174
175    #[test]
176    fn test_help_plugin_handler_name() {
177        let handlers = HelpPlugin::new().handlers();
178        assert_eq!(handlers.len(), 1);
179        assert_eq!(handlers[0].0, "system_help");
180    }
181
182    #[test]
183    fn test_help_plugin_with_config() {
184        let plugin = HelpPlugin::new().with_config(test_config());
185        assert!(plugin.config.is_some());
186        assert_eq!(plugin.config.unwrap().metadata.version, "2.0.0");
187    }
188
189    #[test]
190    fn test_help_handler_executes_with_config() {
191        let plugin = HelpPlugin::new().with_config(test_config());
192        let handlers = plugin.handlers();
193        let (_, handler) = &handlers[0];
194        let mut ctx = TestContext;
195        assert!(handler
196            .execute(&mut ctx, &ParsedArgs::from_scalars(HashMap::new()))
197            .is_ok());
198    }
199
200    #[test]
201    fn test_help_handler_executes_without_config() {
202        let handlers = HelpPlugin::new().handlers();
203        let (_, handler) = &handlers[0];
204        let mut ctx = TestContext;
205        assert!(handler
206            .execute(&mut ctx, &ParsedArgs::from_scalars(HashMap::new()))
207            .is_ok());
208    }
209
210    #[test]
211    fn test_help_plugin_is_send_sync() {
212        fn assert_send_sync<T: Send + Sync>(_: T) {}
213        assert_send_sync(HelpPlugin::new());
214    }
215}