Skip to main content

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