From 39f86b47bef40bd763d01552b3dd752955594911 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 27 Dec 2024 00:39:02 +0000 Subject: [PATCH] fix: Treat nil parents as empty tables Encountered this regression via nextest's test suite (https://github.com/nextest-rs/nextest/pull/2001). If a table is empty, it would be treated as a unit value. If attempted to be deserialized via a `Default` impl, it would lead to deserialization failing with an error like ```console profile.default-miri: invalid type: unit value, expected struct CustomProfileImpl ``` A bisect appears to indicate that ec36bff45eb339077e23d47d2edabc0bcd03cf6c is responsible. For empty tables where the Default impl is supposed to work, it no longer would. I've attempted to restore the old behavior of putting in an empty table, specifically this section: https://github.com/rust-cli/config-rs/commit/ec36bff45eb339077e23d47d2edabc0bcd03cf6c#diff-c5423e2d2d6c87501239c0304c0f496742e00440defdd20368cf548ba42ab184L175-L178 I'm happy to make changes if there's a better approach. I've also added some tests for a few situations around empty tables. In this case only the last one (`profile.baz`) regressed, but I've also added tests for a couple other cases. --- src/path/mod.rs | 6 +++- tests/testsuite/file_toml.rs | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/path/mod.rs b/src/path/mod.rs index 333a4d57..2fa5af2b 100644 --- a/src/path/mod.rs +++ b/src/path/mod.rs @@ -150,12 +150,16 @@ impl Expression { let parent = self.get_mut_forcibly(root); match value.kind { ValueKind::Table(ref incoming_map) => { + // If the parent is nil, treat it as an empty table + if matches!(parent.kind, ValueKind::Nil) { + *parent = Map::::new().into(); + } + // Continue the deep merge for (key, val) in incoming_map { Self::root(key.clone()).set(parent, val.clone()); } } - _ => { *parent = value; } diff --git a/tests/testsuite/file_toml.rs b/tests/testsuite/file_toml.rs index 6c3980c5..9e89fb0c 100644 --- a/tests/testsuite/file_toml.rs +++ b/tests/testsuite/file_toml.rs @@ -1,5 +1,7 @@ #![cfg(feature = "toml")] +use std::collections::BTreeMap; + use chrono::{DateTime, TimeZone, Utc}; use float_cmp::ApproxEqUlps; use serde_derive::Deserialize; @@ -422,6 +424,60 @@ bar = "bar is a lowercase param" ); } +#[test] +fn test_override_empty_tables() { + #[derive(Debug, Deserialize)] + struct Settings { + profile: BTreeMap, + } + + #[derive(Debug, Default, Deserialize)] + struct Profile { + name: Option, + } + + // Test a few scenarios with empty tables: + // * foo: empty table -> table with k/v + // * bar: table with k/v -> empty table + // * baz: empty table relying on Default impl + let cfg = Config::builder() + .add_source(File::from_str( + r#" + [profile.foo] +"#, + FileFormat::Toml, + )) + .add_source(File::from_str( + r#" + [profile.foo] + name = "foo" +"#, + FileFormat::Toml, + )) + .add_source(File::from_str( + r#" + [profile.bar] + name = "bar" +"#, + FileFormat::Toml, + )) + .add_source(File::from_str( + r#" + [profile.bar] + [profile.baz] +"#, + FileFormat::Toml, + )) + .build() + .unwrap(); + + let settings: Settings = cfg.try_deserialize().unwrap(); + assert_eq!(settings.profile.len(), 3); + assert_eq!(settings.profile["foo"].name.as_deref(), Some("foo")); + assert_eq!(settings.profile["bar"].name.as_deref(), Some("bar")); + assert_eq!(settings.profile["baz"].name.as_deref(), None); +} + #[test] fn toml() { let s = Config::builder()