use crate::config::{self, ConfigFile};
use crate::output;
use anyhow::Result;
use clap::Subcommand;
#[derive(Subcommand)]
pub enum ConfigCommands {
Set {
key: String,
value: String,
},
Path,
}
pub fn run(command: Option<ConfigCommands>, json: bool) -> Result<()> {
match command {
None => show_config(json),
Some(ConfigCommands::Set { key, value }) => set_config(&key, &value, json),
Some(ConfigCommands::Path) => print_path(json),
}
}
fn show_config(json: bool) -> Result<()> {
let config = ConfigFile::load()?;
let context = config::resolve_context(&config);
if json {
let data = serde_json::json!({
"context": {
"org": context.org,
"project": context.project,
"environment": context.environment,
"scope": context.scope,
},
"api": {
"url": config.api.url.as_deref().unwrap_or(config::DEFAULT_API_URL),
},
"authenticated": config.auth_header_value().is_some(),
});
println!("{}", serde_json::to_string_pretty(&data).expect("json serialization failed"));
} else {
let api_url = config
.api
.url
.as_deref()
.unwrap_or(config::DEFAULT_API_URL);
let authenticated = if config.auth_header_value().is_some() {
"yes"
} else {
"no"
};
output::print_kv_or_json(
&[
("org", context.org.as_deref().unwrap_or("(not set)")),
("project", context.project.as_deref().unwrap_or("(not set)")),
(
"environment",
context.environment.as_deref().unwrap_or("(not set)"),
),
("scope", context.scope.as_deref().unwrap_or("(not set)")),
("api.url", api_url),
("authenticated", authenticated),
],
&serde_json::json!(null), false,
);
}
Ok(())
}
fn set_config(key: &str, value: &str, json: bool) -> Result<()> {
config::set_config_value(key, value)?;
if json {
println!(
"{}",
serde_json::json!({ "key": key, "value": value, "status": "set" })
);
} else {
println!("Set {key} = \"{value}\"");
}
Ok(())
}
fn print_path(json: bool) -> Result<()> {
let path = config::config_path()?;
let path_str = path.display().to_string();
if json {
println!("{}", serde_json::json!({ "path": path_str }));
} else {
println!("{path_str}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_settable_keys_are_valid() {
for key in config::SETTABLE_KEYS {
match *key {
"org" | "project" | "environment" | "scope" | "api.url" => {}
other => panic!("unhandled settable key: {other}"),
}
}
}
#[test]
fn test_config_set_invalid_key() {
let mut cfg = ConfigFile::default();
let result = match "invalid_key" {
"org" => { cfg.context.org = Some("v".to_string()); Ok(()) }
"project" => { cfg.context.project = Some("v".to_string()); Ok(()) }
"environment" => { cfg.context.environment = Some("v".to_string()); Ok(()) }
"scope" => { cfg.context.scope = Some("v".to_string()); Ok(()) }
"api.url" => { cfg.api.url = Some("v".to_string()); Ok(()) }
other => Err(format!("unknown config key '{other}'")),
};
assert!(result.is_err());
assert!(result.unwrap_err().contains("unknown config key"));
}
#[test]
fn test_config_set_valid_keys_roundtrip() {
let dir = tempfile::TempDir::new().expect("tempdir");
let path = dir.path().join("config.toml");
let mut config = ConfigFile::default();
config.context.org = Some("acme".to_string());
config.context.project = Some("api".to_string());
config.context.environment = Some("prod".to_string());
config.context.scope = Some("default".to_string());
config.api.url = Some("https://custom.dev".to_string());
let content = toml::to_string_pretty(&config).expect("serialize");
std::fs::write(&path, &content).expect("write");
let loaded: ConfigFile =
toml::from_str(&std::fs::read_to_string(&path).expect("read")).expect("parse");
assert_eq!(loaded.context.org.as_deref(), Some("acme"));
assert_eq!(loaded.context.project.as_deref(), Some("api"));
assert_eq!(loaded.context.environment.as_deref(), Some("prod"));
assert_eq!(loaded.context.scope.as_deref(), Some("default"));
assert_eq!(loaded.api.url.as_deref(), Some("https://custom.dev"));
}
}