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