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