Skip to main content

confex_core/
merge.rs

1use crate::value::ConfigValue;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum MergeStrategy {
5    /// Last writer wins. Default for scalars, enums, and arrays.
6    Replace,
7    /// Override only when the incoming value is non-null. Suitable for `Option<T>` fields.
8    ReplaceIfPresent,
9    /// Concatenate arrays; fall back to `Replace` for non-array values.
10    Extend,
11    /// Recurse into nested tables; fall back to `Replace` otherwise.
12    Deep,
13}
14
15/// Pairs of `(field_name, strategy)` that override the default `Replace` behavior.
16pub type FieldStrategies = &'static [(&'static str, MergeStrategy)];
17
18/// Merge `override_val` on top of `base`, applying per-field strategies where specified.
19///
20/// A top-level `Null` override always leaves `base` unchanged.
21#[must_use]
22pub fn merge(
23    base: ConfigValue,
24    override_val: ConfigValue,
25    strategies: FieldStrategies,
26) -> ConfigValue {
27    if override_val.is_null() {
28        return base;
29    }
30
31    match (base, override_val) {
32        (ConfigValue::Table(mut base_map), ConfigValue::Table(over_map)) => {
33            for (key, over_val) in over_map {
34                let strategy = strategies
35                    .iter()
36                    .find(|(k, _)| *k == key.as_str())
37                    .map_or(MergeStrategy::Replace, |(_, s)| *s);
38
39                let current = base_map.remove(&key);
40
41                let merged = match strategy {
42                    MergeStrategy::Replace => over_val,
43                    MergeStrategy::ReplaceIfPresent => {
44                        if over_val.is_null() {
45                            if let Some(kept) = current {
46                                base_map.insert(key, kept);
47                            }
48                            continue;
49                        }
50                        over_val
51                    }
52                    MergeStrategy::Extend => match (current, over_val) {
53                        (Some(ConfigValue::Array(mut base_arr)), ConfigValue::Array(over_arr)) => {
54                            base_arr.extend(over_arr);
55                            ConfigValue::Array(base_arr)
56                        }
57                        (_, val) => val,
58                    },
59                    MergeStrategy::Deep => match (current, over_val) {
60                        (Some(base_inner), over_inner) => merge(base_inner, over_inner, strategies),
61                        (None, val) => val,
62                    },
63                };
64
65                base_map.insert(key, merged);
66            }
67            ConfigValue::Table(base_map)
68        }
69        (_, override_val) => override_val,
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use std::collections::BTreeMap;
76
77    use super::*;
78
79    fn table(pairs: &[(&str, ConfigValue)]) -> ConfigValue {
80        let map: BTreeMap<String, ConfigValue> = pairs
81            .iter()
82            .map(|(k, v)| ((*k).to_owned(), v.clone()))
83            .collect();
84        ConfigValue::Table(map)
85    }
86
87    #[test]
88    fn replace_overwrites() {
89        let base = table(&[("key", ConfigValue::Integer(1))]);
90        let over = table(&[("key", ConfigValue::Integer(2))]);
91        assert_eq!(
92            merge(base, over, &[]),
93            table(&[("key", ConfigValue::Integer(2))])
94        );
95    }
96
97    #[test]
98    fn null_override_leaves_base() {
99        let base = table(&[("key", ConfigValue::Integer(1))]);
100        assert_eq!(merge(base.clone(), ConfigValue::Null, &[]), base);
101    }
102
103    #[test]
104    fn replace_if_present_keeps_base_when_override_null() {
105        let base = table(&[("key", ConfigValue::String("base".into()))]);
106        let over = table(&[("key", ConfigValue::Null)]);
107        assert_eq!(
108            merge(base, over, &[("key", MergeStrategy::ReplaceIfPresent)]),
109            table(&[("key", ConfigValue::String("base".into()))])
110        );
111    }
112
113    #[test]
114    fn replace_if_present_replaces_when_override_non_null() {
115        let base = table(&[("key", ConfigValue::Integer(1))]);
116        let over = table(&[("key", ConfigValue::Integer(2))]);
117        assert_eq!(
118            merge(base, over, &[("key", MergeStrategy::ReplaceIfPresent)]),
119            table(&[("key", ConfigValue::Integer(2))])
120        );
121    }
122
123    #[test]
124    fn deep_merge_nested_tables() {
125        let base = table(&[(
126            "nested",
127            table(&[
128                ("a", ConfigValue::Integer(1)),
129                ("b", ConfigValue::Integer(2)),
130            ]),
131        )]);
132        let over = table(&[(
133            "nested",
134            table(&[
135                ("b", ConfigValue::Integer(3)),
136                ("c", ConfigValue::Integer(4)),
137            ]),
138        )]);
139        assert_eq!(
140            merge(base, over, &[("nested", MergeStrategy::Deep)]),
141            table(&[(
142                "nested",
143                table(&[
144                    ("a", ConfigValue::Integer(1)),
145                    ("b", ConfigValue::Integer(3)),
146                    ("c", ConfigValue::Integer(4)),
147                ])
148            )])
149        );
150    }
151
152    #[test]
153    fn extend_concatenates_arrays() {
154        let base = table(&[(
155            "tags",
156            ConfigValue::Array(vec![ConfigValue::String("a".into())]),
157        )]);
158        let over = table(&[(
159            "tags",
160            ConfigValue::Array(vec![ConfigValue::String("b".into())]),
161        )]);
162        assert_eq!(
163            merge(base, over, &[("tags", MergeStrategy::Extend)]),
164            table(&[(
165                "tags",
166                ConfigValue::Array(vec![
167                    ConfigValue::String("a".into()),
168                    ConfigValue::String("b".into()),
169                ])
170            )])
171        );
172    }
173}