Skip to content

Commit 0840ccd

Browse files
fix(serde_json): value merging was incorrect when not present in a file
1 parent b780764 commit 0840ccd

4 files changed

Lines changed: 126 additions & 63 deletions

File tree

confik/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Add a new helper `MergingUnsetBuilder` and trait `MergingWithUnset`, for aiding in building of custom `Configuration` implementations. See the docs on `MergingWithUnset` for a worked example.
6+
- Fix `serde_json::Value` merge when the first parsed config doesn't have any value set
7+
58
## 0.15.10
69

710
- Add `#[confik(struct_default)]` for struct fields: when no configuration data is present for that field, its value is taken from the same-named field on `<Self as Default>::default()`. Can be combined with `#[confik(skip)]` (in place of an explicit `#[confik(default)]`). Cannot be combined with `#[confik(default)]` on the same field, and is not supported on enum variant fields (the configuration type must remain a struct).

confik/src/helpers.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,90 @@ where
371371
}
372372
}
373373
}
374+
375+
/// Implementation helper for building a type that supports merging in itself, but needs to represent unset.
376+
///
377+
/// See [`MergingWithUnset`] for more info.
378+
#[derive(Debug, Default, Clone)]
379+
pub enum MergingUnsetBuilder<T> {
380+
#[default]
381+
Unset,
382+
Set(T),
383+
}
384+
385+
impl<'de, T> Deserialize<'de> for MergingUnsetBuilder<T>
386+
where
387+
T: Deserialize<'de>,
388+
{
389+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
390+
where
391+
D: serde::Deserializer<'de>,
392+
{
393+
T::deserialize(deserializer).map(Self::Set)
394+
}
395+
}
396+
397+
/// A helper trait for building [`Configuration`], with automated handling of unset merges and builds.
398+
///
399+
/// This trait duplicated the definitions of [Configuration], any set value(s) delegate to these functions, with unset values being handled by [`MergingUnsetBuilder`] itself.
400+
///
401+
/// For an example `AdditiveMerge` type, you would otherwise need to have a custom builder with `Option<usize>` inside, and then handle the `Option` yourself. This trait allows that to be delegated to [`MergingUnsetBuilder`].
402+
/// ```
403+
/// #[derive(Debug, Deserialize)]
404+
/// struct AdditiveMerge(usize);
405+
///
406+
/// impl Configuration for AdditiveMerge {
407+
/// type Builder = MergingUnsetBuilder<Self>;
408+
/// }
409+
///
410+
/// impl MergingWithUnset for AdditiveMerge {
411+
/// type Target = Self;
412+
///
413+
/// fn merge(self, other: Self) -> Self {
414+
/// self.0 + other.0
415+
/// }
416+
///
417+
/// fn try_build(self) -> Result<Self::Target, Error> {
418+
/// Ok(self)
419+
/// }
420+
///
421+
/// fn contains_non_secret_data(&self) -> Result<bool, UnexpectedSecret> {
422+
/// Ok(true)
423+
/// }
424+
/// }
425+
/// ```
426+
pub trait MergingWithUnset {
427+
type Target;
428+
429+
fn merge(self, other: Self) -> Self;
430+
fn try_build(self) -> Result<Self::Target, Error>;
431+
fn contains_non_secret_data(&self) -> Result<bool, UnexpectedSecret>;
432+
}
433+
434+
impl<T> ConfigurationBuilder for MergingUnsetBuilder<T>
435+
where
436+
T: MergingWithUnset + DeserializeOwned,
437+
{
438+
type Target = T::Target;
439+
440+
fn merge(self, other: Self) -> Self {
441+
match (self, other) {
442+
(Self::Unset, merged) | (merged, Self::Unset) => merged,
443+
(Self::Set(s), Self::Set(o)) => Self::Set(s.merge(o)),
444+
}
445+
}
446+
447+
fn try_build(self) -> Result<Self::Target, Error> {
448+
match self {
449+
Self::Unset => Err(Error::MissingValue(MissingValue::default())),
450+
Self::Set(s) => s.try_build(),
451+
}
452+
}
453+
454+
fn contains_non_secret_data(&self) -> Result<bool, UnexpectedSecret> {
455+
match self {
456+
Self::Unset => Ok(false),
457+
Self::Set(s) => s.contains_non_secret_data(),
458+
}
459+
}
460+
}

