use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use super::limits::FileRule;
use crate::structure::{FolderRule, Preset};
const CONFIG_FILE: &str = "sarah.toml";
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub preset: Option<Preset>,
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub files: Vec<FileRule>,
#[serde(default)]
pub folder: Vec<FolderRule>,
}
impl Config {
pub fn load(root: &Path, explicit: Option<&Path>) -> Result<Self> {
let Some(path) = Self::locate(root, explicit) else {
return Ok(Self::default());
};
let text = std::fs::read_to_string(&path)
.with_context(|| format!("cannot read config {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("invalid config {}", path.display()))
}
fn locate(root: &Path, explicit: Option<&Path>) -> Option<PathBuf> {
if explicit.is_some() {
return Some(Self::target(root, explicit));
}
let default = Self::target(root, None);
default.is_file().then_some(default)
}
pub fn target(root: &Path, explicit: Option<&Path>) -> PathBuf {
explicit.map_or_else(|| root.join(CONFIG_FILE), Path::to_path_buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_full_config() {
let config: Config = toml::from_str(
r#"
preset = "rust"
exclude = ["vendor/**"]
[[files]]
path = "tests/**"
max_fn_lines = 200
[[folder]]
path = "src/**"
max_files = 12
naming = "snake_case"
"#,
)
.unwrap();
assert_eq!(config.preset, Some(Preset::Rust));
assert_eq!(config.exclude, vec!["vendor/**"]);
assert_eq!(config.files.len(), 1);
assert_eq!(config.folder.len(), 1);
}
#[test]
fn rejects_unknown_keys() {
assert!(toml::from_str::<Config>("[[folder]]\npath = \"src\"\nmax_flies = 3\n").is_err());
}
}