use std::collections::HashMap;
use std::path::Path;
use serde::Serialize;
use void_core::config;
use crate::context::find_void_dir;
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug, Serialize)]
pub struct ConfigListOutput {
pub values: HashMap<String, String>,
}
#[derive(Debug, Serialize)]
pub struct ConfigGetOutput {
pub key: String,
pub value: String,
}
#[derive(Debug, Serialize)]
pub struct ConfigSetOutput {
pub key: String,
pub value: String,
#[serde(rename = "previousValue")]
pub previous_value: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ConfigUnsetOutput {
pub key: String,
pub removed: bool,
}
#[derive(Debug, Clone)]
pub struct ConfigArgs {
pub list: bool,
pub unset: Option<String>,
pub key: Option<String>,
pub value: Option<String>,
}
pub fn run(cwd: &Path, args: ConfigArgs, opts: &CliOptions) -> Result<(), CliError> {
if args.list {
return run_list(cwd, opts);
}
if let Some(key) = &args.unset {
return run_unset(cwd, key, opts);
}
match (&args.key, &args.value) {
(Some(key), Some(value)) => run_set(cwd, key, value, opts),
(Some(key), None) => run_get(cwd, key, opts),
(None, _) => {
Err(CliError::invalid_args(
"usage: void config --list | void config <key> | void config <key> <value> | void config --unset <key>",
))
}
}
}
fn run_list(cwd: &Path, opts: &CliOptions) -> Result<(), CliError> {
run_command("config", opts, |ctx| {
let void_dir = find_void_dir(cwd)?;
ctx.progress("Loading configuration...");
let values = config::list(&void_dir).map_err(|e| CliError::internal(e.to_string()))?;
if !ctx.use_json() {
if values.is_empty() {
ctx.info("No configuration values set.");
} else {
let mut keys: Vec<_> = values.keys().collect();
keys.sort();
for key in keys {
if let Some(value) = values.get(key) {
ctx.info(format!("{}={}", key, value));
}
}
}
}
Ok(ConfigListOutput { values })
})
}
fn run_get(cwd: &Path, key: &str, opts: &CliOptions) -> Result<(), CliError> {
run_command("config", opts, |ctx| {
let void_dir = find_void_dir(cwd)?;
let value = config::get(&void_dir, key).map_err(|e| CliError::internal(e.to_string()))?;
match value {
Some(v) => {
if !ctx.use_json() {
ctx.info(&v);
}
Ok(ConfigGetOutput {
key: key.to_string(),
value: v,
})
}
None => Err(CliError::not_found(format!("key not found: {}", key))),
}
})
}
fn run_set(cwd: &Path, key: &str, value: &str, opts: &CliOptions) -> Result<(), CliError> {
run_command("config", opts, |ctx| {
let void_dir = find_void_dir(cwd)?;
let previous_value =
config::get(&void_dir, key).map_err(|e| CliError::internal(e.to_string()))?;
config::set(&void_dir, key, value).map_err(|e| {
let msg = e.to_string();
if msg.contains("read-only") {
CliError::invalid_args(msg)
} else if msg.contains("unknown config key") {
CliError::invalid_args(msg)
} else if msg.contains("invalid") {
CliError::invalid_args(msg)
} else {
CliError::internal(msg)
}
})?;
if !ctx.use_json() {
match &previous_value {
Some(prev) => {
ctx.info(format!("{}={} (was: {})", key, value, prev));
}
None => {
ctx.info(format!("{}={}", key, value));
}
}
}
Ok(ConfigSetOutput {
key: key.to_string(),
value: value.to_string(),
previous_value,
})
})
}
fn run_unset(cwd: &Path, key: &str, opts: &CliOptions) -> Result<(), CliError> {
run_command("config", opts, |ctx| {
let void_dir = find_void_dir(cwd)?;
config::unset(&void_dir, key).map_err(|e| {
let msg = e.to_string();
if msg.contains("read-only") {
CliError::invalid_args(msg)
} else if msg.contains("unknown config key") {
CliError::invalid_args(msg)
} else {
CliError::internal(msg)
}
})?;
if !ctx.use_json() {
ctx.info(format!("Removed: {}", key));
}
Ok(ConfigUnsetOutput {
key: key.to_string(),
removed: true,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::CliOptions;
use std::fs;
use tempfile::tempdir;
fn default_opts() -> CliOptions {
CliOptions {
human: true,
..Default::default()
}
}
fn setup_void_repo() -> tempfile::TempDir {
let dir = tempdir().unwrap();
let void_dir = dir.path().join(".void");
fs::create_dir_all(&void_dir).unwrap();
fs::write(void_dir.join("config.json"), "{}").unwrap();
dir
}
#[test]
fn test_config_list_empty() {
let dir = setup_void_repo();
let args = ConfigArgs {
list: true,
unset: None,
key: None,
value: None,
};
let result = run(dir.path(), args, &default_opts());
assert!(result.is_ok());
}
#[test]
fn test_config_set_and_get() {
let dir = setup_void_repo();
let set_args = ConfigArgs {
list: false,
unset: None,
key: Some("user.name".to_string()),
value: Some("Test User".to_string()),
};
let result = run(dir.path(), set_args, &default_opts());
assert!(result.is_ok());
let get_args = ConfigArgs {
list: false,
unset: None,
key: Some("user.name".to_string()),
value: None,
};
let result = run(dir.path(), get_args, &default_opts());
assert!(result.is_ok());
}
#[test]
fn test_config_unset() {
let dir = setup_void_repo();
let set_args = ConfigArgs {
list: false,
unset: None,
key: Some("user.email".to_string()),
value: Some("test@example.com".to_string()),
};
run(dir.path(), set_args, &default_opts()).unwrap();
let unset_args = ConfigArgs {
list: false,
unset: Some("user.email".to_string()),
key: None,
value: None,
};
let result = run(dir.path(), unset_args, &default_opts());
assert!(result.is_ok());
}
#[test]
fn test_config_get_not_found() {
let dir = setup_void_repo();
let args = ConfigArgs {
list: false,
unset: None,
key: Some("nonexistent.key".to_string()),
value: None,
};
let result = run(dir.path(), args, &default_opts());
assert!(result.is_err());
}
#[test]
fn test_config_set_invalid_key() {
let dir = setup_void_repo();
let args = ConfigArgs {
list: false,
unset: None,
key: Some("invalid.nonexistent.key".to_string()),
value: Some("value".to_string()),
};
let result = run(dir.path(), args, &default_opts());
assert!(result.is_err());
}
#[test]
fn test_config_set_readonly_key() {
let dir = setup_void_repo();
let args = ConfigArgs {
list: false,
unset: None,
key: Some("version".to_string()),
value: Some("2".to_string()),
};
let result = run(dir.path(), args, &default_opts());
assert!(result.is_err());
}
#[test]
fn test_config_no_args() {
let dir = setup_void_repo();
let args = ConfigArgs {
list: false,
unset: None,
key: None,
value: None,
};
let result = run(dir.path(), args, &default_opts());
assert!(result.is_err());
}
#[test]
fn test_config_not_initialized() {
let dir = tempdir().unwrap();
let args = ConfigArgs {
list: true,
unset: None,
key: None,
value: None,
};
let result = run(dir.path(), args, &default_opts());
assert!(result.is_err());
}
}