Skip to main content

mq_rest_admin/
mapping_merge.rs

1//! Validation and merging of mapping overrides.
2
3use serde_json::Value;
4
5/// Mode for applying mapping overrides.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum MappingOverrideMode {
8    /// Sparse merge — override entries are layered on top of the built-in data.
9    Merge,
10    /// Complete replacement — override data replaces the built-in data entirely.
11    Replace,
12}
13
14const VALID_TOP_LEVEL_KEYS: &[&str] = &["commands", "qualifiers"];
15
16const VALID_QUALIFIER_SUB_KEYS: &[&str] = &[
17    "request_key_map",
18    "request_key_value_map",
19    "request_value_map",
20    "response_key_map",
21    "response_value_map",
22];
23
24/// Validate the structure of a mapping overrides value.
25///
26/// # Errors
27///
28/// Returns `Err` with a descriptive message for type violations or invalid keys.
29pub fn validate_mapping_overrides(overrides: &Value) -> Result<(), String> {
30    let obj = overrides
31        .as_object()
32        .ok_or("mapping_overrides must be a JSON object")?;
33    for key in obj.keys() {
34        if !VALID_TOP_LEVEL_KEYS.contains(&key.as_str()) {
35            return Err(format!(
36                "Invalid top-level key in mapping_overrides: {key:?} (expected subset of {VALID_TOP_LEVEL_KEYS:?})"
37            ));
38        }
39    }
40    validate_commands_section(obj.get("commands"))?;
41    validate_qualifiers_section(obj.get("qualifiers"))?;
42    Ok(())
43}
44
45fn validate_commands_section(commands: Option<&Value>) -> Result<(), String> {
46    let Some(commands) = commands else {
47        return Ok(());
48    };
49    let obj = commands
50        .as_object()
51        .ok_or("mapping_overrides['commands'] must be an object")?;
52    for (key, entry) in obj {
53        if !entry.is_object() {
54            return Err(format!(
55                "mapping_overrides['commands'][{key:?}] must be an object"
56            ));
57        }
58    }
59    Ok(())
60}
61
62fn validate_qualifiers_section(qualifiers: Option<&Value>) -> Result<(), String> {
63    let Some(qualifiers) = qualifiers else {
64        return Ok(());
65    };
66    let obj = qualifiers
67        .as_object()
68        .ok_or("mapping_overrides['qualifiers'] must be an object")?;
69    for (key, entry) in obj {
70        let entry_obj = entry
71            .as_object()
72            .ok_or_else(|| format!("mapping_overrides['qualifiers'][{key:?}] must be an object"))?;
73        validate_qualifier_entry(key, entry_obj)?;
74    }
75    Ok(())
76}
77
78fn validate_qualifier_entry(
79    qualifier_key: &str,
80    entry: &serde_json::Map<String, Value>,
81) -> Result<(), String> {
82    for sub_key in entry.keys() {
83        if !VALID_QUALIFIER_SUB_KEYS.contains(&sub_key.as_str()) {
84            return Err(format!(
85                "Invalid sub-key {sub_key:?} in mapping_overrides['qualifiers'][{qualifier_key:?}] \
86                 (expected subset of {VALID_QUALIFIER_SUB_KEYS:?})"
87            ));
88        }
89        if !entry[sub_key].is_object() {
90            return Err(format!(
91                "mapping_overrides['qualifiers'][{qualifier_key:?}][{sub_key:?}] must be an object"
92            ));
93        }
94    }
95    Ok(())
96}
97
98/// Deep-copy `base` and merge `overrides` into it.
99#[must_use]
100pub fn merge_mapping_data(base: &Value, overrides: &Value) -> Value {
101    let mut merged = base.clone();
102    merge_commands(&mut merged, overrides.get("commands"));
103    merge_qualifiers(&mut merged, overrides.get("qualifiers"));
104    merged
105}
106
107fn merge_commands(merged: &mut Value, override_commands: Option<&Value>) {
108    let Some(override_obj) = override_commands.and_then(Value::as_object) else {
109        return;
110    };
111    let base_commands = merged
112        .as_object_mut()
113        .unwrap()
114        .entry("commands")
115        .or_insert_with(|| Value::Object(serde_json::Map::new()));
116    let base_obj = base_commands.as_object_mut().unwrap();
117    for (key, entry) in override_obj {
118        if let Some(entry_obj) = entry.as_object() {
119            if let Some(existing) = base_obj.get_mut(key).and_then(Value::as_object_mut) {
120                for (entry_key, entry_value) in entry_obj {
121                    existing.insert(entry_key.clone(), entry_value.clone());
122                }
123            } else {
124                base_obj.insert(key.clone(), entry.clone());
125            }
126        }
127    }
128}
129
130fn merge_qualifiers(merged: &mut Value, override_qualifiers: Option<&Value>) {
131    let Some(override_obj) = override_qualifiers.and_then(Value::as_object) else {
132        return;
133    };
134    let base_qualifiers = merged
135        .as_object_mut()
136        .unwrap()
137        .entry("qualifiers")
138        .or_insert_with(|| Value::Object(serde_json::Map::new()));
139    let base_obj = base_qualifiers.as_object_mut().unwrap();
140    for (key, entry) in override_obj {
141        if let Some(entry_obj) = entry.as_object() {
142            if let Some(existing) = base_obj.get_mut(key).and_then(Value::as_object_mut) {
143                for (sub_key, sub_value) in entry_obj {
144                    if let Some(sub_obj) = sub_value.as_object() {
145                        if let Some(existing_sub) =
146                            existing.get_mut(sub_key).and_then(Value::as_object_mut)
147                        {
148                            for (key, value) in sub_obj {
149                                existing_sub.insert(key.clone(), value.clone());
150                            }
151                        } else {
152                            existing.insert(sub_key.clone(), sub_value.clone());
153                        }
154                    }
155                }
156            } else {
157                base_obj.insert(key.clone(), entry.clone());
158            }
159        }
160    }
161}
162
163/// Validate that `overrides` covers all command and qualifier keys in `base`.
164///
165/// # Errors
166///
167/// Returns `Err` listing any command or qualifier keys present in `base` but
168/// missing from `overrides`.
169pub fn validate_mapping_overrides_complete(base: &Value, overrides: &Value) -> Result<(), String> {
170    let mut missing_parts = Vec::new();
171
172    if let Some(base_commands) = base.get("commands").and_then(Value::as_object) {
173        let override_commands = overrides
174            .get("commands")
175            .and_then(Value::as_object)
176            .cloned()
177            .unwrap_or_default();
178        let mut missing: Vec<&str> = base_commands
179            .keys()
180            .filter(|k| !override_commands.contains_key(k.as_str()))
181            .map(String::as_str)
182            .collect();
183        missing.sort_unstable();
184        for key in missing {
185            missing_parts.push(format!("commands: {key}"));
186        }
187    }
188
189    if let Some(base_qualifiers) = base.get("qualifiers").and_then(Value::as_object) {
190        let override_qualifiers = overrides
191            .get("qualifiers")
192            .and_then(Value::as_object)
193            .cloned()
194            .unwrap_or_default();
195        let mut missing: Vec<&str> = base_qualifiers
196            .keys()
197            .filter(|k| !override_qualifiers.contains_key(k.as_str()))
198            .map(String::as_str)
199            .collect();
200        missing.sort_unstable();
201        for key in missing {
202            missing_parts.push(format!("qualifiers: {key}"));
203        }
204    }
205
206    if !missing_parts.is_empty() {
207        let detail: Vec<String> = missing_parts.iter().map(|e| format!("  {e}")).collect();
208        return Err(format!(
209            "mapping_overrides is incomplete for REPLACE mode. Missing entries:\n{}",
210            detail.join("\n")
211        ));
212    }
213    Ok(())
214}
215
216/// Return a deep copy of `overrides` as the complete mapping data.
217#[must_use]
218pub fn replace_mapping_data(overrides: &Value) -> Value {
219    overrides.clone()
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use serde_json::json;
226
227    // ---- validate_mapping_overrides ----
228
229    #[test]
230    fn validate_valid_overrides() {
231        let v = json!({"commands": {}, "qualifiers": {}});
232        assert!(validate_mapping_overrides(&v).is_ok());
233    }
234
235    #[test]
236    fn validate_not_object() {
237        let v = json!("string");
238        assert!(validate_mapping_overrides(&v).is_err());
239    }
240
241    #[test]
242    fn validate_invalid_top_key() {
243        let v = json!({"bogus": {}});
244        let err = validate_mapping_overrides(&v).unwrap_err();
245        assert!(err.contains("Invalid top-level key"));
246    }
247
248    #[test]
249    fn validate_commands_not_object() {
250        let v = json!({"commands": "bad"});
251        let err = validate_mapping_overrides(&v).unwrap_err();
252        assert!(err.contains("commands"));
253    }
254
255    #[test]
256    fn validate_commands_entry_not_object() {
257        let v = json!({"commands": {"DISPLAY QUEUE": "bad"}});
258        let err = validate_mapping_overrides(&v).unwrap_err();
259        assert!(err.contains("DISPLAY QUEUE"));
260    }
261
262    #[test]
263    fn validate_qualifiers_not_object() {
264        let v = json!({"qualifiers": "bad"});
265        let err = validate_mapping_overrides(&v).unwrap_err();
266        assert!(err.contains("qualifiers"));
267    }
268
269    #[test]
270    fn validate_qualifier_entry_not_object() {
271        let v = json!({"qualifiers": {"queue": "bad"}});
272        let err = validate_mapping_overrides(&v).unwrap_err();
273        assert!(err.contains("queue"));
274    }
275
276    #[test]
277    fn validate_qualifier_invalid_sub_key() {
278        let v = json!({"qualifiers": {"queue": {"bogus_key": {}}}});
279        let err = validate_mapping_overrides(&v).unwrap_err();
280        assert!(err.contains("bogus_key"));
281    }
282
283    #[test]
284    fn validate_qualifier_sub_value_not_object() {
285        let v = json!({"qualifiers": {"queue": {"request_key_map": "bad"}}});
286        let err = validate_mapping_overrides(&v).unwrap_err();
287        assert!(err.contains("request_key_map"));
288    }
289
290    // ---- merge_mapping_data ----
291
292    #[test]
293    fn merge_new_command() {
294        let base = json!({"commands": {"A": {"q": "x"}}, "qualifiers": {}});
295        let overrides = json!({"commands": {"B": {"q": "y"}}});
296        let merged = merge_mapping_data(&base, &overrides);
297        assert!(merged["commands"]["A"].is_object());
298        assert!(merged["commands"]["B"].is_object());
299    }
300
301    #[test]
302    fn merge_existing_command() {
303        let base = json!({"commands": {"A": {"old": "1"}}, "qualifiers": {}});
304        let overrides = json!({"commands": {"A": {"new": "2"}}});
305        let merged = merge_mapping_data(&base, &overrides);
306        assert_eq!(merged["commands"]["A"]["old"], "1");
307        assert_eq!(merged["commands"]["A"]["new"], "2");
308    }
309
310    #[test]
311    fn merge_new_qualifier() {
312        let base = json!({"commands": {}, "qualifiers": {"q1": {"request_key_map": {"a": "A"}}}});
313        let overrides = json!({"qualifiers": {"q2": {"request_key_map": {"b": "B"}}}});
314        let merged = merge_mapping_data(&base, &overrides);
315        assert!(merged["qualifiers"]["q1"].is_object());
316        assert!(merged["qualifiers"]["q2"].is_object());
317    }
318
319    #[test]
320    fn merge_existing_qualifier_nested() {
321        let base = json!({"commands": {}, "qualifiers": {"q1": {"request_key_map": {"a": "A"}}}});
322        let overrides = json!({"qualifiers": {"q1": {"request_key_map": {"b": "B"}}}});
323        let merged = merge_mapping_data(&base, &overrides);
324        assert_eq!(merged["qualifiers"]["q1"]["request_key_map"]["a"], "A");
325        assert_eq!(merged["qualifiers"]["q1"]["request_key_map"]["b"], "B");
326    }
327
328    #[test]
329    fn merge_no_commands_override() {
330        let base = json!({"commands": {"A": {}}, "qualifiers": {}});
331        let overrides = json!({"qualifiers": {}});
332        let merged = merge_mapping_data(&base, &overrides);
333        assert!(merged["commands"]["A"].is_object());
334    }
335
336    #[test]
337    fn merge_no_qualifiers_override() {
338        let base = json!({"commands": {}, "qualifiers": {"q1": {}}});
339        let overrides = json!({"commands": {}});
340        let merged = merge_mapping_data(&base, &overrides);
341        assert!(merged["qualifiers"]["q1"].is_object());
342    }
343
344    // ---- validate_mapping_overrides_complete ----
345
346    #[test]
347    fn validate_complete_ok() {
348        let base = json!({"commands": {"A": {}}, "qualifiers": {"q1": {}}});
349        let overrides = json!({"commands": {"A": {}}, "qualifiers": {"q1": {}}});
350        assert!(validate_mapping_overrides_complete(&base, &overrides).is_ok());
351    }
352
353    #[test]
354    fn validate_complete_missing_commands() {
355        let base = json!({"commands": {"A": {}, "B": {}}, "qualifiers": {}});
356        let overrides = json!({"commands": {"A": {}}, "qualifiers": {}});
357        let err = validate_mapping_overrides_complete(&base, &overrides).unwrap_err();
358        assert!(err.contains("commands: B"));
359    }
360
361    #[test]
362    fn validate_complete_missing_qualifiers() {
363        let base = json!({"commands": {}, "qualifiers": {"q1": {}, "q2": {}}});
364        let overrides = json!({"commands": {}, "qualifiers": {"q1": {}}});
365        let err = validate_mapping_overrides_complete(&base, &overrides).unwrap_err();
366        assert!(err.contains("qualifiers: q2"));
367    }
368
369    #[test]
370    fn validate_complete_missing_both() {
371        let base = json!({"commands": {"A": {}}, "qualifiers": {"q1": {}}});
372        let overrides = json!({"commands": {}, "qualifiers": {}});
373        let err = validate_mapping_overrides_complete(&base, &overrides).unwrap_err();
374        assert!(err.contains("commands: A"));
375        assert!(err.contains("qualifiers: q1"));
376    }
377
378    #[test]
379    fn validate_qualifier_with_valid_sub_keys() {
380        let v = json!({
381            "qualifiers": {
382                "queue": {
383                    "request_key_map": {"a": "A"},
384                    "response_key_map": {"B": "b"}
385                }
386            }
387        });
388        assert!(validate_mapping_overrides(&v).is_ok());
389    }
390
391    #[test]
392    fn validate_commands_with_valid_entries() {
393        let v = json!({
394            "commands": {
395                "DISPLAY QUEUE": {"qualifier": "queue"},
396                "ALTER QMGR": {"qualifier": "qmgr"}
397            }
398        });
399        assert!(validate_mapping_overrides(&v).is_ok());
400    }
401
402    #[test]
403    fn validate_only_commands_key() {
404        let v = json!({"commands": {}});
405        assert!(validate_mapping_overrides(&v).is_ok());
406    }
407
408    #[test]
409    fn validate_only_qualifiers_key() {
410        let v = json!({"qualifiers": {}});
411        assert!(validate_mapping_overrides(&v).is_ok());
412    }
413
414    #[test]
415    fn validate_empty_object() {
416        let v = json!({});
417        assert!(validate_mapping_overrides(&v).is_ok());
418    }
419
420    #[test]
421    fn merge_qualifier_new_sub_key() {
422        let base = json!({
423            "commands": {},
424            "qualifiers": {
425                "q1": {
426                    "request_key_map": {"a": "A"}
427                }
428            }
429        });
430        let overrides = json!({
431            "qualifiers": {
432                "q1": {
433                    "response_key_map": {"B": "b"}
434                }
435            }
436        });
437        let merged = merge_mapping_data(&base, &overrides);
438        assert_eq!(merged["qualifiers"]["q1"]["request_key_map"]["a"], "A");
439        assert_eq!(merged["qualifiers"]["q1"]["response_key_map"]["B"], "b");
440    }
441
442    #[test]
443    fn merge_qualifier_non_object_sub_value_ignored() {
444        let base = json!({"commands": {}, "qualifiers": {"q1": {"request_key_map": {"a": "A"}}}});
445        let overrides = json!({"qualifiers": {"q1": {"request_key_map": "not_object"}}});
446        let merged = merge_mapping_data(&base, &overrides);
447        // Non-object sub-value should be ignored, original preserved
448        assert_eq!(merged["qualifiers"]["q1"]["request_key_map"]["a"], "A");
449    }
450
451    #[test]
452    fn merge_command_non_object_entry_ignored() {
453        let base = json!({"commands": {"A": {"old": "1"}}, "qualifiers": {}});
454        let overrides = json!({"commands": {"A": "not_object"}});
455        let merged = merge_mapping_data(&base, &overrides);
456        assert_eq!(merged["commands"]["A"]["old"], "1");
457    }
458
459    #[test]
460    fn merge_qualifier_non_object_entry_ignored() {
461        let base = json!({"commands": {}, "qualifiers": {"q1": {"request_key_map": {"a": "A"}}}});
462        let overrides = json!({"qualifiers": {"q1": "not_object"}});
463        let merged = merge_mapping_data(&base, &overrides);
464        assert_eq!(merged["qualifiers"]["q1"]["request_key_map"]["a"], "A");
465    }
466
467    #[test]
468    fn validate_complete_no_commands_in_base() {
469        let base = json!({"qualifiers": {"q1": {}}});
470        let overrides = json!({"qualifiers": {"q1": {}}});
471        assert!(validate_mapping_overrides_complete(&base, &overrides).is_ok());
472    }
473
474    #[test]
475    fn validate_complete_no_qualifiers_in_base() {
476        let base = json!({"commands": {"A": {}}});
477        let overrides = json!({"commands": {"A": {}}});
478        assert!(validate_mapping_overrides_complete(&base, &overrides).is_ok());
479    }
480
481    // ---- replace_mapping_data ----
482
483    #[test]
484    fn replace_returns_clone() {
485        let overrides = json!({"commands": {"X": {}}, "qualifiers": {}});
486        let result = replace_mapping_data(&overrides);
487        assert_eq!(result, overrides);
488    }
489
490    #[test]
491    fn merge_into_base_missing_commands_key() {
492        let base = json!({"qualifiers": {}});
493        let overrides = json!({"commands": {"NEW_CMD": {"key": "value"}}});
494        let result = merge_mapping_data(&base, &overrides);
495        assert!(result.get("commands").unwrap().get("NEW_CMD").is_some());
496    }
497
498    #[test]
499    fn merge_into_base_missing_qualifiers_key() {
500        let base = json!({"commands": {}});
501        let overrides = json!({"qualifiers": {"NEW_QUAL": {"sub": {"k": "v"}}}});
502        let result = merge_mapping_data(&base, &overrides);
503        assert!(result.get("qualifiers").unwrap().get("NEW_QUAL").is_some());
504    }
505}