rskit_codec/value/
merge.rs1use serde_json::Value;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ArrayStrategy {
10 Replace,
12 Concat,
14}
15
16#[must_use]
21pub fn merge(base: Value, overlay: Value) -> Value {
22 merge_with(base, overlay, |_| ArrayStrategy::Replace)
23}
24
25#[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 (_, 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}