use camino::Utf8Path;
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OnMissingId {
#[default]
Warn,
Error,
Skip,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct VerifyConfig {
pub strict: bool,
pub on_missing_id: OnMissingId,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VerifyConfigError {
#[error("reading verify config {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("parsing verify config {path}: {message}")]
Parse {
path: String,
message: String,
},
}
#[derive(Debug, Default, Deserialize)]
struct RawConfig {
#[serde(default)]
verify: Option<RawVerify>,
#[serde(flatten)]
_other: serde::de::IgnoredAny,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawVerify {
#[serde(default)]
strict: bool,
#[serde(default)]
on_missing_id: OnMissingId,
}
pub fn load(config_path: &Utf8Path) -> Result<VerifyConfig, VerifyConfigError> {
let text = match std::fs::read_to_string(config_path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(VerifyConfig::default());
}
Err(e) => {
return Err(VerifyConfigError::Io {
path: config_path.to_string(),
source: e,
});
}
};
let raw: RawConfig = toml::from_str(&text).map_err(|e| VerifyConfigError::Parse {
path: config_path.to_string(),
message: e.to_string(),
})?;
Ok(match raw.verify {
Some(v) => VerifyConfig {
strict: v.strict,
on_missing_id: v.on_missing_id,
},
None => VerifyConfig::default(),
})
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use camino::Utf8PathBuf;
fn write(dir: &tempfile::TempDir, body: &str) -> Utf8PathBuf {
let p = Utf8PathBuf::try_from(dir.path().join("config.toml")).expect("utf-8");
std::fs::write(&p, body).expect("write config");
p
}
#[test]
fn missing_file_is_defaults() {
let cfg = load(Utf8Path::new("/no/such/config.toml")).expect("missing is ok");
assert_eq!(cfg, VerifyConfig::default());
assert_eq!(cfg.on_missing_id, OnMissingId::Warn);
assert!(!cfg.strict);
}
#[test]
fn empty_or_unrelated_sections_are_defaults() {
let dir = tempfile::TempDir::new().unwrap();
let p = write(&dir, "[network]\nadditional_hosts = []\n");
let cfg = load(&p).expect("parses");
assert_eq!(cfg, VerifyConfig::default());
}
#[test]
fn reads_on_missing_id_and_strict() {
let dir = tempfile::TempDir::new().unwrap();
let p = write(&dir, "[verify]\non_missing_id = \"error\"\nstrict = true\n");
let cfg = load(&p).expect("parses");
assert_eq!(cfg.on_missing_id, OnMissingId::Error);
assert!(cfg.strict);
}
#[test]
fn skip_value_parses() {
let dir = tempfile::TempDir::new().unwrap();
let p = write(&dir, "[verify]\non_missing_id = \"skip\"\n");
let cfg = load(&p).expect("parses");
assert_eq!(cfg.on_missing_id, OnMissingId::Skip);
}
#[test]
fn unknown_key_in_verify_is_an_error() {
let dir = tempfile::TempDir::new().unwrap();
let p = write(&dir, "[verify]\non_missing_ids = \"warn\"\n");
assert!(matches!(load(&p), Err(VerifyConfigError::Parse { .. })));
}
#[test]
fn invalid_on_missing_id_value_is_an_error() {
let dir = tempfile::TempDir::new().unwrap();
let p = write(&dir, "[verify]\non_missing_id = \"sometimes\"\n");
assert!(matches!(load(&p), Err(VerifyConfigError::Parse { .. })));
}
}