dynamic_cli/parser/
mod.rs

1//! Command-line and REPL parsing
2//!
3//! This module provides comprehensive parsing functionality for both
4//! traditional command-line interfaces (CLI) and interactive REPL mode.
5//!
6//! # Module Structure
7//!
8//! The parser module consists of three main components:
9//!
10//! - [`type_parser`]: Type conversion functions (string → typed values)
11//! - [`cli_parser`]: CLI argument parser (Unix-style options)
12//! - [`repl_parser`]: REPL line parser (interactive mode)
13//!
14//! # Architecture
15//!
16//! ```text
17//! ┌─────────────────────────────────────────┐
18//! │         User Input                      │
19//! │   "process file.txt --verbose"          │
20//! └──────────────┬──────────────────────────┘
21//!                │
22//!                ▼
23//! ┌─────────────────────────────────────────┐
24//! │       ReplParser (REPL mode)            │
25//! │   - Tokenize line                       │
26//! │   - Resolve command name via Registry   │
27//! │   - Delegate to CliParser               │
28//! └──────────────┬──────────────────────────┘
29//!                │
30//!                ▼
31//! ┌─────────────────────────────────────────┐
32//! │       CliParser (CLI mode)              │
33//! │   - Parse positional arguments          │
34//! │   - Parse options (-v, --verbose)       │
35//! │   - Apply defaults                      │
36//! │   - Use TypeParser for conversion       │
37//! └──────────────┬──────────────────────────┘
38//!                │
39//!                ▼
40//! ┌─────────────────────────────────────────┐
41//! │       TypeParser                        │
42//! │   - Convert strings to typed values     │
43//! │   - Validate type constraints           │
44//! └──────────────┬──────────────────────────┘
45//!                │
46//!                ▼
47//! ┌─────────────────────────────────────────┐
48//! │   HashMap<String, String>               │
49//! │   {"input": "file.txt",                 │
50//! │    "verbose": "true"}                   │
51//! └─────────────────────────────────────────┘
52//! ```
53//!
54//! # Design Principles
55//!
56//! ## 1. Separation of Concerns
57//!
58//! Each parser has a specific responsibility:
59//! - **TypeParser**: Handles type conversion only
60//! - **CliParser**: Handles CLI syntax (options, arguments)
61//! - **ReplParser**: Handles REPL-specific concerns (tokenization, command resolution)
62//!
63//! ## 2. Composability
64//!
65//! Parsers compose naturally:
66//! - ReplParser uses CliParser for argument parsing
67//! - CliParser uses TypeParser for type conversion
68//! - Each can be used independently when needed
69//!
70//! ## 3. Error Clarity
71//!
72//! All parsers provide detailed error messages with:
73//! - Clear descriptions of what went wrong
74//! - Suggestions for typos (via Levenshtein distance)
75//! - Hints for correct usage
76//!
77//! # Usage Examples
78//!
79//! ## CLI Mode (Direct Argument Parsing)
80//!
81//! ```
82//! use dynamic_cli::parser::cli_parser::CliParser;
83//! use dynamic_cli::config::schema::{CommandDefinition, ArgumentDefinition, ArgumentType};
84//!
85//! let definition = CommandDefinition {
86//!     name: "process".to_string(),
87//!     aliases: vec![],
88//!     description: "Process files".to_string(),
89//!     required: false,
90//!     arguments: vec![
91//!         ArgumentDefinition {
92//!             name: "input".to_string(),
93//!             arg_type: ArgumentType::Path,
94//!             required: true,
95//!             description: "Input file".to_string(),
96//!             validation: vec![],
97//!         }
98//!     ],
99//!     options: vec![],
100//!     implementation: "handler".to_string(),
101//! };
102//!
103//! let parser = CliParser::new(&definition);
104//! let args = vec!["input.txt".to_string()];
105//! let parsed = parser.parse(&args).unwrap();
106//!
107//! assert_eq!(parsed.get("input"), Some(&"input.txt".to_string()));
108//! ```
109//!
110//! ## REPL Mode (Interactive Parsing)
111//!
112//! ```no_run
113//! use dynamic_cli::parser::repl_parser::ReplParser;
114//! use dynamic_cli::registry::CommandRegistry;
115//!
116//! let registry = CommandRegistry::new();
117//! // ... register commands ...
118//!
119//! let parser = ReplParser::new(&registry);
120//!
121//! // Parse user input
122//! let line = "process input.txt --verbose";
123//! let parsed = parser.parse_line(line).unwrap();
124//!
125//! println!("Command: {}", parsed.command_name);
126//! println!("Arguments: {:?}", parsed.arguments);
127//! ```
128//!
129//! ## Type Parsing (Low-Level)
130//!
131//! ```
132//! use dynamic_cli::parser::type_parser::{parse_integer, parse_bool};
133//!
134//! let number = parse_integer("42").unwrap();
135//! assert_eq!(number, 42);
136//!
137//! let flag = parse_bool("yes").unwrap();
138//! assert_eq!(flag, true);
139//! ```
140//!
141//! # Error Handling
142//!
143//! All parsing functions return [`Result<T>`] where errors are instances
144//! of [`ParseError`]. Common error scenarios:
145//!
146//! - **Unknown command**: User typed a non-existent command
147//!   ```text
148//!   Error: Unknown command: 'simulat'
149//!   ? Did you mean:
150//!     • simulate
151//!     • validation
152//!   ```
153//!
154//! - **Type mismatch**: Value cannot be converted to expected type
155//!   ```text
156//!   Error: Failed to parse count as integer: 'abc'
157//!   ```
158//!
159//! - **Missing argument**: Required argument not provided
160//!   ```text
161//!   Error: Missing required argument: input for command 'process'
162//!   ```
163//!
164//! # Performance Considerations
165//!
166//! - **Type parsing**: O(1) for most types, O(n) for string length
167//! - **CLI parsing**: O(n) where n = number of arguments
168//! - **REPL parsing**: O(m + n) where m = line length (tokenization), n = arguments
169//! - **Command resolution**: O(1) via HashMap lookup in registry
170//!
171//! # Thread Safety
172//!
173//! All parsers are:
174//! - **Stateless**: Can be used concurrently from multiple threads
175//! - **Borrowing**: Use references to definitions/registry (no ownership)
176//! - **Reusable**: Can parse multiple commands with the same parser instance
177//!
178//! # Future Extensions
179//!
180//! Potential enhancements for future versions:
181//! - Support for subcommands (e.g., `git commit`)
182//! - Environment variable expansion
183//! - Glob pattern matching for paths
184//! - Command history and auto-completion hints
185//! - Streaming parser for very large inputs
186
187#[allow(unused_imports)]
188use crate::error::Result;
189
190// Public submodules
191pub mod cli_parser;
192pub mod repl_parser;
193pub mod type_parser;
194
195// Re-export commonly used types
196pub use cli_parser::CliParser;
197pub use repl_parser::{ParsedCommand, ReplParser};
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::config::schema::{
203        ArgumentDefinition, ArgumentType, CommandDefinition, OptionDefinition,
204    };
205    use crate::context::ExecutionContext;
206    use crate::executor::CommandHandler;
207    use crate::registry::CommandRegistry;
208    use std::collections::HashMap;
209
210    // Dummy handler for integration tests
211    struct IntegrationTestHandler;
212
213    impl CommandHandler for IntegrationTestHandler {
214        fn execute(
215            &self,
216            _context: &mut dyn ExecutionContext,
217            _args: &HashMap<String, String>,
218        ) -> Result<()> {
219            Ok(())
220        }
221    }
222
223    /// Helper to create a comprehensive test command
224    fn create_comprehensive_command() -> CommandDefinition {
225        CommandDefinition {
226            name: "analyze".to_string(),
227            aliases: vec!["analyse".to_string(), "check".to_string()],
228            description: "Analyze data files".to_string(),
229            required: false,
230            arguments: vec![
231                ArgumentDefinition {
232                    name: "input".to_string(),
233                    arg_type: ArgumentType::Path,
234                    required: true,
235                    description: "Input data file".to_string(),
236                    validation: vec![],
237                },
238                ArgumentDefinition {
239                    name: "output".to_string(),
240                    arg_type: ArgumentType::Path,
241                    required: false,
242                    description: "Output report file".to_string(),
243                    validation: vec![],
244                },
245            ],
246            options: vec![
247                OptionDefinition {
248                    name: "verbose".to_string(),
249                    short: Some("v".to_string()),
250                    long: Some("verbose".to_string()),
251                    option_type: ArgumentType::Bool,
252                    required: false,
253                    default: Some("false".to_string()),
254                    description: "Enable verbose output".to_string(),
255                    choices: vec![],
256                },
257                OptionDefinition {
258                    name: "iterations".to_string(),
259                    short: Some("i".to_string()),
260                    long: Some("iterations".to_string()),
261                    option_type: ArgumentType::Integer,
262                    required: false,
263                    default: Some("100".to_string()),
264                    description: "Number of iterations".to_string(),
265                    choices: vec![],
266                },
267                OptionDefinition {
268                    name: "threshold".to_string(),
269                    short: Some("t".to_string()),
270                    long: Some("threshold".to_string()),
271                    option_type: ArgumentType::Float,
272                    required: false,
273                    default: Some("0.5".to_string()),
274                    description: "Analysis threshold".to_string(),
275                    choices: vec![],
276                },
277            ],
278            implementation: "analyze_handler".to_string(),
279        }
280    }
281
282    // ========================================================================
283    // Integration tests: CLI Parser
284    // ========================================================================
285
286    #[test]
287    fn test_cli_parser_integration_minimal() {
288        let definition = create_comprehensive_command();
289        let parser = CliParser::new(&definition);
290
291        let args = vec!["data.csv".to_string()];
292        let result = parser.parse(&args).unwrap();
293
294        // Required argument
295        assert_eq!(result.get("input"), Some(&"data.csv".to_string()));
296
297        // Defaults should be applied
298        assert_eq!(result.get("verbose"), Some(&"false".to_string()));
299        assert_eq!(result.get("iterations"), Some(&"100".to_string()));
300        assert_eq!(result.get("threshold"), Some(&"0.5".to_string()));
301    }
302
303    #[test]
304    fn test_cli_parser_integration_full() {
305        let definition = create_comprehensive_command();
306        let parser = CliParser::new(&definition);
307
308        let args = vec![
309            "data.csv".to_string(),
310            "report.txt".to_string(),
311            "--verbose".to_string(),
312            "--iterations=200".to_string(),
313            "-t".to_string(),
314            "0.75".to_string(),
315        ];
316        let result = parser.parse(&args).unwrap();
317
318        assert_eq!(result.get("input"), Some(&"data.csv".to_string()));
319        assert_eq!(result.get("output"), Some(&"report.txt".to_string()));
320        assert_eq!(result.get("verbose"), Some(&"true".to_string()));
321        assert_eq!(result.get("iterations"), Some(&"200".to_string()));
322        assert_eq!(result.get("threshold"), Some(&"0.75".to_string()));
323    }
324
325    #[test]
326    fn test_cli_parser_integration_mixed_options() {
327        let definition = create_comprehensive_command();
328        let parser = CliParser::new(&definition);
329
330        // Options can be interspersed with positional arguments
331        let args = vec![
332            "--verbose".to_string(),
333            "data.csv".to_string(),
334            "-i200".to_string(),
335            "report.txt".to_string(),
336            "--threshold".to_string(),
337            "0.9".to_string(),
338        ];
339        let result = parser.parse(&args).unwrap();
340
341        assert_eq!(result.get("input"), Some(&"data.csv".to_string()));
342        assert_eq!(result.get("output"), Some(&"report.txt".to_string()));
343        assert_eq!(result.get("verbose"), Some(&"true".to_string()));
344        assert_eq!(result.get("iterations"), Some(&"200".to_string()));
345        assert_eq!(result.get("threshold"), Some(&"0.9".to_string()));
346    }
347
348    // ========================================================================
349    // Integration tests: REPL Parser
350    // ========================================================================
351
352    #[test]
353    fn test_repl_parser_integration_simple() {
354        let mut registry = CommandRegistry::new();
355        let definition = create_comprehensive_command();
356        registry
357            .register(definition, Box::new(IntegrationTestHandler))
358            .unwrap();
359
360        let parser = ReplParser::new(&registry);
361
362        let parsed = parser.parse_line("analyze data.csv").unwrap();
363        assert_eq!(parsed.command_name, "analyze");
364        assert_eq!(parsed.arguments.get("input"), Some(&"data.csv".to_string()));
365    }
366
367    #[test]
368    fn test_repl_parser_integration_alias() {
369        let mut registry = CommandRegistry::new();
370        let definition = create_comprehensive_command();
371        registry
372            .register(definition, Box::new(IntegrationTestHandler))
373            .unwrap();
374
375        let parser = ReplParser::new(&registry);
376
377        // Use alias instead of command name
378        let parsed = parser.parse_line("check data.csv --verbose").unwrap();
379        assert_eq!(parsed.command_name, "analyze"); // Resolves to canonical name
380        assert_eq!(parsed.arguments.get("input"), Some(&"data.csv".to_string()));
381        assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
382    }
383
384    #[test]
385    fn test_repl_parser_integration_quoted_paths() {
386        let mut registry = CommandRegistry::new();
387        let definition = create_comprehensive_command();
388        registry
389            .register(definition, Box::new(IntegrationTestHandler))
390            .unwrap();
391
392        let parser = ReplParser::new(&registry);
393
394        let parsed = parser
395            .parse_line(r#"analyze "/path/with spaces/data.csv" "output report.txt""#)
396            .unwrap();
397
398        assert_eq!(
399            parsed.arguments.get("input"),
400            Some(&"/path/with spaces/data.csv".to_string())
401        );
402        assert_eq!(
403            parsed.arguments.get("output"),
404            Some(&"output report.txt".to_string())
405        );
406    }
407
408    #[test]
409    fn test_repl_parser_integration_complex() {
410        let mut registry = CommandRegistry::new();
411        let definition = create_comprehensive_command();
412        registry
413            .register(definition, Box::new(IntegrationTestHandler))
414            .unwrap();
415
416        let parser = ReplParser::new(&registry);
417
418        let parsed = parser
419            .parse_line(r#"analyse "data file.csv" report.txt -v --iterations=500 -t 0.95"#)
420            .unwrap();
421
422        assert_eq!(parsed.command_name, "analyze");
423        assert_eq!(
424            parsed.arguments.get("input"),
425            Some(&"data file.csv".to_string())
426        );
427        assert_eq!(
428            parsed.arguments.get("output"),
429            Some(&"report.txt".to_string())
430        );
431        assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
432        assert_eq!(parsed.arguments.get("iterations"), Some(&"500".to_string()));
433        assert_eq!(parsed.arguments.get("threshold"), Some(&"0.95".to_string()));
434    }
435
436    // ========================================================================
437    // Integration tests: Type Parser
438    // ========================================================================
439
440    #[test]
441    fn test_type_parser_integration_all_types() {
442        use type_parser::parse_value;
443
444        // Test all argument types
445        assert!(parse_value("hello", ArgumentType::String).is_ok());
446        assert!(parse_value("42", ArgumentType::Integer).is_ok());
447        assert!(parse_value("3.14", ArgumentType::Float).is_ok());
448        assert!(parse_value("true", ArgumentType::Bool).is_ok());
449        assert!(parse_value("/path/to/file", ArgumentType::Path).is_ok());
450    }
451
452    #[test]
453    fn test_type_parser_integration_error_propagation() {
454        let definition = create_comprehensive_command();
455        let parser = CliParser::new(&definition);
456
457        // Invalid integer should fail
458        let args = vec![
459            "data.csv".to_string(),
460            "--iterations".to_string(),
461            "not_a_number".to_string(),
462        ];
463
464        let result = parser.parse(&args);
465        assert!(result.is_err());
466    }
467
468    // ========================================================================
469    // Integration tests: End-to-End Workflows
470    // ========================================================================
471
472    #[test]
473    fn test_workflow_cli_to_execution() {
474        // Simulate: User provides CLI args → Parser → Handler could execute
475
476        let definition = create_comprehensive_command();
477        let parser = CliParser::new(&definition);
478
479        let args = vec!["data.csv".to_string(), "-v".to_string()];
480        let parsed = parser.parse(&args).unwrap();
481
482        // Verify parsed data is ready for execution
483        assert!(parsed.contains_key("input"));
484        assert!(parsed.contains_key("verbose"));
485        assert_eq!(parsed.get("verbose"), Some(&"true".to_string()));
486    }
487
488    #[test]
489    fn test_workflow_repl_to_execution() {
490        // Simulate: User types in REPL → Parser → Handler could execute
491
492        let mut registry = CommandRegistry::new();
493        let definition = create_comprehensive_command();
494        registry
495            .register(definition, Box::new(IntegrationTestHandler))
496            .unwrap();
497
498        let parser = ReplParser::new(&registry);
499
500        let line = "analyze data.csv --verbose --iterations=1000";
501        let parsed = parser.parse_line(line).unwrap();
502
503        // Verify parsed command is ready for execution
504        assert_eq!(parsed.command_name, "analyze");
505        assert!(parsed.arguments.contains_key("input"));
506        assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
507        assert_eq!(
508            parsed.arguments.get("iterations"),
509            Some(&"1000".to_string())
510        );
511    }
512
513    #[test]
514    fn test_workflow_typo_suggestions() {
515        let mut registry = CommandRegistry::new();
516        let definition = create_comprehensive_command();
517        registry
518            .register(definition, Box::new(IntegrationTestHandler))
519            .unwrap();
520
521        let parser = ReplParser::new(&registry);
522
523        // User makes a typo
524        let result = parser.parse_line("analyz data.csv");
525
526        assert!(result.is_err());
527
528        // Error should contain suggestions
529        let error = result.unwrap_err();
530        let error_msg = format!("{}", error);
531        assert!(error_msg.contains("Unknown command"));
532    }
533
534    // ========================================================================
535    // Re-export verification tests
536    // ========================================================================
537
538    #[test]
539    fn test_reexports_accessible() {
540        // Verify that re-exported types are accessible from module root
541
542        let definition = create_comprehensive_command();
543
544        // CliParser should be accessible
545        let _cli_parser = CliParser::new(&definition);
546
547        // ReplParser should be accessible (needs registry)
548        let registry = CommandRegistry::new();
549        let _repl_parser = ReplParser::new(&registry);
550
551        // ParsedCommand should be accessible
552        let _parsed = ParsedCommand {
553            command_name: "test".to_string(),
554            arguments: HashMap::new(),
555        };
556    }
557}