1#[allow(unused_imports)]
43use crate::config::schema::{ArgumentDefinition, CommandDefinition, OptionDefinition};
44use crate::error::{ParseError, Result};
45use crate::parser::type_parser;
46use std::collections::HashMap;
47
48pub struct CliParser<'a> {
95 definition: &'a CommandDefinition,
97}
98
99impl<'a> CliParser<'a> {
100 pub fn new(definition: &'a CommandDefinition) -> Self {
124 Self { definition }
125 }
126
127 pub fn parse(&self, args: &[String]) -> Result<HashMap<String, String>> {
181 let mut result = HashMap::new();
182 let mut positional_index = 0;
183 let mut i = 0;
184
185 while i < args.len() {
187 let arg = &args[i];
188
189 if arg.starts_with("--") {
190 self.parse_long_option(arg, args, &mut i, &mut result)?;
192 } else if arg.starts_with('-') && arg.len() > 1 {
193 if arg
195 .chars()
196 .nth(1)
197 .map(|c| c.is_ascii_digit())
198 .unwrap_or(false)
199 {
200 self.parse_positional_argument(arg, positional_index, &mut result)?;
202 positional_index += 1;
203 } else {
204 self.parse_short_option(arg, args, &mut i, &mut result)?;
205 }
206 } else {
207 self.parse_positional_argument(arg, positional_index, &mut result)?;
209 positional_index += 1;
210 }
211
212 i += 1;
213 }
214
215 self.apply_defaults(&mut result)?;
217
218 self.validate_required_arguments(&result)?;
220 self.validate_required_options(&result)?;
221
222 Ok(result)
223 }
224
225 fn parse_long_option(
227 &self,
228 arg: &str,
229 args: &[String],
230 index: &mut usize,
231 result: &mut HashMap<String, String>,
232 ) -> Result<()> {
233 let arg_without_dashes = &arg[2..];
234
235 if let Some(eq_pos) = arg_without_dashes.find('=') {
237 let option_name = &arg_without_dashes[..eq_pos];
238 let value = &arg_without_dashes[eq_pos + 1..];
239
240 let option = self.find_option_by_long(option_name)?;
241 let parsed_value = type_parser::parse_value(value, option.option_type)?;
242 result.insert(option.name.clone(), parsed_value);
243 } else {
244 let option = self.find_option_by_long(arg_without_dashes)?;
246
247 if matches!(
249 option.option_type,
250 crate::config::schema::ArgumentType::Bool
251 ) {
252 result.insert(option.name.clone(), "true".to_string());
253 } else {
254 *index += 1;
256 if *index >= args.len() {
257 return Err(ParseError::InvalidSyntax {
258 details: format!(
259 "Option --{} requires a value",
260 option.long.as_ref().unwrap()
261 ),
262 hint: Some(format!(
263 "Usage: --{}=<value> or --{} <value>",
264 option.long.as_ref().unwrap(),
265 option.long.as_ref().unwrap()
266 )),
267 }
268 .into());
269 }
270
271 let value = &args[*index];
272 let parsed_value = type_parser::parse_value(value, option.option_type)?;
273 result.insert(option.name.clone(), parsed_value);
274 }
275 }
276
277 Ok(())
278 }
279
280 fn parse_short_option(
282 &self,
283 arg: &str,
284 args: &[String],
285 index: &mut usize,
286 result: &mut HashMap<String, String>,
287 ) -> Result<()> {
288 let short_flag = &arg[1..2];
289 let option = self.find_option_by_short(short_flag)?;
290
291 if matches!(
293 option.option_type,
294 crate::config::schema::ArgumentType::Bool
295 ) {
296 result.insert(option.name.clone(), "true".to_string());
297 } else {
298 if arg.len() > 2 {
300 let value = &arg[2..];
301 let parsed_value = type_parser::parse_value(value, option.option_type)?;
302 result.insert(option.name.clone(), parsed_value);
303 } else {
304 *index += 1;
306 if *index >= args.len() {
307 return Err(ParseError::InvalidSyntax {
308 details: format!("Option -{} requires a value", short_flag),
309 hint: Some(format!(
310 "Usage: -{}<value> or -{} <value>",
311 short_flag, short_flag
312 )),
313 }
314 .into());
315 }
316
317 let value = &args[*index];
318 let parsed_value = type_parser::parse_value(value, option.option_type)?;
319 result.insert(option.name.clone(), parsed_value);
320 }
321 }
322
323 Ok(())
324 }
325
326 fn parse_positional_argument(
328 &self,
329 value: &str,
330 index: usize,
331 result: &mut HashMap<String, String>,
332 ) -> Result<()> {
333 if index >= self.definition.arguments.len() {
334 return Err(ParseError::TooManyArguments {
335 command: self.definition.name.clone(),
336 expected: self.definition.arguments.len(),
337 got: index + 1,
338 }
339 .into());
340 }
341
342 let arg_def = &self.definition.arguments[index];
343 let parsed_value = type_parser::parse_value(value, arg_def.arg_type)?;
344 result.insert(arg_def.name.clone(), parsed_value);
345
346 Ok(())
347 }
348
349 fn apply_defaults(&self, result: &mut HashMap<String, String>) -> Result<()> {
351 for option in &self.definition.options {
352 if !result.contains_key(&option.name) {
353 if let Some(ref default) = option.default {
354 let parsed_default = type_parser::parse_value(default, option.option_type)?;
356 result.insert(option.name.clone(), parsed_default);
357 }
358 }
359 }
360 Ok(())
361 }
362
363 fn validate_required_arguments(&self, result: &HashMap<String, String>) -> Result<()> {
365 for arg in &self.definition.arguments {
366 if arg.required && !result.contains_key(&arg.name) {
367 return Err(ParseError::MissingArgument {
368 argument: arg.name.clone(),
369 command: self.definition.name.clone(),
370 }
371 .into());
372 }
373 }
374 Ok(())
375 }
376
377 fn validate_required_options(&self, result: &HashMap<String, String>) -> Result<()> {
379 for option in &self.definition.options {
380 if option.required && !result.contains_key(&option.name) {
381 return Err(ParseError::MissingOption {
382 option: option
383 .long
384 .clone()
385 .or(option.short.clone())
386 .unwrap_or_default(),
387 command: self.definition.name.clone(),
388 }
389 .into());
390 }
391 }
392 Ok(())
393 }
394
395 fn find_option_by_long(&self, long: &str) -> Result<&OptionDefinition> {
397 self.definition
398 .options
399 .iter()
400 .find(|opt| opt.long.as_deref() == Some(long))
401 .ok_or_else(|| {
402 let available: Vec<String> = self
403 .definition
404 .options
405 .iter()
406 .filter_map(|o| o.long.clone())
407 .collect();
408 ParseError::unknown_option_with_suggestions(
409 &format!("--{}", long),
410 &self.definition.name,
411 &available,
412 )
413 .into()
414 })
415 }
416
417 fn find_option_by_short(&self, short: &str) -> Result<&OptionDefinition> {
419 self.definition
420 .options
421 .iter()
422 .find(|opt| opt.short.as_deref() == Some(short))
423 .ok_or_else(|| {
424 let available: Vec<String> = self
425 .definition
426 .options
427 .iter()
428 .filter_map(|o| o.short.clone())
429 .collect();
430 ParseError::unknown_option_with_suggestions(
431 &format!("-{}", short),
432 &self.definition.name,
433 &available,
434 )
435 .into()
436 })
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use crate::config::schema::{ArgumentType, OptionDefinition};
444
445 fn create_test_definition() -> CommandDefinition {
447 CommandDefinition {
448 name: "test".to_string(),
449 aliases: vec![],
450 description: "Test command".to_string(),
451 required: false,
452 arguments: vec![
453 ArgumentDefinition {
454 name: "input".to_string(),
455 arg_type: ArgumentType::Path,
456 required: true,
457 description: "Input file".to_string(),
458 validation: vec![],
459 },
460 ArgumentDefinition {
461 name: "output".to_string(),
462 arg_type: ArgumentType::Path,
463 required: false,
464 description: "Output file".to_string(),
465 validation: vec![],
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}