1use crate::config::schema::{
33 ArgumentDefinition, ArgumentType, CommandDefinition, CommandsConfig, OptionDefinition,
34 ValidationRule,
35};
36use crate::error::{ConfigError, Result};
37use std::collections::{HashMap, HashSet};
38
39pub fn validate_config(config: &CommandsConfig) -> Result<()> {
78 let mut seen_names: HashSet<String> = HashSet::new();
80
81 for (idx, command) in config.commands.iter().enumerate() {
82 validate_command(command)?;
84
85 if !seen_names.insert(command.name.clone()) {
87 return Err(ConfigError::DuplicateCommand {
88 name: command.name.clone(),
89 suggestion: None,
90 }
91 .into());
92 }
93
94 for alias in &command.aliases {
96 if !seen_names.insert(alias.clone()) {
97 return Err(ConfigError::DuplicateCommand {
98 name: alias.clone(),
99 suggestion: None,
100 }
101 .into());
102 }
103 }
104
105 if command.name.trim().is_empty() {
107 return Err(ConfigError::InvalidSchema {
108 reason: "Command name cannot be empty".to_string(),
109 path: Some(format!("commands[{}].name", idx)),
110 suggestion: None,
111 }
112 .into());
113 }
114
115 if command.implementation.trim().is_empty() {
117 return Err(ConfigError::InvalidSchema {
118 reason: "Command implementation cannot be empty".to_string(),
119 path: Some(format!("commands[{}].implementation", idx)),
120 suggestion: None,
121 }
122 .into());
123 }
124 }
125
126 validate_options(&config.global_options, "global_options")?;
128
129 Ok(())
130}
131
132pub fn validate_command(cmd: &CommandDefinition) -> Result<()> {
173 validate_argument_types(&cmd.arguments)?;
175 validate_argument_ordering(&cmd.arguments, &cmd.name)?;
176 validate_argument_names(&cmd.arguments, &cmd.name)?;
177 validate_argument_validation_rules(&cmd.arguments, &cmd.name)?;
178
179 validate_options(&cmd.options, &cmd.name)?;
181 validate_option_flags(&cmd.options, &cmd.name)?;
182
183 check_name_conflicts(&cmd.arguments, &cmd.options, &cmd.name)?;
185
186 Ok(())
187}
188
189pub fn validate_argument_types(args: &[ArgumentDefinition]) -> Result<()> {
221 for arg in args {
225 let _ = arg.arg_type;
228 }
229
230 Ok(())
231}
232
233fn validate_argument_ordering(args: &[ArgumentDefinition], context: &str) -> Result<()> {
243 let mut seen_optional = false;
244
245 for (idx, arg) in args.iter().enumerate() {
246 if !arg.required {
247 seen_optional = true;
248 } else if seen_optional {
249 return Err(ConfigError::InvalidSchema {
250 reason: format!(
251 "Required argument '{}' cannot come after optional arguments",
252 arg.name
253 ),
254 path: Some(format!("{}.arguments[{}]", context, idx)),
255 suggestion: None,
256 }
257 .into());
258 }
259 }
260
261 Ok(())
262}
263
264fn validate_argument_names(args: &[ArgumentDefinition], context: &str) -> Result<()> {
266 let mut seen_names: HashSet<String> = HashSet::new();
267
268 for (idx, arg) in args.iter().enumerate() {
269 if arg.name.trim().is_empty() {
270 return Err(ConfigError::InvalidSchema {
271 reason: "Argument name cannot be empty".to_string(),
272 path: Some(format!("{}.arguments[{}]", context, idx)),
273 suggestion: None,
274 }
275 .into());
276 }
277
278 if !seen_names.insert(arg.name.clone()) {
279 return Err(ConfigError::InvalidSchema {
280 reason: format!("Duplicate argument name: '{}'", arg.name),
281 path: Some(format!("{}.arguments", context)),
282 suggestion: None,
283 }
284 .into());
285 }
286 }
287
288 Ok(())
289}
290
291fn validate_argument_validation_rules(args: &[ArgumentDefinition], _context: &str) -> Result<()> {
293 for arg in args.iter() {
294 for rule in arg.validation.iter() {
295 match rule {
296 ValidationRule::MustExist { .. } | ValidationRule::Extensions { .. } => {
297 if arg.arg_type != ArgumentType::Path {
299 return Err(ConfigError::Inconsistency {
300 details: format!(
301 "Validation rule 'must_exist' or 'extensions' can only be used with 'path' type, \
302 but argument '{}' has type '{}'",
303 arg.name,
304 arg.arg_type.as_str()
305 ),
306 suggestion: None,
307 }.into());
308 }
309 }
310 ValidationRule::Range { min, max } => {
311 if !matches!(arg.arg_type, ArgumentType::Integer | ArgumentType::Float) {
313 return Err(ConfigError::Inconsistency {
314 details: format!(
315 "Validation rule 'range' can only be used with numeric types, \
316 but argument '{}' has type '{}'",
317 arg.name,
318 arg.arg_type.as_str()
319 ),
320 suggestion: None,
321 }
322 .into());
323 }
324
325 if let (Some(min_val), Some(max_val)) = (min, max) {
327 if min_val > max_val {
328 return Err(ConfigError::Inconsistency {
329 details: format!(
330 "Invalid range for argument '{}': min ({}) > max ({})",
331 arg.name, min_val, max_val
332 ),
333 suggestion: None,
334 }
335 .into());
336 }
337 }
338 }
339 }
340 }
341 }
342
343 Ok(())
344}
345
346fn validate_options(options: &[OptionDefinition], context: &str) -> Result<()> {
348 let mut seen_names: HashSet<String> = HashSet::new();
349
350 for (idx, opt) in options.iter().enumerate() {
351 if opt.name.trim().is_empty() {
353 return Err(ConfigError::InvalidSchema {
354 reason: "Option name cannot be empty".to_string(),
355 path: Some(format!("{}.options[{}]", context, idx)),
356 suggestion: None,
357 }
358 .into());
359 }
360
361 if !seen_names.insert(opt.name.clone()) {
363 return Err(ConfigError::InvalidSchema {
364 reason: format!("Duplicate option name: '{}'", opt.name),
365 path: Some(format!("{}.options", context)),
366 suggestion: None,
367 }
368 .into());
369 }
370
371 if opt.short.is_none() && opt.long.is_none() {
373 return Err(ConfigError::InvalidSchema {
374 reason: format!(
375 "Option '{}' must have at least a short or long form",
376 opt.name
377 ),
378 path: Some(format!("{}.options[{}]", context, idx)),
379 suggestion: None,
380 }
381 .into());
382 }
383
384 if opt.repeatable {
386 if let Some(ref default) = opt.default {
391 return Err(ConfigError::Inconsistency {
392 details: format!(
393 "Repeatable option '{}' cannot have a default value ('{}')",
394 opt.name, default
395 ),
396 suggestion: Some(
397 "Remove `default` — a repeatable option's absence already \
398 means zero occurrences, not an implicit one."
399 .to_string(),
400 ),
401 }
402 .into());
403 }
404
405 if opt.choices.is_empty() {
408 return Err(ConfigError::InvalidSchema {
409 reason: format!(
410 "Repeatable option '{}' must declare at least one discriminant in choices",
411 opt.name
412 ),
413 path: Some(format!("{}.options[{}].choices", context, idx)),
414 suggestion: Some(
415 "Add `choices: [...]` listing the valid discriminants for \
416 this repeatable option."
417 .to_string(),
418 ),
419 }
420 .into());
421 }
422
423 for discriminant in &opt.choices {
426 if !opt.option_parameters.contains_key(discriminant) {
427 return Err(ConfigError::InvalidSchema {
428 reason: format!(
429 "Discriminant '{}' is declared in choices for repeatable \
430 option '{}' but has no matching entry in option_parameters",
431 discriminant, opt.name
432 ),
433 path: Some(format!("{}.options[{}].option_parameters", context, idx)),
434 suggestion: Some(format!(
435 "Add an `option_parameters.{}` entry describing this \
436 discriminant's key=value parameters.",
437 discriminant
438 )),
439 }
440 .into());
441 }
442 }
443 let choices_set: HashSet<&String> = opt.choices.iter().collect();
444 for key in opt.option_parameters.keys() {
445 if !choices_set.contains(key) {
446 return Err(ConfigError::InvalidSchema {
447 reason: format!(
448 "option_parameters key '{}' on option '{}' is not declared in choices",
449 key, opt.name
450 ),
451 path: Some(format!(
452 "{}.options[{}].option_parameters.{}",
453 context, idx, key
454 )),
455 suggestion: Some(format!(
456 "Add '{}' to choices, or remove this option_parameters entry.",
457 key
458 )),
459 }
460 .into());
461 }
462 }
463
464 for (discriminant, params) in &opt.option_parameters {
469 let sub_context = format!(
470 "{}.options[{}].option_parameters.{}",
471 context, idx, discriminant
472 );
473 validate_argument_names(params, &sub_context)?;
474 validate_argument_types(params)?;
475 }
476 } else if !opt.option_parameters.is_empty() {
477 return Err(ConfigError::Inconsistency {
479 details: format!(
480 "Option '{}' has option_parameters but repeatable is false",
481 opt.name
482 ),
483 suggestion: Some(
484 "Set `repeatable: true`, or remove `option_parameters`.".to_string(),
485 ),
486 }
487 .into());
488 }
489
490 if let Some(ref default) = opt.default {
492 if !opt.choices.is_empty() && !opt.choices.contains(default) {
493 return Err(ConfigError::Inconsistency {
494 details: format!(
495 "Default value '{}' for option '{}' is not in choices: [{}]",
496 default,
497 opt.name,
498 opt.choices.join(", ")
499 ),
500 suggestion: None,
501 }
502 .into());
503 }
504 }
505
506 if opt.option_type == ArgumentType::Bool && !opt.choices.is_empty() {
508 return Err(ConfigError::Inconsistency {
509 details: format!("Boolean option '{}' cannot have choices", opt.name),
510 suggestion: None,
511 }
512 .into());
513 }
514 }
515
516 Ok(())
517}
518
519fn validate_option_flags(options: &[OptionDefinition], context: &str) -> Result<()> {
521 let mut seen_short: HashMap<String, String> = HashMap::new();
522 let mut seen_long: HashMap<String, String> = HashMap::new();
523
524 for opt in options {
525 if let Some(ref short) = opt.short {
527 if short.len() != 1 {
528 return Err(ConfigError::InvalidSchema {
529 reason: format!(
530 "Short option '{}' for '{}' must be a single character",
531 short, opt.name
532 ),
533 path: Some(format!("{}.options", context)),
534 suggestion: None,
535 }
536 .into());
537 }
538
539 if let Some(existing) = seen_short.insert(short.clone(), opt.name.clone()) {
540 return Err(ConfigError::InvalidSchema {
541 reason: format!(
542 "Short option '-{}' is used by both '{}' and '{}'",
543 short, existing, opt.name
544 ),
545 path: Some(format!("{}.options", context)),
546 suggestion: None,
547 }
548 .into());
549 }
550 }
551
552 if let Some(ref long) = opt.long {
554 if long.is_empty() {
555 return Err(ConfigError::InvalidSchema {
556 reason: format!("Long option for '{}' cannot be empty", opt.name),
557 path: Some(format!("{}.options", context)),
558 suggestion: None,
559 }
560 .into());
561 }
562
563 if let Some(existing) = seen_long.insert(long.clone(), opt.name.clone()) {
564 return Err(ConfigError::InvalidSchema {
565 reason: format!(
566 "Long option '--{}' is used by both '{}' and '{}'",
567 long, existing, opt.name
568 ),
569 path: Some(format!("{}.options", context)),
570 suggestion: None,
571 }
572 .into());
573 }
574 }
575 }
576
577 Ok(())
578}
579
580fn check_name_conflicts(
582 args: &[ArgumentDefinition],
583 options: &[OptionDefinition],
584 context: &str,
585) -> Result<()> {
586 let arg_names: HashSet<String> = args.iter().map(|a| a.name.clone()).collect();
587
588 for opt in options {
589 if arg_names.contains(&opt.name) {
590 return Err(ConfigError::InvalidSchema {
591 reason: format!("Option '{}' has the same name as an argument", opt.name),
592 path: Some(format!("{}.options", context)),
593 suggestion: None,
594 }
595 .into());
596 }
597 }
598
599 Ok(())
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::config::schema::CommandsConfig;
606 use std::collections::HashMap;
607
608 #[test]
609 fn test_validate_config_empty() {
610 let config = CommandsConfig::minimal();
611 assert!(validate_config(&config).is_ok());
612 }
613
614 #[test]
615 fn test_validate_config_duplicate_command_name() {
616 let mut config = CommandsConfig::minimal();
617 config.commands = vec![
618 CommandDefinition {
619 name: "test".to_string(),
620 aliases: vec![],
621 description: "Test 1".to_string(),
622 required: false,
623 arguments: vec![],
624 options: vec![],
625 implementation: "handler1".to_string(),
626 continue_on_failure: false,
627 requires_success: false,
628 },
629 CommandDefinition {
630 name: "test".to_string(), aliases: vec![],
632 description: "Test 2".to_string(),
633 required: false,
634 arguments: vec![],
635 options: vec![],
636 implementation: "handler2".to_string(),
637 continue_on_failure: false,
638 requires_success: false,
639 },
640 ];
641
642 let result = validate_config(&config);
643 assert!(result.is_err());
644 match result.unwrap_err() {
645 crate::error::DynamicCliError::Config(ConfigError::DuplicateCommand {
646 name, ..
647 }) => {
648 assert_eq!(name, "test");
649 }
650 other => panic!("Expected DuplicateCommand error, got {:?}", other),
651 }
652 }
653
654 #[test]
655 fn test_validate_config_duplicate_alias() {
656 let mut config = CommandsConfig::minimal();
657 config.commands = vec![
658 CommandDefinition {
659 name: "cmd1".to_string(),
660 aliases: vec!["c".to_string()],
661 description: "Command 1".to_string(),
662 required: false,
663 arguments: vec![],
664 options: vec![],
665 implementation: "handler1".to_string(),
666 continue_on_failure: false,
667 requires_success: false,
668 },
669 CommandDefinition {
670 name: "cmd2".to_string(),
671 aliases: vec!["c".to_string()], description: "Command 2".to_string(),
673 required: false,
674 arguments: vec![],
675 options: vec![],
676 implementation: "handler2".to_string(),
677 continue_on_failure: false,
678 requires_success: false,
679 },
680 ];
681
682 let result = validate_config(&config);
683 assert!(result.is_err());
684 }
685
686 #[test]
687 fn test_validate_command_empty_name() {
688 let cmd = CommandDefinition {
689 name: "".to_string(), aliases: vec![],
691 description: "Test".to_string(),
692 required: false,
693 arguments: vec![],
694 options: vec![],
695 implementation: "handler".to_string(),
696 continue_on_failure: false,
697 requires_success: false,
698 };
699
700 let mut config = CommandsConfig::minimal();
701 config.commands = vec![cmd];
702
703 let result = validate_config(&config);
704 assert!(result.is_err());
705 }
706
707 #[test]
708 fn test_validate_argument_ordering() {
709 let args = vec![
710 ArgumentDefinition {
711 name: "optional".to_string(),
712 arg_type: ArgumentType::String,
713 required: false,
714 description: "Optional".to_string(),
715 validation: vec![],
716 secure: false,
717 },
718 ArgumentDefinition {
719 name: "required".to_string(),
720 arg_type: ArgumentType::String,
721 required: true, description: "Required".to_string(),
723 validation: vec![],
724 secure: false,
725 },
726 ];
727
728 let result = validate_argument_ordering(&args, "test");
729 assert!(result.is_err());
730 }
731
732 #[test]
733 fn test_validate_argument_names_duplicate() {
734 let args = vec![
735 ArgumentDefinition {
736 name: "arg1".to_string(),
737 arg_type: ArgumentType::String,
738 required: true,
739 description: "Arg 1".to_string(),
740 validation: vec![],
741 secure: false,
742 },
743 ArgumentDefinition {
744 name: "arg1".to_string(), arg_type: ArgumentType::Integer,
746 required: true,
747 description: "Arg 1 again".to_string(),
748 validation: vec![],
749 secure: false,
750 },
751 ];
752
753 let result = validate_argument_names(&args, "test");
754 assert!(result.is_err());
755 }
756
757 #[test]
758 fn test_validate_validation_rules_type_mismatch() {
759 let args = vec![ArgumentDefinition {
760 name: "count".to_string(),
761 arg_type: ArgumentType::Integer,
762 required: true,
763 description: "Count".to_string(),
764 validation: vec![
765 ValidationRule::MustExist { must_exist: true }, ],
767 secure: false,
768 }];
769
770 let result = validate_argument_validation_rules(&args, "test");
771 assert!(result.is_err());
772 }
773
774 #[test]
775 fn test_validate_validation_rules_invalid_range() {
776 let args = vec![ArgumentDefinition {
777 name: "percentage".to_string(),
778 arg_type: ArgumentType::Float,
779 required: true,
780 description: "Percentage".to_string(),
781 validation: vec![ValidationRule::Range {
782 min: Some(100.0),
783 max: Some(0.0), }],
785 secure: false,
786 }];
787
788 let result = validate_argument_validation_rules(&args, "test");
789 assert!(result.is_err());
790 }
791
792 #[test]
793 fn test_validate_options_no_flags() {
794 let options = vec![OptionDefinition {
795 name: "opt1".to_string(),
796 short: None,
797 long: None, option_type: ArgumentType::String,
799 required: false,
800 default: None,
801 description: "Option".to_string(),
802 choices: vec![],
803 repeatable: false,
804 option_parameters: HashMap::new(),
805 }];
806
807 let result = validate_options(&options, "test");
808 assert!(result.is_err());
809 }
810
811 #[test]
812 fn test_validate_options_default_not_in_choices() {
813 let options = vec![OptionDefinition {
814 name: "mode".to_string(),
815 short: Some("m".to_string()),
816 long: Some("mode".to_string()),
817 option_type: ArgumentType::String,
818 required: false,
819 default: Some("invalid".to_string()), description: "Mode".to_string(),
821 choices: vec!["fast".to_string(), "slow".to_string()],
822 repeatable: false,
823 option_parameters: HashMap::new(),
824 }];
825
826 let result = validate_options(&options, "test");
827 assert!(result.is_err());
828 }
829
830 #[test]
831 fn test_validate_option_flags_duplicate_short() {
832 let options = vec![
833 OptionDefinition {
834 name: "opt1".to_string(),
835 short: Some("o".to_string()),
836 long: None,
837 option_type: ArgumentType::String,
838 required: false,
839 default: None,
840 description: "Option 1".to_string(),
841 choices: vec![],
842 repeatable: false,
843 option_parameters: HashMap::new(),
844 },
845 OptionDefinition {
846 name: "opt2".to_string(),
847 short: Some("o".to_string()), long: None,
849 option_type: ArgumentType::String,
850 required: false,
851 default: None,
852 description: "Option 2".to_string(),
853 choices: vec![],
854 repeatable: false,
855 option_parameters: HashMap::new(),
856 },
857 ];
858
859 let result = validate_option_flags(&options, "test");
860 assert!(result.is_err());
861 }
862
863 #[test]
864 fn test_validate_option_flags_invalid_short() {
865 let options = vec![OptionDefinition {
866 name: "opt1".to_string(),
867 short: Some("opt".to_string()), long: None,
869 option_type: ArgumentType::String,
870 required: false,
871 default: None,
872 description: "Option".to_string(),
873 choices: vec![],
874 repeatable: false,
875 option_parameters: HashMap::new(),
876 }];
877
878 let result = validate_option_flags(&options, "test");
879 assert!(result.is_err());
880 }
881
882 #[test]
883 fn test_check_name_conflicts() {
884 let args = vec![ArgumentDefinition {
885 name: "output".to_string(),
886 arg_type: ArgumentType::Path,
887 required: true,
888 description: "Output".to_string(),
889 validation: vec![],
890 secure: false,
891 }];
892
893 let options = vec![OptionDefinition {
894 name: "output".to_string(), short: Some("o".to_string()),
896 long: Some("output".to_string()),
897 option_type: ArgumentType::Path,
898 required: false,
899 default: None,
900 description: "Output".to_string(),
901 choices: vec![],
902 repeatable: false,
903 option_parameters: HashMap::new(),
904 }];
905
906 let result = check_name_conflicts(&args, &options, "test");
907 assert!(result.is_err());
908 }
909
910 #[test]
911 fn test_validate_command_valid() {
912 let cmd = CommandDefinition {
913 name: "process".to_string(),
914 aliases: vec!["proc".to_string()],
915 description: "Process data".to_string(),
916 required: false,
917 arguments: vec![ArgumentDefinition {
918 name: "input".to_string(),
919 arg_type: ArgumentType::Path,
920 required: true,
921 description: "Input file".to_string(),
922 validation: vec![
923 ValidationRule::MustExist { must_exist: true },
924 ValidationRule::Extensions {
925 extensions: vec!["csv".to_string()],
926 },
927 ],
928 secure: false,
929 }],
930 options: vec![OptionDefinition {
931 name: "output".to_string(),
932 short: Some("o".to_string()),
933 long: Some("output".to_string()),
934 option_type: ArgumentType::Path,
935 required: false,
936 default: Some("out.csv".to_string()),
937 description: "Output file".to_string(),
938 choices: vec![],
939 repeatable: false,
940 option_parameters: HashMap::new(),
941 }],
942 implementation: "process_handler".to_string(),
943 continue_on_failure: false,
944 requires_success: false,
945 };
946
947 assert!(validate_command(&cmd).is_ok());
948 }
949
950 #[test]
951 fn test_validate_boolean_with_choices() {
952 let options = vec![OptionDefinition {
953 name: "flag".to_string(),
954 short: Some("f".to_string()),
955 long: Some("flag".to_string()),
956 option_type: ArgumentType::Bool,
957 required: false,
958 default: None,
959 description: "A flag".to_string(),
960 choices: vec!["true".to_string(), "false".to_string()], repeatable: false,
962 option_parameters: HashMap::new(),
963 }];
964
965 let result = validate_options(&options, "test");
966 assert!(result.is_err());
967 }
968
969 #[test]
972 fn test_validate_repeatable_requires_non_empty_choices() {
973 let options = vec![OptionDefinition {
974 name: "output".to_string(),
975 short: None,
976 long: Some("output".to_string()),
977 option_type: ArgumentType::String,
978 required: false,
979 default: None,
980 description: "Output".to_string(),
981 choices: vec![], repeatable: true,
983 option_parameters: HashMap::new(),
984 }];
985
986 let result = validate_options(&options, "test");
987 assert!(result.is_err());
988 }
989
990 #[test]
991 fn test_validate_repeatable_missing_option_parameters_entry() {
992 let mut option_parameters = HashMap::new();
993 option_parameters.insert(
994 "csv".to_string(),
995 vec![ArgumentDefinition {
996 name: "file".to_string(),
997 arg_type: ArgumentType::Path,
998 required: true,
999 description: "Destination file".to_string(),
1000 validation: vec![],
1001 secure: false,
1002 }],
1003 );
1004 let options = vec![OptionDefinition {
1006 name: "output".to_string(),
1007 short: None,
1008 long: Some("output".to_string()),
1009 option_type: ArgumentType::String,
1010 required: false,
1011 default: None,
1012 description: "Output".to_string(),
1013 choices: vec!["csv".to_string(), "plot".to_string()],
1014 repeatable: true,
1015 option_parameters,
1016 }];
1017
1018 let result = validate_options(&options, "test");
1019 assert!(result.is_err());
1020 }
1021
1022 #[test]
1023 fn test_validate_repeatable_orphan_option_parameters_key() {
1024 let mut option_parameters = HashMap::new();
1025 option_parameters.insert(
1026 "csv".to_string(),
1027 vec![ArgumentDefinition {
1028 name: "file".to_string(),
1029 arg_type: ArgumentType::Path,
1030 required: true,
1031 description: "Destination file".to_string(),
1032 validation: vec![],
1033 secure: false,
1034 }],
1035 );
1036 option_parameters.insert(
1038 "json".to_string(),
1039 vec![ArgumentDefinition {
1040 name: "file".to_string(),
1041 arg_type: ArgumentType::Path,
1042 required: true,
1043 description: "Destination file".to_string(),
1044 validation: vec![],
1045 secure: false,
1046 }],
1047 );
1048 let options = vec![OptionDefinition {
1049 name: "output".to_string(),
1050 short: None,
1051 long: Some("output".to_string()),
1052 option_type: ArgumentType::String,
1053 required: false,
1054 default: None,
1055 description: "Output".to_string(),
1056 choices: vec!["csv".to_string()],
1057 repeatable: true,
1058 option_parameters,
1059 }];
1060
1061 let result = validate_options(&options, "test");
1062 assert!(result.is_err());
1063 }
1064
1065 #[test]
1066 fn test_validate_repeatable_option_parameters_reuses_argument_validation() {
1067 let mut option_parameters = HashMap::new();
1068 option_parameters.insert(
1069 "csv".to_string(),
1070 vec![ArgumentDefinition {
1071 name: "".to_string(), arg_type: ArgumentType::Path,
1073 required: true,
1074 description: "Destination file".to_string(),
1075 validation: vec![],
1076 secure: false,
1077 }],
1078 );
1079 let options = vec![OptionDefinition {
1080 name: "output".to_string(),
1081 short: None,
1082 long: Some("output".to_string()),
1083 option_type: ArgumentType::String,
1084 required: false,
1085 default: None,
1086 description: "Output".to_string(),
1087 choices: vec!["csv".to_string()],
1088 repeatable: true,
1089 option_parameters,
1090 }];
1091
1092 let result = validate_options(&options, "test");
1093 assert!(result.is_err());
1094 }
1095
1096 #[test]
1097 fn test_validate_non_repeatable_with_option_parameters_is_error() {
1098 let mut option_parameters = HashMap::new();
1099 option_parameters.insert(
1100 "csv".to_string(),
1101 vec![ArgumentDefinition {
1102 name: "file".to_string(),
1103 arg_type: ArgumentType::Path,
1104 required: true,
1105 description: "Destination file".to_string(),
1106 validation: vec![],
1107 secure: false,
1108 }],
1109 );
1110 let options = vec![OptionDefinition {
1111 name: "output".to_string(),
1112 short: None,
1113 long: Some("output".to_string()),
1114 option_type: ArgumentType::String,
1115 required: false,
1116 default: None,
1117 description: "Output".to_string(),
1118 choices: vec!["csv".to_string()],
1119 repeatable: false, option_parameters,
1121 }];
1122
1123 let result = validate_options(&options, "test");
1124 assert!(result.is_err());
1125 }
1126
1127 #[test]
1128 fn test_validate_repeatable_with_default_is_error() {
1129 let mut option_parameters = HashMap::new();
1130 option_parameters.insert(
1131 "csv".to_string(),
1132 vec![ArgumentDefinition {
1133 name: "file".to_string(),
1134 arg_type: ArgumentType::Path,
1135 required: true,
1136 description: "Destination file".to_string(),
1137 validation: vec![],
1138 secure: false,
1139 }],
1140 );
1141 let options = vec![OptionDefinition {
1142 name: "output".to_string(),
1143 short: None,
1144 long: Some("output".to_string()),
1145 option_type: ArgumentType::String,
1146 required: false,
1147 default: Some("csv".to_string()), description: "Output".to_string(),
1149 choices: vec!["csv".to_string()],
1150 repeatable: true,
1151 option_parameters,
1152 }];
1153
1154 let result = validate_options(&options, "test");
1155 assert!(result.is_err());
1156 }
1157
1158 #[test]
1159 fn test_validate_repeatable_valid_config_passes() {
1160 let mut option_parameters = HashMap::new();
1161 option_parameters.insert(
1162 "csv".to_string(),
1163 vec![
1164 ArgumentDefinition {
1165 name: "file".to_string(),
1166 arg_type: ArgumentType::Path,
1167 required: true,
1168 description: "Destination CSV file".to_string(),
1169 validation: vec![],
1170 secure: false,
1171 },
1172 ArgumentDefinition {
1173 name: "resolution".to_string(),
1174 arg_type: ArgumentType::Integer,
1175 required: false,
1176 description: "Time-step resolution".to_string(),
1177 validation: vec![],
1178 secure: false,
1179 },
1180 ],
1181 );
1182 option_parameters.insert(
1183 "plot".to_string(),
1184 vec![ArgumentDefinition {
1185 name: "file".to_string(),
1186 arg_type: ArgumentType::Path,
1187 required: true,
1188 description: "Destination image file".to_string(),
1189 validation: vec![],
1190 secure: false,
1191 }],
1192 );
1193 let options = vec![OptionDefinition {
1194 name: "output".to_string(),
1195 short: None,
1196 long: Some("output".to_string()),
1197 option_type: ArgumentType::String,
1198 required: false,
1199 default: None,
1200 description: "Write simulation results in one or more output kinds".to_string(),
1201 choices: vec!["csv".to_string(), "plot".to_string()],
1202 repeatable: true,
1203 option_parameters,
1204 }];
1205
1206 let result = validate_options(&options, "test");
1207 assert!(result.is_ok());
1208 }
1209}