Warn on unknown config fields at any nesting depth
Uses serde_ignored to detect and log (tracing::warn!) unknown fields
during config.fnl deserialization without rejecting the config.
https://claude.ai/code/session_015tdp3ahAmmdE69yCsV7J8b
diff --git a/Cargo.lock b/Cargo.lock
index b5e10ca..3871690 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2586,6 +2586,7 @@ dependencies = [
"sentry-opentelemetry",
"sentry-tracing",
"serde",
+ "serde_ignored",
"serde_json",
"tempfile",
"thiserror",
@@ -3168,6 +3169,16 @@ dependencies = [
"syn",
]
+[[package]]
+name = "serde_ignored"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "serde_json"
version = "1.0.149"
diff --git a/Cargo.toml b/Cargo.toml
index 65d34ce..815c387 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ opentelemetry_sdk = "*"
sentry-opentelemetry = "*"
tracing-opentelemetry = "*"
serde = { version = "*", features = ["derive"] }
+serde_ignored = "*"
serde_json = "*"
tempfile = "*"
thiserror = "*"
diff --git a/quire-core/Cargo.toml b/quire-core/Cargo.toml
index c15a087..f7fd9d9 100644
--- a/quire-core/Cargo.toml
+++ b/quire-core/Cargo.toml
@@ -18,6 +18,7 @@ sentry-opentelemetry = { workspace = true }
sentry-tracing = { workspace = true }
tracing-opentelemetry = { workspace = true }
serde = { workspace = true }
+serde_ignored = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
diff --git a/quire-core/src/fennel.rs b/quire-core/src/fennel.rs
index 564ff99..ccce305 100644
--- a/quire-core/src/fennel.rs
+++ b/quire-core/src/fennel.rs
@@ -154,18 +154,31 @@ impl Fennel {
///
/// `name` is used in error messages — typically a filename or a synthetic
/// label like `HEAD:.quire/config.fnl`.
+ ///
+ /// Unknown fields at any nesting depth are logged as warnings via
+ /// `tracing::warn!` but do not cause a failure.
pub fn load_string<T>(&self, source: &str, name: &str) -> Result<T, FennelError>
where
T: serde::de::DeserializeOwned,
{
let result = self.eval_raw(source, name, |_| Ok(()))?;
- self.lua.from_value(result).map_err(|e| {
+ // Convert to a generic JSON value so serde_ignored can walk the tree
+ // and detect fields that no known struct key consumes.
+ let json_val: serde_json::Value = self.lua.from_value(result).map_err(|e| {
let message = format!("{name}: {e}");
FennelError::TypeMismatch {
message,
source: Box::new(e),
}
+ })?;
+
+ serde_ignored::deserialize(&json_val, |path| {
+ tracing::warn!(config = %name, field = %path, "unknown config field ignored");
+ })
+ .map_err(|e| FennelError::TypeMismatch {
+ message: format!("{name}: {e}"),
+ source: Box::new(mlua::Error::external(e)),
})
}
@@ -413,6 +426,28 @@ mod tests {
);
}
+ #[test]
+ fn load_string_warns_on_unknown_top_level_field() {
+ // tracing::warn! fires but we verify it doesn't cause an error.
+ let f = fennel();
+ let result: Result<MirrorConfig, _> = f.load_string(
+ r#"{:mirror {:url "https://github.com/owner/repo.git"} :oops 1}"#,
+ "warn.fnl",
+ );
+ // Unknown field must not be an error.
+ assert!(result.is_ok(), "unexpected error: {:?}", result.err());
+ }
+
+ #[test]
+ fn load_string_warns_on_unknown_nested_field() {
+ let f = fennel();
+ let result: Result<MirrorConfig, _> = f.load_string(
+ r#"{:mirror {:url "https://github.com/owner/repo.git" :extra "hi"}}"#,
+ "warn-nested.fnl",
+ );
+ assert!(result.is_ok(), "unexpected error: {:?}", result.err());
+ }
+
#[test]
fn load_file_reads_from_disk() {
let dir = tempfile::tempdir().expect("tempdir");