use clap::ValueEnum;
use serde::Deserialize;
use std::path::PathBuf;
use crate::output::OutputFormat;
pub const CONFIG_ENV: &str = "ASPHYXIA_CONFIG";
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub timeout: Option<u64>,
pub concurrency: Option<usize>,
pub retries: Option<u32>,
pub rate: Option<u32>,
pub output: Option<String>,
pub interface: Option<String>,
}
impl Config {
pub fn from_toml_str(s: &str) -> Result<Config, String> {
toml::from_str(s).map_err(|e| e.to_string())
}
pub fn load() -> Config {
let Some(path) = config_path() else {
return Config::default();
};
let Ok(text) = std::fs::read_to_string(&path) else {
return Config::default();
};
Self::from_toml_str(&text).unwrap_or_else(|e| {
eprintln!("warning: ignoring invalid config {}: {}", path.display(), e);
Config::default()
})
}
pub fn output_format(&self) -> Option<OutputFormat> {
self.output
.as_deref()
.and_then(|s| OutputFormat::from_str(s, true).ok())
}
}
pub fn config_path() -> Option<PathBuf> {
if let Some(path) = std::env::var_os(CONFIG_ENV) {
return Some(PathBuf::from(path));
}
std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".asphyxia.toml"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_full_config() {
let cfg = Config::from_toml_str(
"timeout = 500\nconcurrency = 128\nretries = 2\noutput = \"json\"\n",
)
.unwrap();
assert_eq!(cfg.timeout, Some(500));
assert_eq!(cfg.concurrency, Some(128));
assert_eq!(cfg.retries, Some(2));
assert_eq!(cfg.output_format(), Some(OutputFormat::Json));
}
#[test]
fn parses_the_bind_interface() {
let cfg = Config::from_toml_str("interface = \"en0\"\n").unwrap();
assert_eq!(cfg.interface.as_deref(), Some("en0"));
}
#[test]
fn empty_config_is_all_none() {
let cfg = Config::from_toml_str("").unwrap();
assert_eq!(cfg, Config::default());
assert_eq!(cfg.output_format(), None);
}
#[test]
fn partial_config_leaves_the_rest_unset() {
let cfg = Config::from_toml_str("timeout = 1000\n").unwrap();
assert_eq!(cfg.timeout, Some(1000));
assert_eq!(cfg.concurrency, None);
}
#[test]
fn unknown_keys_are_rejected() {
assert!(Config::from_toml_str("bogus = 1\n").is_err());
}
#[test]
fn invalid_output_name_yields_none() {
let cfg = Config::from_toml_str("output = \"nonsense\"\n").unwrap();
assert_eq!(cfg.output_format(), None);
}
}