1use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
34pub struct CommandsConfig {
35 pub metadata: Metadata,
37
38 pub commands: Vec<CommandDefinition>,
40
41 #[serde(default)]
43 pub global_options: Vec<OptionDefinition>,
44}
45
46#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
57pub struct Metadata {
58 pub version: String,
60
61 pub prompt: String,
65
66 #[serde(default = "default_prompt_suffix")]
68 pub prompt_suffix: String,
69}
70
71fn default_prompt_suffix() -> String {
73 " > ".to_string()
74}
75
76#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
100pub struct CommandDefinition {
101 pub name: String,
103
104 #[serde(default)]
106 pub aliases: Vec<String>,
107
108 pub description: String,
110
111 #[serde(default)]
115 pub required: bool,
116
117 #[serde(default)]
119 pub arguments: Vec<ArgumentDefinition>,
120
121 #[serde(default)]
123 pub options: Vec<OptionDefinition>,
124
125 pub implementation: String,
130}
131
132#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
165pub struct ArgumentDefinition {
166 pub name: String,
168
169 pub arg_type: ArgumentType,
171
172 pub required: bool,
174
175 pub description: String,
177
178 #[serde(default)]
180 pub validation: Vec<ValidationRule>,
181
182 #[serde(default)]
187 pub secure: bool,
188}
189
190#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
208pub struct OptionDefinition {
209 pub name: String,
211
212 pub short: Option<String>,
214
215 pub long: Option<String>,
217
218 pub option_type: ArgumentType,
220
221 #[serde(default)]
223 pub required: bool,
224
225 pub default: Option<String>,
227
228 pub description: String,
230
231 #[serde(default)]
235 pub choices: Vec<String>,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
252#[serde(rename_all = "lowercase")]
253pub enum ArgumentType {
254 String,
256
257 Integer,
259
260 Float,
262
263 Bool,
265
266 Path,
271}
272
273impl ArgumentType {
274 pub fn as_str(&self) -> &'static str {
285 match self {
286 ArgumentType::String => "string",
287 ArgumentType::Integer => "integer",
288 ArgumentType::Float => "float",
289 ArgumentType::Bool => "bool",
290 ArgumentType::Path => "path",
291 }
292 }
293}
294
295#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
322#[serde(untagged)]
323pub enum ValidationRule {
324 MustExist { must_exist: bool },
326
327 Extensions { extensions: Vec<String> },
332
333 Range { min: Option<f64>, max: Option<f64> },
340}
341
342impl CommandsConfig {
343 #[cfg(test)]
357 pub fn minimal() -> Self {
358 Self {
359 metadata: Metadata {
360 version: "0.1.0".to_string(),
361 prompt: "test".to_string(),
362 prompt_suffix: " > ".to_string(),
363 },
364 commands: vec![],
365 global_options: vec![],
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_argument_type_as_str() {
376 assert_eq!(ArgumentType::String.as_str(), "string");
377 assert_eq!(ArgumentType::Integer.as_str(), "integer");
378 assert_eq!(ArgumentType::Float.as_str(), "float");
379 assert_eq!(ArgumentType::Bool.as_str(), "bool");
380 assert_eq!(ArgumentType::Path.as_str(), "path");
381 }
382
383 #[test]
384 fn test_default_prompt_suffix() {
385 assert_eq!(default_prompt_suffix(), " > ");
386 }
387
388 #[test]
389 fn test_minimal_config() {
390 let config = CommandsConfig::minimal();
391
392 assert_eq!(config.metadata.version, "0.1.0");
393 assert_eq!(config.metadata.prompt, "test");
394 assert_eq!(config.metadata.prompt_suffix, " > ");
395 assert!(config.commands.is_empty());
396 assert!(config.global_options.is_empty());
397 }
398
399 #[test]
400 fn test_deserialize_argument_type() {
401 let yaml = r#"
403 type: string
404 "#;
405
406 #[derive(Deserialize)]
407 struct TestStruct {
408 #[serde(rename = "type")]
409 type_field: ArgumentType,
410 }
411
412 let result: TestStruct = serde_yaml::from_str(yaml).unwrap();
413 assert_eq!(result.type_field, ArgumentType::String);
414 }
415
416 #[test]
417 fn test_deserialize_metadata() {
418 let yaml = r#"
419 version: "1.0.0"
420 prompt: "myapp"
421 prompt_suffix: " $ "
422 "#;
423
424 let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
425
426 assert_eq!(metadata.version, "1.0.0");
427 assert_eq!(metadata.prompt, "myapp");
428 assert_eq!(metadata.prompt_suffix, " $ ");
429 }
430
431 #[test]
432 fn test_deserialize_metadata_with_default() {
433 let yaml = r#"
435 version: "1.0.0"
436 prompt: "myapp"
437 "#;
438
439 let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
440
441 assert_eq!(metadata.prompt_suffix, " > ");
442 }
443
444 #[test]
445 fn test_deserialize_command_definition() {
446 let yaml = r#"
447 name: test_cmd
448 aliases: [tc, test]
449 description: "A test command"
450 required: true
451 arguments: []
452 options: []
453 implementation: "test_handler"
454 "#;
455
456 let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
457
458 assert_eq!(cmd.name, "test_cmd");
459 assert_eq!(cmd.aliases, vec!["tc", "test"]);
460 assert_eq!(cmd.description, "A test command");
461 assert!(cmd.required);
462 assert_eq!(cmd.implementation, "test_handler");
463 }
464
465 #[test]
466 fn test_deserialize_argument_definition() {
467 let yaml = r#"
468 name: input_file
469 arg_type: path
470 required: true
471 description: "Input file"
472 validation:
473 - must_exist: true
474 - extensions: [yaml, yml]
475 "#;
476
477 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
478
479 assert_eq!(arg.name, "input_file");
480 assert_eq!(arg.arg_type, ArgumentType::Path);
481 assert!(arg.required);
482 assert_eq!(arg.description, "Input file");
483 assert_eq!(arg.validation.len(), 2);
484 assert!(!arg.secure);
486 }
487
488 #[test]
489 fn test_deserialize_argument_definition_secure_default_false() {
490 let yaml = r#"
492 name: username
493 arg_type: string
494 required: true
495 description: "User name"
496 "#;
497
498 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
499 assert!(!arg.secure, "secure must default to false when absent");
500 }
501
502 #[test]
503 fn test_deserialize_argument_definition_secure_true() {
504 let yaml = r#"
506 name: password
507 arg_type: string
508 required: true
509 description: "User password"
510 secure: true
511 "#;
512
513 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
514 assert!(arg.secure, "secure must be true when set in YAML");
515 }
516
517 #[test]
518 fn test_deserialize_argument_definition_secure_false_explicit() {
519 let yaml = r#"
521 name: output
522 arg_type: path
523 required: false
524 description: "Output path"
525 secure: false
526 "#;
527
528 let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
529 assert!(!arg.secure);
530 }
531
532 #[test]
533 fn test_serialize_argument_definition_secure_roundtrip() {
534 let original = ArgumentDefinition {
535 name: "secret".to_string(),
536 arg_type: ArgumentType::String,
537 required: true,
538 description: "A secret value".to_string(),
539 validation: vec![],
540 secure: true,
541 };
542
543 let yaml = serde_yaml::to_string(&original).unwrap();
544 let deserialized: ArgumentDefinition = serde_yaml::from_str(&yaml).unwrap();
545
546 assert_eq!(original, deserialized);
547 assert!(deserialized.secure);
548 }
549
550 #[test]
551 fn test_deserialize_option_definition() {
552 let yaml = r#"
553 name: output
554 short: o
555 long: output
556 option_type: path
557 required: false
558 default: "out.txt"
559 description: "Output file"
560 choices: []
561 "#;
562
563 let opt: OptionDefinition = serde_yaml::from_str(yaml).unwrap();
564
565 assert_eq!(opt.name, "output");
566 assert_eq!(opt.short, Some("o".to_string()));
567 assert_eq!(opt.long, Some("output".to_string()));
568 assert_eq!(opt.option_type, ArgumentType::Path);
569 assert!(!opt.required);
570 assert_eq!(opt.default, Some("out.txt".to_string()));
571 }
572
573 #[test]
574 fn test_deserialize_validation_rule_must_exist() {
575 let yaml = r#"
576 must_exist: true
577 "#;
578
579 let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
580
581 assert_eq!(rule, ValidationRule::MustExist { must_exist: true });
582 }
583
584 #[test]
585 fn test_deserialize_validation_rule_extensions() {
586 let yaml = r#"
587 extensions: [yaml, yml, json]
588 "#;
589
590 let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
591
592 match rule {
593 ValidationRule::Extensions { extensions } => {
594 assert_eq!(extensions, vec!["yaml", "yml", "json"]);
595 }
596 _ => panic!("Wrong variant"),
597 }
598 }
599
600 #[test]
601 fn test_deserialize_validation_rule_range() {
602 let yaml = r#"
603 min: 0.0
604 max: 100.0
605 "#;
606
607 let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
608
609 match rule {
610 ValidationRule::Range { min, max } => {
611 assert_eq!(min, Some(0.0));
612 assert_eq!(max, Some(100.0));
613 }
614 _ => panic!("Wrong variant"),
615 }
616 }
617
618 #[test]
619 fn test_deserialize_full_config() {
620 let yaml = r#"
621 metadata:
622 version: "1.0.0"
623 prompt: "test"
624 prompt_suffix: " > "
625 commands:
626 - name: hello
627 aliases: []
628 description: "Say hello"
629 required: false
630 arguments: []
631 options: []
632 implementation: "hello_handler"
633 global_options: []
634 "#;
635
636 let config: CommandsConfig = serde_yaml::from_str(yaml).unwrap();
637
638 assert_eq!(config.metadata.version, "1.0.0");
639 assert_eq!(config.commands.len(), 1);
640 assert_eq!(config.commands[0].name, "hello");
641 }
642
643 #[test]
644 fn test_serialize_and_deserialize_roundtrip() {
645 let original = CommandsConfig {
646 metadata: Metadata {
647 version: "1.0.0".to_string(),
648 prompt: "test".to_string(),
649 prompt_suffix: " > ".to_string(),
650 },
651 commands: vec![CommandDefinition {
652 name: "cmd1".to_string(),
653 aliases: vec!["c1".to_string()],
654 description: "Test command".to_string(),
655 required: true,
656 arguments: vec![],
657 options: vec![],
658 implementation: "handler1".to_string(),
659 }],
660 global_options: vec![],
661 };
662
663 let yaml = serde_yaml::to_string(&original).unwrap();
665
666 let deserialized: CommandsConfig = serde_yaml::from_str(&yaml).unwrap();
668
669 assert_eq!(original, deserialized);
670 }
671
672 #[test]
673 fn test_json_deserialization() {
674 let json = r#"
675 {
676 "metadata": {
677 "version": "1.0.0",
678 "prompt": "test",
679 "prompt_suffix": " > "
680 },
681 "commands": [],
682 "global_options": []
683 }
684 "#;
685
686 let config: CommandsConfig = serde_json::from_str(json).unwrap();
687
688 assert_eq!(config.metadata.version, "1.0.0");
689 assert_eq!(config.commands.len(), 0);
690 }
691}