1#[allow(unused_imports)]
44use crate::config::schema::{ArgumentDefinition, CommandDefinition, OptionDefinition};
45use crate::error::{ParseError, Result};
46use crate::parser::type_parser;
47use std::collections::HashMap;
48
49pub struct CliParser<'a> {
96 definition: &'a CommandDefinition,
98}
99
100impl<'a> CliParser<'a> {
101 pub fn new(definition: &'a CommandDefinition) -> Self {
125 Self { definition }
126 }
127
128 pub fn parse(&self, args: &[String]) -> Result<HashMap<String, String>> {
183 let mut result = HashMap::new();
184 let mut positional_index = 0;
185 let mut i = 0;
186
187 while i < args.len() {
189 let arg = &args[i];
190
191 if arg.starts_with("--") {
192 self.parse_long_option(arg, args, &mut i, &mut result)?;
194 } else if arg.starts_with('-') && arg.len() > 1 {
195 if arg
197 .chars()
198 .nth(1)
199 .map(|c| c.is_ascii_digit())
200 .unwrap_or(false)
201 {
202 self.parse_positional_argument(arg, positional_index, &mut result)?;
204 positional_index += 1;
205 } else {
206 self.parse_short_option(arg, args, &mut i, &mut result)?;
207 }
208 } else {
209 self.parse_positional_argument(arg, positional_index, &mut result)?;
211 positional_index += 1;
212 }
213
214 i += 1;
215 }
216
217 self.apply_defaults(&mut result)?;
219
220 self.validate_required_arguments(&result)?;
222 self.validate_required_options(&result)?;
223
224 Ok(result)
225 }
226
227 fn parse_long_option(
229 &self,
230 arg: &str,
231 args: &[String],
232 index: &mut usize,
233 result: &mut HashMap<String, String>,
234 ) -> Result<()> {
235 let arg_without_dashes = &arg[2..];
236
237 if let Some(eq_pos) = arg_without_dashes.find('=') {
239 let option_name = &arg_without_dashes[..eq_pos];
240 let value = &arg_without_dashes[eq_pos + 1..];
241
242 let option = self.find_option_by_long(option_name)?;
243 let parsed_value = type_parser::parse_value(value, option.option_type)?;
244 result.insert(option.name.clone(), parsed_value);
245 } else {
246 let option = self.find_option_by_long(arg_without_dashes)?;
248
249 if matches!(
251 option.option_type,
252 crate::config::schema::ArgumentType::Bool
253 ) {
254 result.insert(option.name.clone(), "true".to_string());
255 } else {
256 *index += 1;
258 if *index >= args.len() {
259 return Err(ParseError::InvalidSyntax {
260 details: format!(
261 "Option --{} requires a value",
262 option.long.as_ref().unwrap()
263 ),
264 hint: Some(format!(
265 "Usage: --{}=<value> or --{} <value>",
266 option.long.as_ref().unwrap(),
267 option.long.as_ref().unwrap()
268 )),
269 }
270 .into());
271 }
272
273 let value = &args[*index];
274 let parsed_value = type_parser::parse_value(value, option.option_type)?;
275 result.insert(option.name.clone(), parsed_value);
276 }
277 }
278
279 Ok(())
280 }
281
282 fn parse_short_option(
284 &self,
285 arg: &str,
286 args: &[String],
287 index: &mut usize,
288 result: &mut HashMap<String, String>,
289 ) -> Result<()> {
290 let short_flag = &arg[1..2];
291 let option = self.find_option_by_short(short_flag)?;
292
293 if matches!(
295 option.option_type,
296 crate::config::schema::ArgumentType::Bool
297 ) {
298 result.insert(option.name.clone(), "true".to_string());
299 } else {
300 if arg.len() > 2 {
302 let value = &arg[2..];
303 let parsed_value = type_parser::parse_value(value, option.option_type)?;
304 result.insert(option.name.clone(), parsed_value);
305 } else {
306 *index += 1;
308 if *index >= args.len() {
309 return Err(ParseError::InvalidSyntax {
310 details: format!("Option -{} requires a value", short_flag),
311 hint: Some(format!(
312 "Usage: -{}<value> or -{} <value>",
313 short_flag, short_flag
314 )),
315 }
316 .into());
317 }
318
319 let value = &args[*index];
320 let parsed_value = type_parser::parse_value(value, option.option_type)?;
321 result.insert(option.name.clone(), parsed_value);
322 }
323 }
324
325 Ok(())
326 }
327
328 fn parse_positional_argument(
330 &self,
331 value: &str,
332 index: usize,
333 result: &mut HashMap<String, String>,
334 ) -> Result<()> {
335 if index >= self.definition.arguments.len() {
336 return Err(ParseError::too_many_arguments(
337 &self.definition.name,
338 self.definition.arguments.len(),
339 index + 1,
340 )
341 .into());
342 }
343
344 let arg_def = &self.definition.arguments[index];
345 let parsed_value = type_parser::parse_value(value, arg_def.arg_type)?;
346 result.insert(arg_def.name.clone(), parsed_value);
347
348 Ok(())
349 }
350
351 fn apply_defaults(&self, result: &mut HashMap<String, String>) -> Result<()> {
353 for option in &self.definition.options {
354 if !result.contains_key(&option.name) {
355 if let Some(ref default) = option.default {
356 let parsed_default = type_parser::parse_value(default, option.option_type)?;
358 result.insert(option.name.clone(), parsed_default);
359 }
360 }
361 }
362 Ok(())
363 }
364
365 fn validate_required_arguments(&self, result: &HashMap<String, String>) -> Result<()> {
367 for arg in &self.definition.arguments {
368 if arg.required && !result.contains_key(&arg.name) {
369 return Err(ParseError::missing_argument(&arg.name, &self.definition.name).into());
370 }
371 }
372 Ok(())
373 }
374
375 fn validate_required_options(&self, result: &HashMap<String, String>) -> Result<()> {
377 for option in &self.definition.options {
378 if option.required && !result.contains_key(&option.name) {
379 return Err(ParseError::missing_option(
380 &option
381 .long
382 .clone()
383 .or(option.short.clone())
384 .unwrap_or_default(),
385 &self.definition.name,
386 )
387 .into());
388 }
389 }
390 Ok(())
391 }
392
393 fn find_option_by_long(&self, long: &str) -> Result<&OptionDefinition> {
395 self.definition
396 .options
397 .iter()
398 .find(|opt| opt.long.as_deref() == Some(long))
399 .ok_or_else(|| {
400 let available: Vec<String> = self
401 .definition
402 .options
403 .iter()
404 .filter_map(|o| o.long.clone())
405 .collect();
406 ParseError::unknown_option_with_suggestions(
407 &format!("--{}", long),
408 &self.definition.name,
409 &available,
410 )
411 .into()
412 })
413 }
414
415 fn find_option_by_short(&self, short: &str) -> Result<&OptionDefinition> {
417 self.definition
418 .options
419 .iter()
420 .find(|opt| opt.short.as_deref() == Some(short))
421 .ok_or_else(|| {
422 let available: Vec<String> = self
423 .definition
424 .options
425 .iter()
426 .filter_map(|o| o.short.clone())
427 .collect();
428 ParseError::unknown_option_with_suggestions(
429 &format!("-{}", short),
430 &self.definition.name,
431 &available,
432 )
433 .into()
434 })
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::config::schema::{ArgumentType, OptionDefinition};
442
443 fn create_test_definition() -> CommandDefinition {
445 CommandDefinition {
446 name: "test".to_string(),
447 aliases: vec![],
448 description: "Test command".to_string(),
449 required: false,
450 arguments: vec![
451 ArgumentDefinition {
452 name: "input".to_string(),
453 arg_type: ArgumentType::Path,
454 required: true,
455 description: "Input file".to_string(),
456 validation: vec![],
457 secure: false,
458 },
459 ArgumentDefinition {
460 name: "output".to_string(),
461 arg_type: ArgumentType::Path,
462 required: false,
463 description: "Output file".to_string(),
464 validation: vec![],
465 secure: false,
466 },
467 ],
468 options: vec![
469 OptionDefinition {
470 name: "verbose".to_string(),
471 short: Some("v".to_string()),
472 long: Some("verbose".to_string()),
473 option_type: ArgumentType::Bool,
474 required: false,
475 default: Some("false".to_string()),
476 description: "Verbose output".to_string(),
477 choices: vec![],
478 },
479 OptionDefinition {
480 name: "count".to_string(),
481 short: Some("c".to_string()),
482 long: Some("count".to_string()),
483 option_type: ArgumentType::Integer,
484 required: false,
485 default: Some("10".to_string()),
486 description: "Count".to_string(),
487 choices: vec![],
488 },
489 ],
490 implementation: "handler".to_string(),
491 }
492 }
493
494 #[test]
499 fn test_parse_single_positional_argument() {
500 let definition = create_test_definition();
501 let parser = CliParser::new(&definition);
502
503 let args = vec!["input.txt".to_string()];
504 let result = parser.parse(&args).unwrap();
505
506 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
507 }
508
509 #[test]
510 fn test_parse_multiple_positional_arguments() {
511 let definition = create_test_definition();
512 let parser = CliParser::new(&definition);
513
514 let args = vec!["input.txt".to_string(), "output.txt".to_string()];
515 let result = parser.parse(&args).unwrap();
516
517 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
518 assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
519 }
520
521 #[test]
522 fn test_parse_missing_required_argument() {
523 let definition = create_test_definition();
524 let parser = CliParser::new(&definition);
525
526 let args: Vec<String> = vec![];
527 let result = parser.parse(&args);
528
529 assert!(result.is_err());
530 match result.unwrap_err() {
531 crate::error::DynamicCliError::Parse(ParseError::MissingArgument {
532 argument, ..
533 }) => {
534 assert_eq!(argument, "input");
535 }
536 other => panic!("Expected MissingArgument error, got {:?}", other),
537 }
538 }
539
540 #[test]
541 fn test_parse_too_many_positional_arguments() {
542 let definition = create_test_definition();
543 let parser = CliParser::new(&definition);
544
545 let args = vec![
546 "input.txt".to_string(),
547 "output.txt".to_string(),
548 "extra.txt".to_string(),
549 ];
550 let result = parser.parse(&args);
551
552 assert!(result.is_err());
553 match result.unwrap_err() {
554 crate::error::DynamicCliError::Parse(ParseError::TooManyArguments { .. }) => {}
555 other => panic!("Expected TooManyArguments error, got {:?}", other),
556 }
557 }
558
559 #[test]
564 fn test_parse_long_boolean_option() {
565 let definition = create_test_definition();
566 let parser = CliParser::new(&definition);
567
568 let args = vec!["input.txt".to_string(), "--verbose".to_string()];
569 let result = parser.parse(&args).unwrap();
570
571 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
572 }
573
574 #[test]
575 fn test_parse_long_option_with_equals() {
576 let definition = create_test_definition();
577 let parser = CliParser::new(&definition);
578
579 let args = vec!["input.txt".to_string(), "--count=42".to_string()];
580 let result = parser.parse(&args).unwrap();
581
582 assert_eq!(result.get("count"), Some(&"42".to_string()));
583 }
584
585 #[test]
586 fn test_parse_long_option_with_space() {
587 let definition = create_test_definition();
588 let parser = CliParser::new(&definition);
589
590 let args = vec![
591 "input.txt".to_string(),
592 "--count".to_string(),
593 "42".to_string(),
594 ];
595 let result = parser.parse(&args).unwrap();
596
597 assert_eq!(result.get("count"), Some(&"42".to_string()));
598 }
599
600 #[test]
601 fn test_parse_unknown_long_option() {
602 let definition = create_test_definition();
603 let parser = CliParser::new(&definition);
604
605 let args = vec!["input.txt".to_string(), "--unknown".to_string()];
606 let result = parser.parse(&args);
607
608 assert!(result.is_err());
609 match result.unwrap_err() {
610 crate::error::DynamicCliError::Parse(ParseError::UnknownOption { .. }) => {}
611 other => panic!("Expected UnknownOption error, got {:?}", other),
612 }
613 }
614
615 #[test]
620 fn test_parse_short_boolean_option() {
621 let definition = create_test_definition();
622 let parser = CliParser::new(&definition);
623
624 let args = vec!["input.txt".to_string(), "-v".to_string()];
625 let result = parser.parse(&args).unwrap();
626
627 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
628 }
629
630 #[test]
631 fn test_parse_short_option_with_space() {
632 let definition = create_test_definition();
633 let parser = CliParser::new(&definition);
634
635 let args = vec!["input.txt".to_string(), "-c".to_string(), "42".to_string()];
636 let result = parser.parse(&args).unwrap();
637
638 assert_eq!(result.get("count"), Some(&"42".to_string()));
639 }
640
641 #[test]
642 fn test_parse_short_option_attached_value() {
643 let definition = create_test_definition();
644 let parser = CliParser::new(&definition);
645
646 let args = vec!["input.txt".to_string(), "-c42".to_string()];
647 let result = parser.parse(&args).unwrap();
648
649 assert_eq!(result.get("count"), Some(&"42".to_string()));
650 }
651
652 #[test]
653 fn test_parse_negative_number_as_positional() {
654 let definition = create_test_definition();
655 let parser = CliParser::new(&definition);
656
657 let args = vec!["-123".to_string()];
659 let result = parser.parse(&args).unwrap();
660
661 assert_eq!(result.get("input"), Some(&"-123".to_string()));
662 }
663
664 #[test]
669 fn test_apply_default_values() {
670 let definition = create_test_definition();
671 let parser = CliParser::new(&definition);
672
673 let args = vec!["input.txt".to_string()];
674 let result = parser.parse(&args).unwrap();
675
676 assert_eq!(result.get("verbose"), Some(&"false".to_string()));
678 assert_eq!(result.get("count"), Some(&"10".to_string()));
679 }
680
681 #[test]
682 fn test_override_default_values() {
683 let definition = create_test_definition();
684 let parser = CliParser::new(&definition);
685
686 let args = vec![
687 "input.txt".to_string(),
688 "-v".to_string(),
689 "-c".to_string(),
690 "5".to_string(),
691 ];
692 let result = parser.parse(&args).unwrap();
693
694 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
696 assert_eq!(result.get("count"), Some(&"5".to_string()));
697 }
698
699 #[test]
704 fn test_type_conversion_error() {
705 let definition = create_test_definition();
706 let parser = CliParser::new(&definition);
707
708 let args = vec![
710 "input.txt".to_string(),
711 "--count".to_string(),
712 "abc".to_string(),
713 ];
714 let result = parser.parse(&args);
715
716 assert!(result.is_err());
717 }
718
719 #[test]
724 fn test_parse_complex_command_line() {
725 let definition = create_test_definition();
726 let parser = CliParser::new(&definition);
727
728 let args = vec![
729 "input.txt".to_string(),
730 "output.txt".to_string(),
731 "--verbose".to_string(),
732 "--count=100".to_string(),
733 ];
734 let result = parser.parse(&args).unwrap();
735
736 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
737 assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
738 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
739 assert_eq!(result.get("count"), Some(&"100".to_string()));
740 }
741
742 #[test]
743 fn test_parse_mixed_options_and_arguments() {
744 let definition = create_test_definition();
745 let parser = CliParser::new(&definition);
746
747 let args = vec![
749 "--verbose".to_string(),
750 "input.txt".to_string(),
751 "-c".to_string(),
752 "50".to_string(),
753 "output.txt".to_string(),
754 ];
755 let result = parser.parse(&args).unwrap();
756
757 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
758 assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
759 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
760 assert_eq!(result.get("count"), Some(&"50".to_string()));
761 }
762}