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    ///     directives: vec![],
96    /// };
97    ///
98    /// let plugin = ConfigPlugin::new().with_config(config);
99    /// assert_eq!(plugin.name(), "config");
100    /// ```
101    pub fn with_config(mut self, config: CommandsConfig) -> Self {
102        self.config = Some(config);
103        self
104    }
105}
106
107impl Default for ConfigPlugin {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113impl Plugin for ConfigPlugin {
114    fn name(&self) -> &str {
115        "config"
116    }
117
118    fn version(&self) -> &str {
119        env!("CARGO_PKG_VERSION")
120    }
121
122    fn description(&self) -> &str {
123        "Show/validate the loaded YAML config, without restarting (feature-gated, #47)"
124    }
125
126    fn handlers(&self) -> Vec<(String, Box<dyn CommandHandler>)> {
127        vec![
128            (
129                "config_show".to_string(),
130                Box::new(ConfigShowHandler {
131                    config: self.config.clone(),
132                }),
133            ),
134            (
135                "config_validate".to_string(),
136                Box::new(ConfigValidateHandler {
137                    config: self.config.clone(),
138                }),
139            ),
140        ]
141    }
142}
143
144/// Handler for `config_show` — prints the loaded config as YAML.
145struct ConfigShowHandler {
146    config: Option<CommandsConfig>,
147}
148
149impl CommandHandler for ConfigShowHandler {
150    fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
151        match &self.config {
152            Some(cfg) => match serde_yaml::to_string(cfg) {
153                Ok(yaml) => println!("{yaml}"),
154                Err(e) => println!("Failed to render config as YAML: {e}"),
155            },
156            None => println!("No configuration attached to this application."),
157        }
158        Ok(())
159    }
160}
161
162/// Handler for `config_validate` — re-runs schema validation on the
163/// loaded config, without restarting the application.
164struct ConfigValidateHandler {
165    config: Option<CommandsConfig>,
166}
167
168impl CommandHandler for ConfigValidateHandler {
169    fn execute(&self, _ctx: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
170        match &self.config {
171            Some(cfg) => match validate_config(cfg) {
172                Ok(()) => println!("Configuration is valid."),
173                Err(e) => println!("Configuration is invalid: {e}"),
174            },
175            None => println!("No configuration attached to this application."),
176        }
177        Ok(())
178    }
179}
180
181// ============================================================================
182// Tests
183// ============================================================================
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::config::schema::Metadata;
189    use std::any::Any;
190
191    #[derive(Default)]
192    struct TestContext;
193
194    impl ExecutionContext for TestContext {
195        fn as_any(&self) -> &dyn Any {
196            self
197        }
198        fn as_any_mut(&mut self) -> &mut dyn Any {
199            self
200        }
201    }
202
203    fn test_config() -> CommandsConfig {
204        CommandsConfig {
205            metadata: Metadata {
206                version: "2.0.0".to_string(),
207                prompt: "testapp".to_string(),
208                prompt_suffix: " > ".to_string(),
209            },
210            commands: vec![],
211            global_options: vec![],
212            directives: vec![],
213        }
214    }
215
216    #[test]
217    fn test_config_plugin_metadata() {
218        let p = ConfigPlugin::new();
219        assert_eq!(p.name(), "config");
220        assert!(!p.version().is_empty());
221        assert!(!p.description().is_empty());
222    }
223
224    #[test]
225    fn test_config_plugin_default() {
226        let p = ConfigPlugin::default();
227        assert_eq!(p.name(), "config");
228    }
229
230    #[test]
231    fn test_config_plugin_handler_names() {
232        let handlers = ConfigPlugin::new().handlers();
233        assert_eq!(handlers.len(), 2);
234        let names: Vec<&str> = handlers.iter().map(|(n, _)| n.as_str()).collect();
235        assert!(names.contains(&"config_show"));
236        assert!(names.contains(&"config_validate"));
237    }
238
239    #[test]
240    fn test_config_plugin_with_config() {
241        let plugin = ConfigPlugin::new().with_config(test_config());
242        assert!(plugin.config.is_some());
243        assert_eq!(plugin.config.unwrap().metadata.version, "2.0.0");
244    }
245
246    #[test]
247    fn test_config_show_executes_with_config() {
248        let plugin = ConfigPlugin::new().with_config(test_config());
249        let handlers = plugin.handlers();
250        let (name, handler) = &handlers[0];
251        assert_eq!(name, "config_show");
252        let mut ctx = TestContext;
253        assert!(handler
254            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
255            .is_ok());
256    }
257
258    #[test]
259    fn test_config_show_executes_without_config() {
260        let handlers = ConfigPlugin::new().handlers();
261        let (_, handler) = &handlers[0];
262        let mut ctx = TestContext;
263        assert!(handler
264            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
265            .is_ok());
266    }
267
268    #[test]
269    fn test_config_validate_executes_with_valid_config() {
270        let plugin = ConfigPlugin::new().with_config(test_config());
271        let handlers = plugin.handlers();
272        let (name, handler) = &handlers[1];
273        assert_eq!(name, "config_validate");
274        let mut ctx = TestContext;
275        assert!(handler
276            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
277            .is_ok());
278    }
279
280    #[test]
281    fn test_config_validate_executes_without_config() {
282        let handlers = ConfigPlugin::new().handlers();
283        let (_, handler) = &handlers[1];
284        let mut ctx = TestContext;
285        assert!(handler
286            .execute(&mut ctx, &ParsedArgs::from_scalars(Default::default()))
287            .is_ok());
288    }
289
290    #[test]
291    fn test_config_plugin_is_send_sync() {
292        fn assert_send_sync<T: Send + Sync>(_: T) {}
293        assert_send_sync(ConfigPlugin::new());
294    }
295}