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