use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct CommandsConfig {
pub metadata: Metadata,
pub commands: Vec<CommandDefinition>,
#[serde(default)]
pub global_options: Vec<OptionDefinition>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Metadata {
pub version: String,
pub prompt: String,
#[serde(default = "default_prompt_suffix")]
pub prompt_suffix: String,
}
fn default_prompt_suffix() -> String {
" > ".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct CommandDefinition {
pub name: String,
#[serde(default)]
pub aliases: Vec<String>,
pub description: String,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub arguments: Vec<ArgumentDefinition>,
#[serde(default)]
pub options: Vec<OptionDefinition>,
pub implementation: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ArgumentDefinition {
pub name: String,
pub arg_type: ArgumentType,
pub required: bool,
pub description: String,
#[serde(default)]
pub validation: Vec<ValidationRule>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct OptionDefinition {
pub name: String,
pub short: Option<String>,
pub long: Option<String>,
pub option_type: ArgumentType,
#[serde(default)]
pub required: bool,
pub default: Option<String>,
pub description: String,
#[serde(default)]
pub choices: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ArgumentType {
String,
Integer,
Float,
Bool,
Path,
}
impl ArgumentType {
pub fn as_str(&self) -> &'static str {
match self {
ArgumentType::String => "string",
ArgumentType::Integer => "integer",
ArgumentType::Float => "float",
ArgumentType::Bool => "bool",
ArgumentType::Path => "path",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum ValidationRule {
MustExist { must_exist: bool },
Extensions { extensions: Vec<String> },
Range { min: Option<f64>, max: Option<f64> },
}
impl CommandsConfig {
#[cfg(test)]
pub fn minimal() -> Self {
Self {
metadata: Metadata {
version: "0.1.0".to_string(),
prompt: "test".to_string(),
prompt_suffix: " > ".to_string(),
},
commands: vec![],
global_options: vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_argument_type_as_str() {
assert_eq!(ArgumentType::String.as_str(), "string");
assert_eq!(ArgumentType::Integer.as_str(), "integer");
assert_eq!(ArgumentType::Float.as_str(), "float");
assert_eq!(ArgumentType::Bool.as_str(), "bool");
assert_eq!(ArgumentType::Path.as_str(), "path");
}
#[test]
fn test_default_prompt_suffix() {
assert_eq!(default_prompt_suffix(), " > ");
}
#[test]
fn test_minimal_config() {
let config = CommandsConfig::minimal();
assert_eq!(config.metadata.version, "0.1.0");
assert_eq!(config.metadata.prompt, "test");
assert_eq!(config.metadata.prompt_suffix, " > ");
assert!(config.commands.is_empty());
assert!(config.global_options.is_empty());
}
#[test]
fn test_deserialize_argument_type() {
let yaml = r#"
type: string
"#;
#[derive(Deserialize)]
struct TestStruct {
#[serde(rename = "type")]
type_field: ArgumentType,
}
let result: TestStruct = serde_yaml::from_str(yaml).unwrap();
assert_eq!(result.type_field, ArgumentType::String);
}
#[test]
fn test_deserialize_metadata() {
let yaml = r#"
version: "1.0.0"
prompt: "myapp"
prompt_suffix: " $ "
"#;
let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
assert_eq!(metadata.version, "1.0.0");
assert_eq!(metadata.prompt, "myapp");
assert_eq!(metadata.prompt_suffix, " $ ");
}
#[test]
fn test_deserialize_metadata_with_default() {
let yaml = r#"
version: "1.0.0"
prompt: "myapp"
"#;
let metadata: Metadata = serde_yaml::from_str(yaml).unwrap();
assert_eq!(metadata.prompt_suffix, " > ");
}
#[test]
fn test_deserialize_command_definition() {
let yaml = r#"
name: test_cmd
aliases: [tc, test]
description: "A test command"
required: true
arguments: []
options: []
implementation: "test_handler"
"#;
let cmd: CommandDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cmd.name, "test_cmd");
assert_eq!(cmd.aliases, vec!["tc", "test"]);
assert_eq!(cmd.description, "A test command");
assert!(cmd.required);
assert_eq!(cmd.implementation, "test_handler");
}
#[test]
fn test_deserialize_argument_definition() {
let yaml = r#"
name: input_file
arg_type: path
required: true
description: "Input file"
validation:
- must_exist: true
- extensions: [yaml, yml]
"#;
let arg: ArgumentDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(arg.name, "input_file");
assert_eq!(arg.arg_type, ArgumentType::Path);
assert!(arg.required);
assert_eq!(arg.description, "Input file");
assert_eq!(arg.validation.len(), 2);
}
#[test]
fn test_deserialize_option_definition() {
let yaml = r#"
name: output
short: o
long: output
option_type: path
required: false
default: "out.txt"
description: "Output file"
choices: []
"#;
let opt: OptionDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(opt.name, "output");
assert_eq!(opt.short, Some("o".to_string()));
assert_eq!(opt.long, Some("output".to_string()));
assert_eq!(opt.option_type, ArgumentType::Path);
assert!(!opt.required);
assert_eq!(opt.default, Some("out.txt".to_string()));
}
#[test]
fn test_deserialize_validation_rule_must_exist() {
let yaml = r#"
must_exist: true
"#;
let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
assert_eq!(rule, ValidationRule::MustExist { must_exist: true });
}
#[test]
fn test_deserialize_validation_rule_extensions() {
let yaml = r#"
extensions: [yaml, yml, json]
"#;
let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
match rule {
ValidationRule::Extensions { extensions } => {
assert_eq!(extensions, vec!["yaml", "yml", "json"]);
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_deserialize_validation_rule_range() {
let yaml = r#"
min: 0.0
max: 100.0
"#;
let rule: ValidationRule = serde_yaml::from_str(yaml).unwrap();
match rule {
ValidationRule::Range { min, max } => {
assert_eq!(min, Some(0.0));
assert_eq!(max, Some(100.0));
}
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_deserialize_full_config() {
let yaml = r#"
metadata:
version: "1.0.0"
prompt: "test"
prompt_suffix: " > "
commands:
- name: hello
aliases: []
description: "Say hello"
required: false
arguments: []
options: []
implementation: "hello_handler"
global_options: []
"#;
let config: CommandsConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "hello");
}
#[test]
fn test_serialize_and_deserialize_roundtrip() {
let original = CommandsConfig {
metadata: Metadata {
version: "1.0.0".to_string(),
prompt: "test".to_string(),
prompt_suffix: " > ".to_string(),
},
commands: vec![CommandDefinition {
name: "cmd1".to_string(),
aliases: vec!["c1".to_string()],
description: "Test command".to_string(),
required: true,
arguments: vec![],
options: vec![],
implementation: "handler1".to_string(),
}],
global_options: vec![],
};
let yaml = serde_yaml::to_string(&original).unwrap();
let deserialized: CommandsConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_json_deserialization() {
let json = r#"
{
"metadata": {
"version": "1.0.0",
"prompt": "test",
"prompt_suffix": " > "
},
"commands": [],
"global_options": []
}
"#;
let config: CommandsConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
assert_eq!(config.commands.len(), 0);
}
}