Skip to content

Commit c2450cc

Browse files
authored
Merge pull request #627 from sunshowers/empty-default
treat nil parents as empty tables if required
2 parents 9a0c1a1 + 6203c80 commit c2450cc

File tree

2 files changed

+295
-1
lines changed

2 files changed

+295
-1
lines changed

src/path/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,17 @@ impl Expression {
150150
let parent = self.get_mut_forcibly(root);
151151
match value.kind {
152152
ValueKind::Table(ref incoming_map) => {
153+
// If the parent is not a table, overwrite it, treating it as a
154+
// table
155+
if !matches!(parent.kind, ValueKind::Table(_)) {
156+
*parent = Map::<String, Value>::new().into();
157+
}
158+
153159
// Continue the deep merge
154160
for (key, val) in incoming_map {
155161
Self::root(key.clone()).set(parent, val.clone());
156162
}
157163
}
158-
159164
_ => {
160165
*parent = value;
161166
}

tests/testsuite/merge.rs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use snapbox::{assert_data_eq, prelude::*, str};
2+
13
use config::{Config, File, FileFormat, Map};
24

35
#[test]
@@ -80,3 +82,290 @@ fn test_merge_whole_config() {
8082
assert_eq!(config3.get("x").ok(), Some(10));
8183
assert_eq!(config3.get("y").ok(), Some(25));
8284
}
85+
86+
#[test]
87+
#[cfg(feature = "json")]
88+
/// Test a few scenarios with empty maps:
89+
fn test_merge_empty_maps() {
90+
use std::collections::BTreeMap;
91+
92+
#[derive(Debug, Deserialize)]
93+
#[allow(dead_code)] // temporary while this test is broken
94+
struct Settings {
95+
profile: BTreeMap<String, Profile>,
96+
}
97+
98+
#[derive(Debug, Default, Deserialize)]
99+
#[allow(dead_code)] // temporary while this test is broken
100+
struct Profile {
101+
name: Option<String>,
102+
}
103+
104+
// * missing_to_empty: no key -> empty map
105+
let cfg = Config::builder()
106+
.add_source(File::from_str(r#"{ "profile": {} }"#, FileFormat::Json))
107+
.add_source(File::from_str(
108+
r#"{ "profile": { "missing_to_empty": {} } }"#,
109+
FileFormat::Json,
110+
))
111+
.build()
112+
.unwrap();
113+
let res = cfg.try_deserialize::<Settings>();
114+
assert_data_eq!(
115+
res.unwrap().to_debug(),
116+
str![[r#"
117+
Settings {
118+
profile: {
119+
"missing_to_empty": Profile {
120+
name: None,
121+
},
122+
},
123+
}
124+
125+
"#]]
126+
);
127+
128+
// * missing_to_non_empty: no key -> map with k/v
129+
let cfg = Config::builder()
130+
.add_source(File::from_str(r#"{ "profile": {} }"#, FileFormat::Json))
131+
.add_source(File::from_str(
132+
r#"{ "profile": { "missing_to_non_empty": { "name": "bar" } } }"#,
133+
FileFormat::Json,
134+
))
135+
.build()
136+
.unwrap();
137+
let res = cfg.try_deserialize::<Settings>();
138+
assert_data_eq!(
139+
res.unwrap().to_debug(),
140+
str![[r#"
141+
Settings {
142+
profile: {
143+
"missing_to_non_empty": Profile {
144+
name: Some(
145+
"bar",
146+
),
147+
},
148+
},
149+
}
150+
151+
"#]]
152+
);
153+
154+
// * empty_to_empty: empty map -> empty map
155+
let cfg = Config::builder()
156+
.add_source(File::from_str(
157+
r#"{ "profile": { "empty_to_empty": {} } }"#,
158+
FileFormat::Json,
159+
))
160+
.add_source(File::from_str(
161+
r#"{ "profile": { "empty_to_empty": {} } }"#,
162+
FileFormat::Json,
163+
))
164+
.build()
165+
.unwrap();
166+
let res = cfg.try_deserialize::<Settings>();
167+
assert_data_eq!(
168+
res.unwrap().to_debug(),
169+
str![[r#"
170+
Settings {
171+
profile: {
172+
"empty_to_empty": Profile {
173+
name: None,
174+
},
175+
},
176+
}
177+
178+
"#]]
179+
);
180+
181+
// * empty_to_non_empty: empty map -> map with k/v
182+
let cfg = Config::builder()
183+
.add_source(File::from_str(
184+
r#"{ "profile": { "empty_to_non_empty": {} } }"#,
185+
FileFormat::Json,
186+
))
187+
.add_source(File::from_str(
188+
r#"{ "profile": { "empty_to_non_empty": { "name": "bar" } } }"#,
189+
FileFormat::Json,
190+
))
191+
.build()
192+
.unwrap();
193+
let res = cfg.try_deserialize::<Settings>();
194+
assert_data_eq!(
195+
res.unwrap().to_debug(),
196+
str![[r#"
197+
Settings {
198+
profile: {
199+
"empty_to_non_empty": Profile {
200+
name: Some(
201+
"bar",
202+
),
203+
},
204+
},
205+
}
206+
207+
"#]]
208+
);
209+
210+
// * non_empty_to_empty: map with k/v -> empty map
211+
let cfg = Config::builder()
212+
.add_source(File::from_str(
213+
r#"{ "profile": { "non_empty_to_empty": { "name": "foo" } } }"#,
214+
FileFormat::Json,
215+
))
216+
.add_source(File::from_str(
217+
r#"{ "profile": { "non_empty_to_empty": {} } }"#,
218+
FileFormat::Json,
219+
))
220+
.build()
221+
.unwrap();
222+
let res = cfg.try_deserialize::<Settings>();
223+
assert_data_eq!(
224+
res.unwrap().to_debug(),
225+
str![[r#"
226+
Settings {
227+
profile: {
228+
"non_empty_to_empty": Profile {
229+
name: Some(
230+
"foo",
231+
),
232+
},
233+
},
234+
}
235+
236+
"#]]
237+
);
238+
239+
// * non_empty_to_non_empty: map with k/v -> map with k/v (override)
240+
let cfg = Config::builder()
241+
.add_source(File::from_str(
242+
r#"{ "profile": { "non_empty_to_non_empty": { "name": "foo" } } }"#,
243+
FileFormat::Json,
244+
))
245+
.add_source(File::from_str(
246+
r#"{ "profile": { "non_empty_to_non_empty": { "name": "bar" } } }"#,
247+
FileFormat::Json,
248+
))
249+
.build()
250+
.unwrap();
251+
let res = cfg.try_deserialize::<Settings>();
252+
assert_data_eq!(
253+
res.unwrap().to_debug(),
254+
str![[r#"
255+
Settings {
256+
profile: {
257+
"non_empty_to_non_empty": Profile {
258+
name: Some(
259+
"bar",
260+
),
261+
},
262+
},
263+
}
264+
265+
"#]]
266+
);
267+
268+
// * null_to_empty: null -> empty map
269+
// * null_to_non_empty: null -> map with k/v
270+
// * int_to_empty: int -> empty map
271+
// * int_to_non_empty: int -> map with k/v
272+
let cfg = Config::builder()
273+
.add_source(File::from_str(
274+
r#"{ "profile": { "null_to_empty": null } }"#,
275+
FileFormat::Json,
276+
))
277+
.add_source(File::from_str(
278+
r#"{ "profile": { "null_to_empty": {} } }"#,
279+
FileFormat::Json,
280+
))
281+
.build()
282+
.unwrap();
283+
let res = cfg.try_deserialize::<Settings>();
284+
assert_data_eq!(
285+
res.unwrap().to_debug(),
286+
str![[r#"
287+
Settings {
288+
profile: {
289+
"null_to_empty": Profile {
290+
name: None,
291+
},
292+
},
293+
}
294+
295+
"#]]
296+
);
297+
298+
// * null_to_non_empty: null -> map with k/v
299+
let cfg = Config::builder()
300+
.add_source(File::from_str(
301+
r#"{ "profile": { "null_to_non_empty": null } }"#,
302+
FileFormat::Json,
303+
))
304+
.add_source(File::from_str(
305+
r#"{ "profile": { "null_to_non_empty": { "name": "bar" } } }"#,
306+
FileFormat::Json,
307+
))
308+
.build()
309+
.unwrap();
310+
let res = cfg.try_deserialize::<Settings>();
311+
assert_data_eq!(
312+
res.unwrap().to_debug(),
313+
str![[r#"
314+
Settings {
315+
profile: {
316+
"null_to_non_empty": Profile {
317+
name: Some(
318+
"bar",
319+
),
320+
},
321+
},
322+
}
323+
324+
"#]]
325+
);
326+
327+
// * int_to_empty: int -> empty map
328+
let cfg = Config::builder()
329+
.add_source(File::from_str(
330+
r#"{ "profile": { "int_to_empty": 42 } }"#,
331+
FileFormat::Json,
332+
))
333+
.add_source(File::from_str(
334+
r#"{ "profile": { "int_to_empty": {} } }"#,
335+
FileFormat::Json,
336+
))
337+
.build()
338+
.unwrap();
339+
let res = cfg.try_deserialize::<Settings>();
340+
assert_data_eq!(
341+
res.unwrap().to_debug(),
342+
str![[r#"
343+
Settings {
344+
profile: {
345+
"int_to_empty": Profile {
346+
name: None,
347+
},
348+
},
349+
}
350+
351+
"#]]
352+
);
353+
354+
// * int_to_non_empty: int -> map with k/v
355+
let cfg = Config::builder()
356+
.add_source(File::from_str(
357+
r#"{ "profile": { "int_to_non_empty": 42 } }"#,
358+
FileFormat::Json,
359+
))
360+
.add_source(File::from_str(
361+
r#"{ "int_to_non_empty": { "name": "bar" } }"#,
362+
FileFormat::Json,
363+
))
364+
.build()
365+
.unwrap();
366+
let res = cfg.try_deserialize::<Settings>();
367+
assert_data_eq!(
368+
res.unwrap_err().to_string(),
369+
str!["invalid type: integer `42`, expected struct Profile for key `profile.int_to_non_empty`"]
370+
);
371+
}

0 commit comments

Comments
 (0)