Skip to main content

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