use anyhow::Context;
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct DroastConfig {
pub skip: Option<Vec<String>>,
pub min_severity: Option<String>,
pub no_roast: Option<bool>,
pub no_fail: Option<bool>,
pub format: Option<String>,
}
impl DroastConfig {
pub fn load() -> Self {
if let Some(path) = Self::find() {
Self::load_from(&path).unwrap_or_default()
} else {
Self::default()
}
}
fn find() -> Option<std::path::PathBuf> {
let cwd = std::env::current_dir().ok()?;
let mut dir: &Path = &cwd;
loop {
let candidate = dir.join("droast.toml");
if candidate.is_file() {
return Some(candidate);
}
if dir.join(".git").exists() {
break;
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
None
}
pub fn load_from(path: &Path) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file '{}'", path.display()))?;
let cfg: DroastConfig = toml::from_str(&content)
.map_err(|e| anyhow::anyhow!("Invalid config file '{}': {e}", path.display()))?;
Ok(cfg)
}
}
#[cfg(test)]
mod tests {
use super::DroastConfig;
#[test]
fn loads_config_from_explicit_path() {
let path = std::env::temp_dir().join(format!(
"droast-explicit-config-{}.toml",
std::process::id()
));
std::fs::write(&path, "skip = [\"DF001\"]\nno-roast = true\n").unwrap();
let config = DroastConfig::load_from(&path).unwrap();
assert_eq!(config.skip.unwrap(), ["DF001"]);
assert_eq!(config.no_roast, Some(true));
std::fs::remove_file(path).unwrap();
}
#[test]
fn explicit_config_reports_invalid_toml() {
let path = std::env::temp_dir().join(format!(
"droast-invalid-config-{}.toml",
std::process::id()
));
std::fs::write(&path, "skip = [\n").unwrap();
let error = DroastConfig::load_from(&path).unwrap_err().to_string();
assert!(error.contains("Invalid config file"));
std::fs::remove_file(path).unwrap();
}
}