Skip to main content

rskit_codec/value/
merge.rs

1use serde_json::Value;
2
3/// How to combine two arrays found at the same key during a merge.
4///
5/// The strategy is chosen per key by the caller (see [`merge_with`]); the merge
6/// mechanism itself is policy-free — it does not know what any key "means".
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ArrayStrategy {
10    /// The overlay array replaces the base array wholesale (last-wins).
11    Replace,
12    /// The overlay array is appended to the base array (concatenation).
13    Concat,
14}
15
16/// Deep-merge `overlay` onto `base`, replacing arrays wholesale.
17///
18/// Objects merge recursively; on a key collision the overlay value wins
19/// (last-wins scalars). Every array is replaced by the overlay. Use
20/// [`merge_with`] to concatenate selected arrays instead.
21#[must_use]
22pub fn merge(base: Value, overlay: Value) -> Value {
23    merge_with(base, overlay, |_| ArrayStrategy::Replace)
24}
25
26/// Deep-merge `overlay` onto `base`, choosing an array strategy per key.
27///
28/// Objects merge recursively; on a key collision the overlay value wins. When
29/// both sides hold an array at the same key, `array_strategy` is consulted with
30/// that key to decide [`ArrayStrategy::Replace`] vs [`ArrayStrategy::Concat`].
31/// Type mismatches (for example object vs scalar) resolve to the overlay.
32///
33/// The mechanism is framework-agnostic: identity rules, duplicate detection, and
34/// "which keys are mergeable lists" are caller policy expressed through
35/// `array_strategy`, not built in here.
36#[must_use]
37pub fn merge_with<F>(base: Value, overlay: Value, mut array_strategy: F) -> Value
38where
39    F: FnMut(&str) -> ArrayStrategy,
40{
41    merge_inner(base, overlay, None, &mut array_strategy)
42}
43
44fn merge_inner<F>(base: Value, overlay: Value, key: Option<&str>, array_strategy: &mut F) -> Value
45where
46    F: FnMut(&str) -> ArrayStrategy,
47{
48    match (base, overlay) {
49        (Value::Object(mut base), Value::Object(overlay)) => {
50            for (key, overlay_value) in overlay {
51                let merged = match base.remove(&key) {
52                    Some(base_value) => {
53                        merge_inner(base_value, overlay_value, Some(&key), array_strategy)
54                    }
55                    None => overlay_value,
56                };
57                base.insert(key, merged);
58            }
59            Value::Object(base)
60        }
61        (Value::Array(mut base), Value::Array(overlay)) => {
62            match key.map_or(ArrayStrategy::Replace, array_strategy) {
63                ArrayStrategy::Concat => {
64                    base.extend(overlay);
65                    Value::Array(base)
66                }
67                ArrayStrategy::Replace => Value::Array(overlay),
68            }
69        }
70        // Scalars and type mismatches: overlay wins.
71        (_, overlay) => overlay,
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use serde_json::json;
79
80    #[test]
81    fn overlay_scalar_wins_last() {
82        let merged = merge(
83            json!({ "name": "base", "retries": 1 }),
84            json!({ "retries": 5 }),
85        );
86
87        assert_eq!(merged, json!({ "name": "base", "retries": 5 }));
88    }
89
90    #[test]
91    fn objects_merge_recursively() {
92        let merged = merge(
93            json!({ "server": { "host": "a", "port": 1 } }),
94            json!({ "server": { "port": 2 } }),
95        );
96
97        assert_eq!(merged, json!({ "server": { "host": "a", "port": 2 } }));
98    }
99
100    #[test]
101    fn overlay_adds_new_keys() {
102        let merged = merge(json!({ "a": 1 }), json!({ "b": 2 }));
103
104        assert_eq!(merged, json!({ "a": 1, "b": 2 }));
105    }
106
107    #[test]
108    fn unselected_arrays_replace_under_concat_strategy() {
109        let merged = merge_with(
110            json!({ "groups": [1], "ports": [1, 2] }),
111            json!({ "groups": [2], "ports": [9] }),
112            |key| {
113                if key == "groups" {
114                    ArrayStrategy::Concat
115                } else {
116                    ArrayStrategy::Replace
117                }
118            },
119        );
120
121        assert_eq!(merged, json!({ "groups": [1, 2], "ports": [9] }));
122    }
123
124    #[test]
125    fn arrays_replace_by_default() {
126        let merged = merge(json!({ "ports": [1, 2, 3] }), json!({ "ports": [9] }));
127
128        assert_eq!(merged, json!({ "ports": [9] }));
129    }
130
131    #[test]
132    fn selected_arrays_concatenate() {
133        let merged = merge_with(
134            json!({ "groups": [{ "name": "a" }] }),
135            json!({ "groups": [{ "name": "b" }] }),
136            |key| {
137                if key == "groups" {
138                    ArrayStrategy::Concat
139                } else {
140                    ArrayStrategy::Replace
141                }
142            },
143        );
144
145        assert_eq!(
146            merged,
147            json!({ "groups": [{ "name": "a" }, { "name": "b" }] })
148        );
149    }
150
151    #[test]
152    fn type_mismatch_resolves_to_overlay() {
153        let merged = merge(json!({ "x": { "deep": true } }), json!({ "x": 5 }));
154
155        assert_eq!(merged, json!({ "x": 5 }));
156    }
157}