summaryrefslogtreecommitdiffstats
path: root/src/config/file.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-24 15:38:32 +0200
committertv <tv@krebsco.de>2026-10-24 15:38:32 +0200
commiteb9cc70985a27fe16788a885b854fb514234c43a (patch)
tree6dfc192abea479f4690766c06b1ffa64d7e95f28 /src/config/file.rs
parentfeb349ee09aa2151c421840bec521719c6149dcb (diff)
allow unsetting config options
Diffstat (limited to 'src/config/file.rs')
-rw-r--r--src/config/file.rs73
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");