use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde_json::Value;
pub fn load_config(path: &str) -> Result<BTreeMap<String, Value>, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read config file '{path}': {e}"))?;
let parsed: Value = serde_json::from_str(&content)
.map_err(|e| format!("Invalid JSON config file '{path}': {e}"))?;
match parsed {
Value::Object(map) => {
let btree: BTreeMap<String, Value> = map.into_iter().collect();
Ok(btree)
}
_ => Err(format!("Invalid config file: expected a top-level object in '{path}'").into()),
}
}
pub fn resolve_config_path(flag_value: Option<&str>, files: &[String]) -> Option<String> {
if let Some(explicit) = flag_value {
return Some(resolve_path(explicit));
}
for file in files {
let resolved = resolve_path(file);
if Path::new(&resolved).exists() {
return Some(resolved);
}
}
None
}
pub fn extract_command_section(
config: &BTreeMap<String, Value>,
cli_name: &str,
command_path: &str,
) -> Result<Option<BTreeMap<String, Value>>, Box<dyn std::error::Error>> {
let segments: Vec<&str> = if command_path == cli_name {
Vec::new()
} else {
command_path.split(' ').collect()
};
let mut node: &Value =
&Value::Object(config.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
for seg in &segments {
let obj = node.as_object().ok_or_else(|| {
format!("Invalid config section for '{command_path}': expected an object")
})?;
let commands = match obj.get("commands") {
Some(c) => c,
None => return Ok(None),
};
let commands_obj = commands.as_object().ok_or_else(|| {
format!("Invalid config 'commands' for '{command_path}': expected an object")
})?;
node = match commands_obj.get(*seg) {
Some(n) => n,
None => return Ok(None),
};
}
let obj = node.as_object().ok_or_else(|| {
format!("Invalid config section for '{command_path}': expected an object")
})?;
let options = match obj.get("options") {
Some(o) => o,
None => return Ok(None),
};
let options_obj = options.as_object().ok_or_else(|| {
format!("Invalid config 'options' for '{command_path}': expected an object")
})?;
if options_obj.is_empty() {
return Ok(None);
}
let btree: BTreeMap<String, Value> = options_obj
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Ok(Some(btree))
}
fn resolve_path(file_path: &str) -> String {
if (file_path.starts_with("~/") || file_path == "~")
&& let Some(home) = dirs::home_dir()
{
return home.join(&file_path[1..]).to_string_lossy().into_owned();
}
let path = PathBuf::from(file_path);
if path.is_absolute() {
file_path.to_string()
} else {
match std::env::current_dir() {
Ok(cwd) => cwd.join(file_path).to_string_lossy().into_owned(),
Err(_) => file_path.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_path_absolute() {
assert_eq!(resolve_path("/etc/config.json"), "/etc/config.json");
}
#[test]
fn test_resolve_path_tilde() {
let resolved = resolve_path("~/config.json");
assert!(!resolved.starts_with("~"));
assert!(resolved.ends_with("config.json"));
}
#[test]
fn test_extract_command_section_root() {
let config: BTreeMap<String, Value> =
serde_json::from_str(r#"{ "options": { "verbose": true } }"#).unwrap();
let result = extract_command_section(&config, "my-cli", "my-cli").unwrap();
let opts = result.unwrap();
assert_eq!(opts.get("verbose"), Some(&Value::Bool(true)));
}
#[test]
fn test_extract_command_section_nested() {
let config: BTreeMap<String, Value> = serde_json::from_str(
r#"{
"commands": {
"deploy": {
"options": {
"environment": "staging"
}
}
}
}"#,
)
.unwrap();
let result = extract_command_section(&config, "my-cli", "deploy").unwrap();
let opts = result.unwrap();
assert_eq!(
opts.get("environment"),
Some(&Value::String("staging".to_string()))
);
}
#[test]
fn test_extract_command_section_missing() {
let config: BTreeMap<String, Value> =
serde_json::from_str(r#"{ "commands": {} }"#).unwrap();
let result = extract_command_section(&config, "my-cli", "nonexistent").unwrap();
assert!(result.is_none());
}
#[test]
fn test_extract_command_section_empty_options() {
let config: BTreeMap<String, Value> =
serde_json::from_str(r#"{ "commands": { "test": { "options": {} } } }"#).unwrap();
let result = extract_command_section(&config, "my-cli", "test").unwrap();
assert!(result.is_none());
}
}