dynamic_cli/config/
mod.rs

1//! Configuration module
2//!
3//! This module provides functionality for loading, parsing, and validating
4//! YAML/JSON configuration files that define CLI/REPL commands.
5//!
6//! # Module Structure
7//!
8//! - [`schema`]: Data structures for configuration
9//! - [`loader`]: Functions to load configuration files
10//! - [`validator`]: Configuration validation logic
11//!
12//! # Quick Start
13//!
14//! ```no_run
15//! use dynamic_cli::config::{loader::load_config, validator::validate_config};
16//!
17//! // Load configuration from file
18//! let config = load_config("commands.yaml")?;
19//!
20//! // Validate the configuration
21//! validate_config(&config)?;
22//!
23//! println!("Loaded {} commands", config.commands.len());
24//! # Ok::<(), dynamic_cli::error::DynamicCliError>(())
25//! ```
26//!
27//! # Configuration File Format
28//!
29//! ## YAML Example
30//!
31//! ```yaml
32//! metadata:
33//!   version: "1.0.0"
34//!   prompt: "myapp"
35//!   prompt_suffix: " > "
36//!
37//! commands:
38//!   - name: hello
39//!     aliases: [hi]
40//!     description: "Say hello"
41//!     required: false
42//!     arguments:
43//!       - name: name
44//!         arg_type: string
45//!         required: true
46//!         description: "Name to greet"
47//!         validation: []
48//!     options:
49//!       - name: loud
50//!         short: l
51//!         long: loud
52//!         option_type: bool
53//!         required: false
54//!         description: "Use uppercase"
55//!         choices: []
56//!     implementation: "hello_handler"
57//!
58//! global_options: []
59//! ```
60//!
61//! ## JSON Example
62//!
63//! ```json
64//! {
65//!   "metadata": {
66//!     "version": "1.0.0",
67//!     "prompt": "myapp"
68//!   },
69//!   "commands": [
70//!     {
71//!       "name": "hello",
72//!       "aliases": [],
73//!       "description": "Say hello",
74//!       "required": false,
75//!       "arguments": [],
76//!       "options": [],
77//!       "implementation": "hello_handler"
78//!     }
79//!   ],
80//!   "global_options": []
81//! }
82//! ```
83
84// Public submodules
85pub mod loader;
86pub mod schema;
87pub mod validator;
88
89// Re-export commonly used types and functions for convenience
90#[allow(unused_imports)]
91pub use schema::{
92    ArgumentDefinition, ArgumentType, CommandDefinition, CommandsConfig, Metadata,
93    OptionDefinition, ValidationRule,
94};
95
96#[allow(unused_imports)]
97pub use loader::{load_config, load_json, load_yaml};
98
99#[allow(unused_imports)]
100pub use validator::{validate_argument_types, validate_command, validate_config};
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    /// Integration test: Load and validate a complete configuration
107    #[test]
108    fn test_integration_load_and_validate() {
109        let yaml = r#"
110metadata:
111  version: "1.0.0"
112  prompt: "test"
113  prompt_suffix: " > "
114
115commands:
116  - name: process
117    aliases: [proc, p]
118    description: "Process data files"
119    required: true
120    arguments:
121      - name: input
122        arg_type: path
123        required: true
124        description: "Input file path"
125        validation:
126          - must_exist: true
127          - extensions: [csv, tsv]
128      - name: output
129        arg_type: path
130        required: false
131        description: "Output file path"
132        validation: []
133    options:
134      - name: verbose
135        short: v
136        long: verbose
137        option_type: bool
138        required: false
139        description: "Verbose output"
140        choices: []
141      - name: format
142        short: f
143        long: format
144        option_type: string
145        required: false
146        default: "json"
147        description: "Output format"
148        choices: [json, xml, csv]
149    implementation: "process_handler"
150
151  - name: help
152    aliases: [h]
153    description: "Show help information"
154    required: false
155    arguments: []
156    options: []
157    implementation: "help_handler"
158
159global_options:
160  - name: config
161    short: c
162    long: config
163    option_type: path
164    required: false
165    description: "Configuration file"
166    choices: []
167        "#;
168
169        // Load the configuration
170        let config = load_yaml(yaml).unwrap();
171
172        // Validate it
173        validate_config(&config).unwrap();
174
175        // Verify structure
176        assert_eq!(config.metadata.version, "1.0.0");
177        assert_eq!(config.commands.len(), 2);
178        assert_eq!(config.commands[0].name, "process");
179        assert_eq!(config.commands[0].arguments.len(), 2);
180        assert_eq!(config.commands[0].options.len(), 2);
181        assert_eq!(config.global_options.len(), 1);
182    }
183
184    /// Test that validation catches errors in loaded config
185    #[test]
186    fn test_integration_invalid_config() {
187        let yaml = r#"
188metadata:
189  version: "1.0.0"
190  prompt: "test"
191
192commands:
193  - name: cmd1
194    aliases: []
195    description: "Command 1"
196    required: false
197    arguments: []
198    options: []
199    implementation: "handler1"
200  - name: cmd1
201    aliases: []
202    description: "Command 2 with duplicate name"
203    required: false
204    arguments: []
205    options: []
206    implementation: "handler2"
207
208global_options: []
209        "#;
210
211        let config = load_yaml(yaml).unwrap();
212        let result = validate_config(&config);
213
214        // Should fail due to duplicate command name
215        assert!(result.is_err());
216    }
217
218    /// Test loading from JSON format
219    #[test]
220    fn test_integration_json_format() {
221        let json = r#"
222{
223  "metadata": {
224    "version": "2.0.0",
225    "prompt": "myapp",
226    "prompt_suffix": " $ "
227  },
228  "commands": [
229    {
230      "name": "test",
231      "aliases": [],
232      "description": "Test command",
233      "required": false,
234      "arguments": [
235        {
236          "name": "value",
237          "arg_type": "integer",
238          "required": true,
239          "description": "A test value",
240          "validation": [
241            {
242              "min": 0.0,
243              "max": 100.0
244            }
245          ]
246        }
247      ],
248      "options": [],
249      "implementation": "test_handler"
250    }
251  ],
252  "global_options": []
253}
254        "#;
255
256        let config = load_json(json).unwrap();
257        validate_config(&config).unwrap();
258
259        assert_eq!(config.metadata.version, "2.0.0");
260        assert_eq!(
261            config.commands[0].arguments[0].arg_type,
262            ArgumentType::Integer
263        );
264    }
265
266    /// Test re-exported types are accessible
267    #[test]
268    fn test_reexports() {
269        // This test verifies that re-exported types are accessible
270        // from the module root
271        let _config = CommandsConfig::minimal();
272        let _arg_type = ArgumentType::String;
273
274        // If this compiles, re-exports are working
275    }
276
277    /// Test complex validation rules
278    #[test]
279    fn test_integration_complex_validation() {
280        let yaml = r#"
281metadata:
282  version: "1.0.0"
283  prompt: "test"
284
285commands:
286  - name: analyze
287    aliases: []
288    description: "Analyze data"
289    required: false
290    arguments:
291      - name: data_file
292        arg_type: path
293        required: true
294        description: "Data file"
295        validation:
296          - must_exist: true
297          - extensions: [dat, bin]
298      - name: threshold
299        arg_type: float
300        required: true
301        description: "Analysis threshold"
302        validation:
303          - min: 0.0
304            max: 1.0
305    options:
306      - name: iterations
307        short: i
308        long: iterations
309        option_type: integer
310        required: false
311        default: "100"
312        description: "Number of iterations"
313        choices: []
314    implementation: "analyze_handler"
315
316global_options: []
317        "#;
318
319        let config = load_yaml(yaml).unwrap();
320        let result = validate_config(&config);
321
322        assert!(result.is_ok());
323
324        // Verify validation rules were parsed correctly
325        let cmd = &config.commands[0];
326        assert_eq!(cmd.arguments[0].validation.len(), 2);
327        assert_eq!(cmd.arguments[1].validation.len(), 1);
328    }
329
330    /// Test error message quality
331    #[test]
332    fn test_integration_error_messages() {
333        // Test various error conditions to ensure error messages are helpful
334
335        // 1. Invalid YAML syntax
336        let bad_yaml = "metadata:\n  version: [unclosed";
337        let result = load_yaml(bad_yaml);
338        assert!(result.is_err());
339
340        // 2. Type mismatch in validation rules
341        let yaml_type_error = r#"
342metadata:
343  version: "1.0.0"
344  prompt: "test"
345commands:
346  - name: cmd
347    aliases: []
348    description: "Test"
349    required: false
350    arguments:
351      - name: count
352        arg_type: integer
353        required: true
354        description: "Count"
355        validation:
356          - must_exist: true
357    options: []
358    implementation: "handler"
359global_options: []
360        "#;
361
362        let config = load_yaml(yaml_type_error).unwrap();
363        let result = validate_config(&config);
364        assert!(result.is_err());
365    }
366}