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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry);
550
551 // ParsedCommand should be accessible
552 let _parsed = ParsedCommand {
553 command_name: "test".to_string(),
554 arguments: HashMap::new(),
555 };
556 }
557}