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