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//!
6//! [`SystemPlugin`]: crate::plugin::system::SystemPlugin
7//! [`SystemHelpHandler`]: crate::plugin::system::SystemHelpHandler
8
9use crate::config::schema::CommandsConfig;
10use crate::executor::CommandHandler;
11use crate::plugin::system::SystemHelpHandler;
12use crate::plugin::Plugin;
13
14/// Standalone builtin providing only the `help` command.
15///
16/// Use this instead of [`SystemPlugin`][crate::plugin::system::SystemPlugin]
17/// when an application wants `help` without also registering `version` and
18/// `exit`.
19///
20/// # YAML config
21///
22/// ```yaml
23/// commands:
24///   - name: help
25///     implementation: system_help
26///     description: "Show help"
27///     aliases: ["h", "?"]
28///     required: false
29///     arguments: []
30///     options: []
31/// ```
32///
33/// # Example
34///
35/// ```
36/// use dynamic_cli::plugin::{HelpPlugin, Plugin};
37///
38/// let builtin = HelpPlugin::new();
39/// assert_eq!(builtin.name(), "help");
40///
41/// let handlers = builtin.handlers();
42/// assert_eq!(handlers.len(), 1);
43/// assert_eq!(handlers[0].0, "system_help");
44/// ```
45pub struct HelpPlugin {
46    /// Application config, needed to render help content.
47    config: Option<CommandsConfig>,
48}
49
50impl HelpPlugin {
51    /// Create a new `HelpPlugin` with no config attached.
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// use dynamic_cli::plugin::{HelpPlugin, Plugin};
57    ///
58    /// let builtin = HelpPlugin::new();
59    /// assert_eq!(builtin.name(), "help");
60    /// ```
61    pub fn new() -> Self {
62        Self { config: None }
63    }
64
65    /// Attach a config so the handler can render app/command help.
66    ///
67    /// Call this yourself before
68    /// [`register_plugin`][crate::builder::CliBuilder::register_plugin] —
69    /// `CliBuilder::build()` does not attach config to plugins on its own.
70    ///
71    /// # Example
72    ///
73    /// ```
74    /// use dynamic_cli::plugin::{HelpPlugin, Plugin};
75    /// use dynamic_cli::config::schema::{CommandsConfig, Metadata};
76    ///
77    /// let config = CommandsConfig {
78    ///     metadata: Metadata {
79    ///         version: "1.0.0".to_string(),
80    ///         prompt: "myapp".to_string(),
81    ///         prompt_suffix: " > ".to_string(),
82    ///     },
83    ///     commands: vec![],
84    ///     global_options: vec![],
85    ///     directives: 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            directives: vec![],
159        }
160    }
161
162    #[test]
163    fn test_help_plugin_metadata() {
164        let p = HelpPlugin::new();
165        assert_eq!(p.name(), "help");
166        assert!(!p.version().is_empty());
167        assert!(!p.description().is_empty());
168    }
169
170    #[test]
171    fn test_help_plugin_default() {
172        let p = HelpPlugin::default();
173        assert_eq!(p.name(), "help");
174    }
175
176    #[test]
177    fn test_help_plugin_handler_name() {
178        let handlers = HelpPlugin::new().handlers();
179        assert_eq!(handlers.len(), 1);
180        assert_eq!(handlers[0].0, "system_help");
181    }
182
183    #[test]
184    fn test_help_plugin_with_config() {
185        let plugin = HelpPlugin::new().with_config(test_config());
186        assert!(plugin.config.is_some());
187        assert_eq!(plugin.config.unwrap().metadata.version, "2.0.0");
188    }
189
190    #[test]
191    fn test_help_handler_executes_with_config() {
192        let plugin = HelpPlugin::new().with_config(test_config());
193        let handlers = plugin.handlers();
194        let (_, handler) = &handlers[0];
195        let mut ctx = TestContext;
196        assert!(handler
197            .execute(&mut ctx, &ParsedArgs::from_scalars(HashMap::new()))
198            .is_ok());
199    }
200
201    #[test]
202    fn test_help_handler_executes_without_config() {
203        let handlers = HelpPlugin::new().handlers();
204        let (_, handler) = &handlers[0];
205        let mut ctx = TestContext;
206        assert!(handler
207            .execute(&mut ctx, &ParsedArgs::from_scalars(HashMap::new()))
208            .is_ok());
209    }
210
211    #[test]
212    fn test_help_plugin_is_send_sync() {
213        fn assert_send_sync<T: Send + Sync>(_: T) {}
214        assert_send_sync(HelpPlugin::new());
215    }
216}