Skip to main content

dynamic_cli/plugin/builtin/
config.rs

1//! Standalone `ConfigPlugin` (feature-gated).
2//!
3//! Contributes two handlers over the application's own loaded
4//! [`CommandsConfig`] — `config_show` (display it as YAML) and
5//! `config_validate` (re-run schema validation without restarting the
6//! application) — following the same `Option<CommandsConfig>`
7//! attachment pattern as [`SystemPlugin::with_config`][crate::plugin::system::SystemPlugin::with_config].
8//!
9//! Only available with the `config-plugin` feature.
10
11use crate::config::{validate_config, CommandsConfig};
12use crate::context::ExecutionContext;
13use crate::executor::CommandHandler;
14use crate::parser::ParsedArgs;
15use crate::plugin::Plugin;
16use crate::Result;
17
18/// Standalone plugin providing `config` commands: `show` and `validate`.
19///
20/// Both handlers operate on the same [`CommandsConfig`] the application
21/// attaches via [`with_config`][Self::with_config] — there is no
22/// separate config-loading logic here, only display and re-validation
23/// of what the application already loaded.
24///
25/// # YAML config
26///
27/// ```yaml
28/// commands:
29///   - name: config-show
30///     implementation: config_show
31///     description: "Show the loaded configuration"
32///     required: false
33///     arguments: []
34///     options: []
35///   - name: config-validate
36///     implementation: config_validate
37///     description: "Re-validate the loaded configuration"
38///     required: false
39///     arguments: []
40///     options: []
41/// ```
42///
43/// # Example
44///
45/// ```
46/// use dynamic_cli::plugin::{ConfigPlugin, Plugin};
47///
48/// let plugin = ConfigPlugin::new();
49/// assert_eq!(plugin.name(), "config");
50///
51/// let handlers = plugin.handlers();
52/// assert_eq!(handlers.len(), 2);
53/// ```
54pub struct ConfigPlugin {
55    /// The application's own config, attached via [`with_config`][Self::with_config].
56    config: Option<CommandsConfig>,
57}
58
59impl ConfigPlugin {
60    /// Create a new `ConfigPlugin` with no config attached.
61    ///
62    /// # Example
63    ///
64    /// ```
65    /// use dynamic_cli::plugin::{ConfigPlugin, Plugin};
66    ///
67    /// let plugin = ConfigPlugin::new();
68    /// assert_eq!(plugin.name(), "config");
69    /// ```
70    pub fn new() -> Self {
71        Self { config: None }
72    }
73
74    /// Attach the application's config so `config_show`/`config_validate`
75    /// have something to operate on.
76    ///
77    /// Call this yourself before
78    /// [`register_plugin`][crate::builder::CliBuilder::register_plugin] —
79    /// `CliBuilder::build()` does not attach config to plugins on its own.
80    ///
81    /// # Example
82    ///
83    /// ```
84    /// use dynamic_cli::plugin::{ConfigPlugin, Plugin};
85    /// use dynamic_cli::config::schema::{CommandsConfig, Metadata};
86    ///
87    /// let config = CommandsConfig {
88    ///     metadata: Metadata {
89    ///         version: "1.0.0".to_string(),
90    ///         prompt: "myapp".to_string(),
91    ///         prompt_suffix: " > ".to_string(),
92    ///     },
93    ///     commands: vec![],
94    ///     global_options: vec![],
95    /// };
96    ///
97    /// let plugin = ConfigPlugin::new().with_config(config);
98    /// assert_eq!(plugin.name(), "config");
99    /// ```
100    pub fn with_config(mut self, config: CommandsConfig) -> Self {
101        self.config = Some(config);
102        self
103    }
104}
105
106impl Default for ConfigPlugin {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl Plugin for ConfigPlugin {
113    fn name(&self) -> &str {
114        "config"
115    }
116
117    fn version(&self) -> &str {
118        env!("CARGO_PKG_VERSION")
119    }
120
121    fn description(&self) -> &str {
122        "Show/validate the loaded YAML config, without restarting (feature-gated, #47)"
123    }
124
125    fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
126        vec![
127            (
128                "config_show".to_string(),
129                Box::new(ConfigShowHandler {
130                    config: self.config.clone(),
131                }),
132            ),
133            (
134                "config_validate".to_string(),
135                Box::new(ConfigValidateHandler {
136                    config: self.config.clone(),
137                }),
138            ),
139        ]
140    }
141}
142
143/// Handler for `config_show` — prints the loaded config as YAML.
144struct ConfigShowHandler {
145    config: Option<CommandsConfig>,
146}
147
148impl CommandHandler for ConfigShowHandler {
149    fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
150        match &self.config {
151            Some(cfg) => match serde_yaml::to_string(cfg) {
152                Ok(yaml) => println!("{yaml}"),
153                Err(e) => println!("Failed to render config as YAML: {e}"),
154            },
155            None => println!("No configuration attached to this application."),
156        }
157        Ok(())
158    }
159}
160
161/// Handler for `config_validate` — re-runs schema validation on the
162/// loaded config, without restarting the application.
163struct ConfigValidateHandler {
164    config: Option<CommandsConfig>,
165}
166
167impl CommandHandler for ConfigValidateHandler {
168    fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
169        match &self.config {
170            Some(cfg) => match validate_config(cfg) {
171                Ok(()) => println!("Configuration is valid."),
172                Err(e) => println!("Configuration is invalid: {e}"),
173            },
174            None => println!("No configuration attached to this application."),
175        }
176        Ok(())
177    }
178}
179
180// ============================================================================
181// Tests
182// ============================================================================
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::config::schema::Metadata;
188    use std::any::Any;
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    fn test_config() -> CommandsConfig {
203        CommandsConfig {
204            metadata: Metadata {
205                version: "2.0.0".to_string(),
206                prompt: "testapp".to_string(),
207                prompt_suffix: " > ".to_string(),
208            },
209            commands: vec![],
210            global_options: vec![],
211        }
212    }
213
214    #[test]
215    fn test_config_plugin_metadata() {
216        let p = ConfigPlugin::new();
217        assert_eq!(p.name(), "config");
218        assert!(!p.version().is_empty());
219        assert!(!p.description().is_empty());
220    }
221
222    #[test]
223    fn test_config_plugin_default() {
224        let p = ConfigPlugin::default();
225        assert_eq!(p.name(), "config");
226    }
227
228    #[test]
229    fn test_config_plugin_handler_names() {
230        let handlers = ConfigPlugin::new().handlers();
231        assert_eq!(handlers.len(), 2);
232        let names: Vec<&str> = handlers.iter().map(|(n, _)| n.as_str()).collect();
233        assert!(names.contains(&"config_show"));
234        assert!(names.contains(&"config_validate"));
235    }
236
237    #[test]
238    fn test_config_plugin_with_config() {
239        let plugin = ConfigPlugin::new().with_config(test_config());
240        assert!(plugin.config.is_some());
241        assert_eq!(plugin.config.unwrap().metadata.version, "2.0.0");
242    }
243
244    #[test]
245    fn test_config_show_executes_with_config() {
246        let plugin = ConfigPlugin::new().with_config(test_config());
247        let handlers = plugin.handlers();
248        let (name, handler) = &handlers[0];
249        assert_eq!(name, "config_show");
250        let mut ctx = TestContext;
251        assert!(handler
252            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
253            .is_ok());
254    }
255
256    #[test]
257    fn test_config_show_executes_without_config() {
258        let handlers = ConfigPlugin::new().handlers();
259        let (_, handler) = &handlers[0];
260        let mut ctx = TestContext;
261        assert!(handler
262            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
263            .is_ok());
264    }
265
266    #[test]
267    fn test_config_validate_executes_with_valid_config() {
268        let plugin = ConfigPlugin::new().with_config(test_config());
269        let handlers = plugin.handlers();
270        let (name, handler) = &handlers[1];
271        assert_eq!(name, "config_validate");
272        let mut ctx = TestContext;
273        assert!(handler
274            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
275            .is_ok());
276    }
277
278    #[test]
279    fn test_config_validate_executes_without_config() {
280        let handlers = ConfigPlugin::new().handlers();
281        let (_, handler) = &handlers[1];
282        let mut ctx = TestContext;
283        assert!(handler
284            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
285            .is_ok());
286    }
287
288    #[test]
289    fn test_config_plugin_is_send_sync() {
290        fn assert_send_sync<T: Send + Sync>(_: T) {}
291        assert_send_sync(ConfigPlugin::new());
292    }
293}