diff options
| author | tv <tv@krebsco.de> | 2026-10-24 15:38:32 +0200 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-24 15:38:32 +0200 |
| commit | eb9cc70985a27fe16788a885b854fb514234c43a (patch) | |
| tree | 6dfc192abea479f4690766c06b1ffa64d7e95f28 /src/config/file.rs | |
| parent | feb349ee09aa2151c421840bec521719c6149dcb (diff) | |
allow unsetting config options
Diffstat (limited to 'src/config/file.rs')
| -rw-r--r-- | src/config/file.rs | 73 |
1 files changed, 50 insertions, 23 deletions
diff --git a/src/config/file.rs b/src/config/file.rs index fea601b..b6eb94d 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -5,18 +5,22 @@ //! `include = [paths]` and `include-optional = [paths]` load other files first //! (relative to this one); this file's own values win over theirs, and later includes //! over earlier ones. A missing optional include is skipped; a cycle is an error. +//! +//! `unset = [options]` sets options back to their defaults, overriding includes and +//! earlier files; in JSON, `null` does the same for one option. use std::path::{Path, PathBuf}; use anyhow::{Context, Result, anyhow, bail}; use serde_json::{Map as JsonMap, Value as JsonValue}; -use tracing::{debug, warn}; +use tracing::debug; use super::import; -use super::{Entry, Layer, Origin, Provenance, spec}; +use super::{Entry, Layer, Origin, Overlay, Provenance, overlay, spec, unset_entry}; const INCLUDE: &str = "include"; const INCLUDE_OPTIONAL: &str = "include-optional"; +const UNSET: &str = "unset"; /// The settings `path` makes, its includes resolved. pub(super) fn load(path: &Path) -> Result<Layer> { @@ -38,6 +42,7 @@ fn load_inner(path: &Path, loading: &mut Vec<PathBuf>) -> Result<Layer> { let mut root = parse(&canonical)?; let includes = take_paths(&mut root, INCLUDE, &canonical)?; let optional = take_paths(&mut root, INCLUDE_OPTIONAL, &canonical)?; + let unsets = take_paths(&mut root, UNSET, &canonical)?; loading.push(canonical.clone()); let mut layer = Layer::new(); @@ -54,21 +59,22 @@ fn load_inner(path: &Path, loading: &mut Vec<PathBuf>) -> Result<Layer> { let included = load_inner(&include, loading) .with_context(|| format!("included from {}", canonical.display()))?; for (name, entry) in included { - if let Some(previous) = layer.get(&name) { - warn!( - option = %name, - value = %entry.shown(), - from = %entry.origin, - overrides = %previous.shown(), - overridden_from = %previous.origin, - "Two included configuration files set the same option; the later one wins" - ); - } - layer.insert(name, entry); + overlay(&mut layer, name, entry, Overlay::IncludeOverInclude); } } loading.pop(); + for name in unsets { + if spec(&name).is_none() { + bail!( + "unknown option {name:?} in unset, in {}", + canonical.display() + ); + } + let entry = unset_entry(Provenance::File(canonical.clone())); + overlay(&mut layer, name, entry, Overlay::FileOverFile); + } + let mut own = Vec::new(); flatten(&root, "", &dir, &mut own).with_context(|| format!("in {}", canonical.display()))?; for (name, value) in own { @@ -77,21 +83,14 @@ fn load_inner(path: &Path, loading: &mut Vec<PathBuf>) -> Result<Layer> { let (value, imports) = super::resolve_file_value(spec, value, &dir) .with_context(|| format!("option {name} in {}", canonical.display()))?; let entry = Entry { - value, origin: Origin { provenance: Provenance::File(canonical.clone()), + unset: value.is_null(), imports, }, + value, }; - if let Some(previous) = layer.get(&name) { - debug!( - option = %name, - from = %entry.origin, - overrides = %previous.origin, - "Configuration file overrides an included one" - ); - } - layer.insert(name, entry); + overlay(&mut layer, name, entry, Overlay::FileOverFile); } Ok(layer) } @@ -219,6 +218,34 @@ mod tests { } #[test] + fn unset() { + let dir = scratch("unset"); + write( + &dir, + "base.toml", + "alias = \"base\"\nneighbours = 8\nkeepalive-secs = 30\n", + ); + let toml = write( + &dir, + "local.toml", + "include = \"base.toml\"\nunset = [\"alias\"]\n", + ); + let layer = load(&toml).unwrap(); + assert!(layer["alias"].value.is_null() && layer["alias"].origin.unset); + assert_eq!(layer["neighbours"].value, json!(8)); + let json = write( + &dir, + "local.json", + r#"{ "include": "base.toml", "neighbours": null }"#, + ); + let layer = load(&json).unwrap(); + assert!(layer["neighbours"].origin.unset); + assert_eq!(layer["keepalive-secs"].value, json!(30)); + let unknown = write(&dir, "bad.toml", "unset = [\"nope\"]\n"); + assert!(load(&unknown).is_err()); + } + + #[test] fn errors() { let dir = scratch("errors"); let unknown = write(&dir, "unknown.toml", "nieghbours = 3\n"); |
