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]
22pub fn merge(base: Value, overlay: Value) -> Value {
23 merge_with(base, overlay, |_| ArrayStrategy::Replace)
24}
25
26#[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 (_, 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}