nap_core/merge/
normalization.rs1use serde_json::Value;
20
21pub fn normalize(base: &Value, candidate: &Value) -> Value {
35 merge_missing(base, candidate)
37}
38
39fn merge_missing(base: &Value, candidate: &Value) -> Value {
41 match (base, candidate) {
42 (Value::Object(base_map), Value::Object(candidate_map)) => {
44 let mut result = candidate_map.clone();
45
46 for (key, base_val) in base_map {
47 match result.get(key) {
48 None => {
50 result.insert(key.clone(), base_val.clone());
51 }
52 Some(candidate_val) => {
54 let merged = merge_missing(base_val, candidate_val);
55 if &merged != candidate_val {
58 result.insert(key.clone(), merged);
59 }
60 }
61 }
62 }
63
64 Value::Object(result)
65 }
66
67 (Value::Object(_), _) => candidate.clone(),
70
71 _ => candidate.clone(),
74 }
75}
76
77pub fn is_explicit_null(value: &Value) -> bool {
79 value.is_null()
80}
81
82pub fn extract_optional(value: Option<&Value>) -> OptionalValue {
85 match value {
86 None => OptionalValue::Missing,
87 Some(v) if v.is_null() => OptionalValue::Null,
88 Some(v) => OptionalValue::Present(v.clone()),
89 }
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub enum OptionalValue {
95 Missing,
97 Null,
99 Present(Value),
101}
102
103#[cfg(test)]
106mod tests {
107 use super::*;
108 use serde_json::json;
109
110 #[test]
111 fn test_missing_field_copied_from_base() {
112 let base = json!({"name": "Obi Wan", "homeworld": "Stewjon"});
113 let candidate = json!({"name": "Obi Wan Kenobi"});
114
115 let result = normalize(&base, &candidate);
116
117 assert_eq!(result.get("name"), Some(&json!("Obi Wan Kenobi")));
118 assert_eq!(result.get("homeworld"), Some(&json!("Stewjon")));
119 }
120
121 #[test]
122 fn test_explicit_null_preserved() {
123 let base = json!({"homeworld": "Stewjon"});
124 let candidate = json!({"homeworld": null});
125
126 let result = normalize(&base, &candidate);
127
128 assert_eq!(result.get("homeworld"), Some(&Value::Null));
129 }
130
131 #[test]
132 fn test_present_field_unchanged() {
133 let base = json!({"name": "Luke", "species": "human"});
134 let candidate = json!({"name": "Luke Skywalker", "species": "human"});
135
136 let result = normalize(&base, &candidate);
137
138 assert_eq!(result.get("name"), Some(&json!("Luke Skywalker")));
139 assert_eq!(result.get("species"), Some(&json!("human")));
140 }
141
142 #[test]
143 fn test_additions_are_preserved() {
144 let base = json!({"name": "Luke"});
145 let candidate = json!({"name": "Luke", "homeworld": "Tatooine"});
146
147 let result = normalize(&base, &candidate);
148
149 assert_eq!(result.get("name"), Some(&json!("Luke")));
150 assert_eq!(result.get("homeworld"), Some(&json!("Tatooine")));
151 }
152
153 #[test]
154 fn test_nested_missing_filled() {
155 let base = json!({
156 "character": {
157 "name": "Obi Wan",
158 "homeworld": "Stewjon"
159 }
160 });
161 let candidate = json!({
162 "character": {
163 "name": "Obi Wan Kenobi"
164 }
165 });
166
167 let result = normalize(&base, &candidate);
168
169 assert_eq!(
170 result.pointer("/character/name"),
171 Some(&json!("Obi Wan Kenobi"))
172 );
173 assert_eq!(
174 result.pointer("/character/homeworld"),
175 Some(&json!("Stewjon"))
176 );
177 }
178
179 #[test]
180 fn test_nested_null_preserved() {
181 let base = json!({
182 "character": {
183 "homeworld": "Stewjon",
184 "species": "human"
185 }
186 });
187 let candidate = json!({
188 "character": {
189 "homeworld": null,
190 "species": "human"
191 }
192 });
193
194 let result = normalize(&base, &candidate);
195
196 assert_eq!(result.pointer("/character/homeworld"), Some(&Value::Null));
197 assert_eq!(result.pointer("/character/species"), Some(&json!("human")));
198 }
199
200 #[test]
201 fn test_all_equal_no_change() {
202 let base = json!({"a": 1, "b": 2});
203 let candidate = json!({"a": 1, "b": 2});
204
205 let result = normalize(&base, &candidate);
206
207 assert_eq!(result, candidate);
208 }
209
210 #[test]
211 fn test_candidate_wins_for_new_paths() {
212 let base = json!({"a": 1});
213 let candidate = json!({"a": 1, "b": 2, "c": 3});
214
215 let result = normalize(&base, &candidate);
216
217 assert_eq!(result, candidate);
218 }
219
220 #[test]
221 fn test_empty_base_returns_candidate_unchanged() {
222 let base = json!({});
223 let candidate = json!({"a": 1, "b": 2});
224
225 let result = normalize(&base, &candidate);
226
227 assert_eq!(result, candidate);
228 }
229
230 #[test]
231 fn test_type_mismatch_candidate_wins() {
232 let base = json!({"nested": {"key": "value"}});
235 let candidate = json!({"nested": "replaced"});
236
237 let result = normalize(&base, &candidate);
238
239 assert_eq!(result, candidate);
240 }
241
242 #[test]
243 fn test_optional_value_semantics() {
244 assert_eq!(extract_optional(None), OptionalValue::Missing);
245 assert_eq!(extract_optional(Some(&Value::Null)), OptionalValue::Null);
246 assert_eq!(
247 extract_optional(Some(&json!("hello"))),
248 OptionalValue::Present(json!("hello"))
249 );
250 assert!(is_explicit_null(&Value::Null));
251 assert!(!is_explicit_null(&json!("hello")));
252 }
253}