Skip to content

Commit

Permalink
Merge pull request #627 from sunshowers/empty-default
Browse files Browse the repository at this point in the history
treat nil parents as empty tables if required
  • Loading branch information
epage authored Jan 14, 2025
2 parents 9a0c1a1 + 6203c80 commit c2450cc
Show file tree
Hide file tree
Showing 2 changed files with 295 additions and 1 deletion.
7 changes: 6 additions & 1 deletion src/path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,17 @@ impl Expression {
let parent = self.get_mut_forcibly(root);
match value.kind {
ValueKind::Table(ref incoming_map) => {
// If the parent is not a table, overwrite it, treating it as a
// table
if !matches!(parent.kind, ValueKind::Table(_)) {
*parent = Map::<String, Value>::new().into();
}

// Continue the deep merge
for (key, val) in incoming_map {
Self::root(key.clone()).set(parent, val.clone());
}
}

_ => {
*parent = value;
}
Expand Down
289 changes: 289 additions & 0 deletions tests/testsuite/merge.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use snapbox::{assert_data_eq, prelude::*, str};

use config::{Config, File, FileFormat, Map};

#[test]
Expand Down Expand Up @@ -80,3 +82,290 @@ fn test_merge_whole_config() {
assert_eq!(config3.get("x").ok(), Some(10));
assert_eq!(config3.get("y").ok(), Some(25));
}

#[test]
#[cfg(feature = "json")]
/// Test a few scenarios with empty maps:
fn test_merge_empty_maps() {
use std::collections::BTreeMap;

#[derive(Debug, Deserialize)]
#[allow(dead_code)] // temporary while this test is broken
struct Settings {
profile: BTreeMap<String, Profile>,
}

#[derive(Debug, Default, Deserialize)]
#[allow(dead_code)] // temporary while this test is broken
struct Profile {
name: Option<String>,
}

// * missing_to_empty: no key -> empty map
let cfg = Config::builder()
.add_source(File::from_str(r#"{ "profile": {} }"#, FileFormat::Json))
.add_source(File::from_str(
r#"{ "profile": { "missing_to_empty": {} } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"missing_to_empty": Profile {
name: None,
},
},
}
"#]]
);

// * missing_to_non_empty: no key -> map with k/v
let cfg = Config::builder()
.add_source(File::from_str(r#"{ "profile": {} }"#, FileFormat::Json))
.add_source(File::from_str(
r#"{ "profile": { "missing_to_non_empty": { "name": "bar" } } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"missing_to_non_empty": Profile {
name: Some(
"bar",
),
},
},
}
"#]]
);

// * empty_to_empty: empty map -> empty map
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "empty_to_empty": {} } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "empty_to_empty": {} } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"empty_to_empty": Profile {
name: None,
},
},
}
"#]]
);

// * empty_to_non_empty: empty map -> map with k/v
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "empty_to_non_empty": {} } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "empty_to_non_empty": { "name": "bar" } } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"empty_to_non_empty": Profile {
name: Some(
"bar",
),
},
},
}
"#]]
);

// * non_empty_to_empty: map with k/v -> empty map
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "non_empty_to_empty": { "name": "foo" } } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "non_empty_to_empty": {} } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"non_empty_to_empty": Profile {
name: Some(
"foo",
),
},
},
}
"#]]
);

// * non_empty_to_non_empty: map with k/v -> map with k/v (override)
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "non_empty_to_non_empty": { "name": "foo" } } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "non_empty_to_non_empty": { "name": "bar" } } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"non_empty_to_non_empty": Profile {
name: Some(
"bar",
),
},
},
}
"#]]
);

// * null_to_empty: null -> empty map
// * null_to_non_empty: null -> map with k/v
// * int_to_empty: int -> empty map
// * int_to_non_empty: int -> map with k/v
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "null_to_empty": null } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "null_to_empty": {} } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"null_to_empty": Profile {
name: None,
},
},
}
"#]]
);

// * null_to_non_empty: null -> map with k/v
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "null_to_non_empty": null } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "null_to_non_empty": { "name": "bar" } } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"null_to_non_empty": Profile {
name: Some(
"bar",
),
},
},
}
"#]]
);

// * int_to_empty: int -> empty map
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "int_to_empty": 42 } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "profile": { "int_to_empty": {} } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap().to_debug(),
str![[r#"
Settings {
profile: {
"int_to_empty": Profile {
name: None,
},
},
}
"#]]
);

// * int_to_non_empty: int -> map with k/v
let cfg = Config::builder()
.add_source(File::from_str(
r#"{ "profile": { "int_to_non_empty": 42 } }"#,
FileFormat::Json,
))
.add_source(File::from_str(
r#"{ "int_to_non_empty": { "name": "bar" } }"#,
FileFormat::Json,
))
.build()
.unwrap();
let res = cfg.try_deserialize::<Settings>();
assert_data_eq!(
res.unwrap_err().to_string(),
str!["invalid type: integer `42`, expected struct Profile for key `profile.int_to_non_empty`"]
);
}

0 comments on commit c2450cc

Please sign in to comment.