use anyhow::{Context, Result};
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize, Default, Clone)]
#[serde(default)]
pub struct RepoConfig {
pub fail_on: Option<String>,
pub timeout_ms: Option<u64>,
pub fail_open: Option<bool>,
pub allowlist: Vec<String>,
pub watchlist: Vec<String>,
}
pub fn resolve_path(explicit: Option<&str>) -> Option<PathBuf> {
if let Some(p) = explicit {
let path = PathBuf::from(p);
return path.exists().then_some(path);
}
for candidate in [".pkgradar.yml", ".pkgradar.yaml"] {
let path = Path::new(candidate);
if path.exists() {
return Some(path.to_path_buf());
}
}
None
}
pub fn load(path: Option<&Path>) -> Result<RepoConfig> {
let Some(path) = path else {
return Ok(RepoConfig::default());
};
let content =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let cfg: RepoConfig =
serde_yaml::from_str(&content).with_context(|| format!("parsing {}", path.display()))?;
Ok(cfg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_config_is_default() {
let cfg: RepoConfig = serde_yaml::from_str("").unwrap_or_default();
assert!(cfg.fail_on.is_none());
assert!(cfg.allowlist.is_empty());
}
#[test]
fn full_config_round_trip() {
let src = r#"
fail_on: review
timeout_ms: 30000
fail_open: false
allowlist:
- "@types/node@22.5.4"
- "lodash@4.17.21"
watchlist:
- "react@18.3.1"
"#;
let cfg: RepoConfig = serde_yaml::from_str(src).unwrap();
assert_eq!(cfg.fail_on.as_deref(), Some("review"));
assert_eq!(cfg.timeout_ms, Some(30_000));
assert_eq!(cfg.fail_open, Some(false));
assert_eq!(cfg.allowlist.len(), 2);
assert_eq!(cfg.watchlist, vec!["react@18.3.1"]);
}
}