confik/src/third_party.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,16 @@ mod ahash {
476476
mod serde_json {
477477
use serde_json::Value;
478478

479-
use crate::{Configuration, ConfigurationBuilder};
479+
use crate::{
480+
helpers::{MergingUnsetBuilder, MergingWithUnset},
481+
Configuration,
482+
};
480483

481484
impl Configuration for Value {
482-
type Builder = Self;
485+
type Builder = MergingUnsetBuilder<Self>;
483486
}
484487

485-
impl ConfigurationBuilder for Value {
488+
impl MergingWithUnset for Value {
486489
type Target = Self;
487490

488491
fn merge(self, other: Self) -> Self {

confik/tests/third_party.rs

Lines changed: 30 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -157,102 +157,72 @@ mod bigdecimal {
157157

158158
#[cfg(feature = "serde_json")]
159159
mod serde_json {
160-
use confik::ConfigurationBuilder as _;
161-
use serde_json::{json, Value};
162-
163-
#[test]
164-
fn merge_left_type_wins_over_right_type() {
165-
// When the two sides have different types, the left always wins
166-
assert_eq!(Value::Null.merge(json!({"key": "value"})), Value::Null);
167-
assert_eq!(json!(true).merge(json!([1, 2, 3])), json!(true));
168-
assert_eq!(json!(42).merge(json!({"key": "value"})), json!(42));
169-
assert_eq!(json!("hello").merge(json!(99)), json!("hello"));
170-
assert_eq!(json!([1, 2]).merge(json!({"key": "value"})), json!([1, 2]));
171-
assert_eq!(
172-
json!({"key": "value"}).merge(json!([1, 2])),
173-
json!({"key": "value"})
174-
);
175-
}
176-
177-
#[test]
178-
fn merge_arrays_are_concatenated() {
179-
assert_eq!(json!([1, 2]).merge(json!([3, 4])), json!([1, 2, 3, 4]));
180-
}
181-
182-
#[test]
183-
fn merge_objects_combine_disjoint_keys() {
184-
assert_eq!(
185-
json!({"a": 1}).merge(json!({"b": 2})),
186-
json!({"a": 1, "b": 2})
187-
);
188-
}
189-
190-
#[test]
191-
fn try_build_returns_value_unchanged() {
192-
let value = json!({"key": "value", "num": 42});
193-
assert_eq!(value.clone().try_build().unwrap(), value);
194-
}
195-
196-
#[test]
197-
fn contains_non_secret_data() {
198-
for value in [Value::Null, json!(""), json!([]), json!({})] {
199-
assert!(!value.contains_non_secret_data().unwrap());
200-
}
201-
for value in [
202-
json!(true),
203-
json!(42),
204-
json!("hello"),
205-
json!([1, 2, 3]),
206-
json!({"key": "value"}),
207-
] {
208-
assert!(value.contains_non_secret_data().unwrap());
209-
}
210-
}
211-
212160
#[cfg(feature = "toml")]
213161
mod toml {
214162
use confik::{Configuration, TomlSource};
215163
use serde_json::{json, Value};
216164

217165
#[derive(Configuration)]
218-
struct Config {
166+
struct Root {
167+
leaf: Leaf,
168+
}
169+
170+
#[derive(Configuration)]
171+
struct Leaf {
219172
data: Value,
220173
}
221174

222175
#[test]
223176
fn value_loads_from_toml() {
224177
let toml = r#"
225-
[data]
178+
[leaf.data]
226179
key = "hello"
227180
num = 42
228181
"#;
229182

230-
let config = Config::builder()
183+
let config = Root::builder()
231184
.override_with(TomlSource::new(toml))
232185
.try_build()
233186
.unwrap();
234187

235-
assert_eq!(config.data, json!({"key": "hello", "num": 42}));
188+
assert_eq!(config.leaf.data, json!({"key": "hello", "num": 42}));
236189
}
237190

238191
#[test]
239192
fn objects_merged_from_multiple_sources() {
240193
let toml_1 = r#"
241-
[data]
194+
[leaf.data]
242195
item_1 = 1
243196
"#;
244197
let toml_2 = r#"
245-
[data]
198+
[leaf.data]
199+
item_2 = 2
200+
"#;
201+
202+
let config = Root::builder()
203+
.override_with(TomlSource::new(toml_1))
204+
.override_with(TomlSource::new(toml_2))
205+
.try_build()
206+
.unwrap();
207+
208+
assert_eq!(config.leaf.data, json!({"item_1": 1, "item_2": 2}));
209+
}
210+
211+
#[test]
212+
fn object_merged_missing_in_first_source() {
213+
let toml_1 = r#"
214+
[leaf.data]
246215
item_2 = 2
247216
"#;
217+
let toml_2 = "[leaf]";
248218

249-
let config = Config::builder()
219+
let config = Root::builder()
250220
.override_with(TomlSource::new(toml_1))
251221
.override_with(TomlSource::new(toml_2))
252222
.try_build()
253223
.unwrap();
254224

255-
assert_eq!(config.data, json!({"item_1": 1, "item_2": 2}));
225+
assert_eq!(config.leaf.data, json!({ "item_2": 2 }));
256226
}
257227
}
258228
}

0 commit comments

Comments
 (0)