use crate::config::schema::CommandsConfig;
use crate::error::{ConfigError, DynamicCliError, Result};
use std::fs;
use std::path::Path;
pub fn load_config<P: AsRef<Path>>(path: P) -> Result<CommandsConfig> {
let path = path.as_ref();
if !path.exists() {
return Err(ConfigError::file_not_found(path.to_path_buf()).into());
}
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| ConfigError::unsupported_format("<none>"))?;
let content = fs::read_to_string(path).map_err(DynamicCliError::from)?;
match extension.to_lowercase().as_str() {
"yaml" | "yml" => load_yaml(&content),
"json" => load_json(&content),
other => Err(ConfigError::unsupported_format(other).into()),
}
}
pub fn load_yaml(content: &str) -> Result<CommandsConfig> {
serde_yaml::from_str(content).map_err(|e| {
ConfigError::yaml_parse_with_location(e).into()
})
}
pub fn load_json(content: &str) -> Result<CommandsConfig> {
serde_json::from_str(content).map_err(|e| {
ConfigError::json_parse_with_location(e).into()
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(extension)
.tempfile()
.unwrap();
file.write_all(content.as_bytes()).unwrap();
file.flush().unwrap();
file
}
#[test]
fn test_load_yaml_valid() {
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 = load_yaml(yaml).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
assert_eq!(config.metadata.prompt, "test");
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].name, "hello");
}
#[test]
fn test_load_yaml_invalid_syntax() {
let yaml = r#"
metadata:
version: "1.0.0"
prompt: "test"
commands: [
"#;
let result = load_yaml(yaml);
assert!(result.is_err());
match result.unwrap_err() {
DynamicCliError::Config(ConfigError::YamlParse { .. }) => {}
other => panic!("Expected YamlParse error, got {:?}", other),
}
}
#[test]
fn test_load_json_valid() {
let json = 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 = load_json(json).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_load_json_invalid_syntax() {
let json = r#"
{
"metadata": {
"version": "1.0.0",
"prompt": "test"
},
"commands": [
"#;
let result = load_json(json);
assert!(result.is_err());
match result.unwrap_err() {
DynamicCliError::Config(ConfigError::JsonParse { .. }) => {}
other => panic!("Expected JsonParse error, got {:?}", other),
}
}
#[test]
fn test_load_config_yaml_file() {
let yaml = r#"
metadata:
version: "1.0.0"
prompt: "test"
commands: []
global_options: []
"#;
let file = create_temp_file(yaml, ".yaml");
let config = load_config(file.path()).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
}
#[test]
fn test_load_config_yml_extension() {
let yaml = r#"
metadata:
version: "1.0.0"
prompt: "test"
commands: []
global_options: []
"#;
let file = create_temp_file(yaml, ".yml");
let config = load_config(file.path()).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
}
#[test]
fn test_load_config_json_file() {
let json = r#"
{
"metadata": {
"version": "1.0.0",
"prompt": "test"
},
"commands": [],
"global_options": []
}
"#;
let file = create_temp_file(json, ".json");
let config = load_config(file.path()).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
}
#[test]
fn test_load_config_file_not_found() {
let result = load_config("nonexistent_file.yaml");
assert!(result.is_err());
match result.unwrap_err() {
DynamicCliError::Config(ConfigError::FileNotFound { path, .. }) => {
assert!(path.to_str().unwrap().contains("nonexistent_file.yaml"));
}
other => panic!("Expected FileNotFound error, got {:?}", other),
}
}
#[test]
fn test_load_config_unsupported_extension() {
let content = "some content";
let file = create_temp_file(content, ".txt");
let result = load_config(file.path());
assert!(result.is_err());
match result.unwrap_err() {
DynamicCliError::Config(ConfigError::UnsupportedFormat { extension, .. }) => {
assert_eq!(extension, "txt");
}
other => panic!("Expected UnsupportedFormat error, got {:?}", other),
}
}
#[test]
fn test_load_config_no_extension() {
let content = "some content";
let mut file = tempfile::Builder::new()
.suffix("") .tempfile()
.unwrap();
file.write_all(content.as_bytes()).unwrap();
file.flush().unwrap();
let path_without_ext = file.path().with_file_name("configfile");
std::fs::copy(file.path(), &path_without_ext).unwrap();
let result = load_config(&path_without_ext);
let _ = std::fs::remove_file(&path_without_ext);
assert!(result.is_err());
match result.unwrap_err() {
DynamicCliError::Config(ConfigError::UnsupportedFormat { .. }) => {}
other => panic!("Expected UnsupportedFormat error, got {:?}", other),
}
}
#[test]
fn test_load_yaml_with_complex_structure() {
let yaml = r#"
metadata:
version: "2.0.0"
prompt: "myapp"
prompt_suffix: " $ "
commands:
- name: process
aliases: [proc, p]
description: "Process data"
required: true
arguments:
- name: input
arg_type: path
required: true
description: "Input file"
validation:
- must_exist: true
- extensions: [csv, tsv]
options:
- name: output
short: o
long: output
option_type: path
required: false
default: "output.txt"
description: "Output file"
choices: []
implementation: "process_handler"
global_options:
- name: verbose
short: v
long: verbose
option_type: bool
required: false
description: "Verbose output"
choices: []
"#;
let config = load_yaml(yaml).unwrap();
assert_eq!(config.metadata.version, "2.0.0");
assert_eq!(config.commands.len(), 1);
assert_eq!(config.commands[0].arguments.len(), 1);
assert_eq!(config.commands[0].options.len(), 1);
assert_eq!(config.global_options.len(), 1);
}
#[test]
fn test_load_json_with_complex_structure() {
let json = r#"
{
"metadata": {
"version": "2.0.0",
"prompt": "myapp"
},
"commands": [
{
"name": "process",
"aliases": ["proc"],
"description": "Process data",
"required": true,
"arguments": [
{
"name": "input",
"arg_type": "path",
"required": true,
"description": "Input file",
"validation": [
{"must_exist": true},
{"extensions": ["csv"]}
]
}
],
"options": [],
"implementation": "process_handler"
}
],
"global_options": []
}
"#;
let config = load_json(json).unwrap();
assert_eq!(config.metadata.version, "2.0.0");
assert_eq!(config.commands[0].arguments.len(), 1);
}
#[test]
fn test_error_contains_position_yaml() {
let yaml_syntax_error = "{{{";
let result = load_yaml(yaml_syntax_error);
assert!(result.is_err());
match result.unwrap_err() {
DynamicCliError::Config(ConfigError::YamlParse { .. }) => {
}
other => panic!("Expected YamlParse error, got {:?}", other),
}
}
#[test]
fn test_case_insensitive_extension() {
let yaml = r#"
metadata:
version: "1.0.0"
prompt: "test"
commands: []
global_options: []
"#;
let file = create_temp_file(yaml, ".YAML");
let config = load_config(file.path()).unwrap();
assert_eq!(config.metadata.version, "1.0.0");
}
}