use serde_json::Value;
pub fn deep_merge(base: &mut Value, overlay: &Value) {
match (base, overlay) {
(Value::Object(base_map), Value::Object(overlay_map)) => {
for (key, value) in overlay_map {
if value.is_null() {
base_map.remove(key);
} else if let Some(base_value) = base_map.get_mut(key) {
deep_merge(base_value, value);
} else {
base_map.insert(key.clone(), value.clone());
}
}
}
(base, overlay) => {
*base = overlay.clone();
}
}
}
pub fn strip_null_entries(value: &mut Value) {
match value {
Value::Object(map) => {
map.retain(|_, v| !v.is_null());
for v in map.values_mut() {
strip_null_entries(v);
}
}
Value::Array(arr) => {
for item in arr.iter_mut() {
strip_null_entries(item);
}
}
_ => {}
}
}
pub fn normalize_keys(value: &mut Value) {
match value {
Value::Object(map) => {
let entries: Vec<(String, Value)> = map
.iter()
.map(|(k, v)| (camel_to_snake(k), v.clone()))
.collect();
map.clear();
for (key, mut val) in entries {
normalize_keys(&mut val);
map.insert(key, val);
}
}
Value::Array(arr) => {
for item in arr.iter_mut() {
normalize_keys(item);
}
}
_ => {}
}
}
fn camel_to_snake(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
let chars: Vec<char> = s.chars().collect();
for (i, &ch) in chars.iter().enumerate() {
if ch.is_uppercase() {
if i > 0 {
let prev_lower = chars[i - 1].is_lowercase();
let next_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());
if prev_lower || next_lower {
result.push('_');
}
}
result.push(ch.to_lowercase().next().unwrap());
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn merge_objects_recursively() {
let mut base = json!({"a": {"x": 1, "y": 2}, "b": 10});
let overlay = json!({"a": {"y": 3, "z": 4}, "c": 20});
deep_merge(&mut base, &overlay);
assert_eq!(base["a"]["x"], 1); assert_eq!(base["a"]["y"], 3); assert_eq!(base["a"]["z"], 4); assert_eq!(base["b"], 10); assert_eq!(base["c"], 20); }
#[test]
fn merge_arrays_replaced_not_concatenated() {
let mut base = json!({"list": [1, 2, 3]});
let overlay = json!({"list": [4, 5]});
deep_merge(&mut base, &overlay);
assert_eq!(base["list"], json!([4, 5]));
}
#[test]
fn strip_null_entries_removes_null_leaves() {
let mut v = json!({"a": 1, "b": null, "c": {"x": null, "y": 2}});
strip_null_entries(&mut v);
assert_eq!(v, json!({"a": 1, "c": {"y": 2}}));
}
#[test]
fn merge_null_deletes_key() {
let mut base = json!({"a": 1, "b": 2, "c": 3});
let overlay = json!({"b": null});
deep_merge(&mut base, &overlay);
assert_eq!(base["a"], 1);
assert!(base.get("b").is_none());
assert_eq!(base["c"], 3);
}
#[test]
fn merge_scalars_overridden() {
let mut base = json!({"x": "old", "y": 42});
let overlay = json!({"x": "new", "y": 99});
deep_merge(&mut base, &overlay);
assert_eq!(base["x"], "new");
assert_eq!(base["y"], 99);
}
#[test]
fn merge_nested_null_deletes_nested_key() {
let mut base = json!({"a": {"x": 1, "y": 2}});
let overlay = json!({"a": {"x": null}});
deep_merge(&mut base, &overlay);
assert!(base["a"].get("x").is_none());
assert_eq!(base["a"]["y"], 2);
}
#[test]
fn merge_empty_overlay_preserves_base() {
let mut base = json!({"a": 1});
let overlay = json!({});
deep_merge(&mut base, &overlay);
assert_eq!(base["a"], 1);
}
#[test]
fn merge_into_empty_base() {
let mut base = json!({});
let overlay = json!({"a": 1, "b": {"c": 2}});
deep_merge(&mut base, &overlay);
assert_eq!(base["a"], 1);
assert_eq!(base["b"]["c"], 2);
}
#[test]
fn camel_to_snake_simple() {
assert_eq!(camel_to_snake("mcpServers"), "mcp_servers");
assert_eq!(camel_to_snake("maxTokens"), "max_tokens");
assert_eq!(camel_to_snake("allowFrom"), "allow_from");
}
#[test]
fn camel_to_snake_already_snake() {
assert_eq!(camel_to_snake("already_snake"), "already_snake");
assert_eq!(camel_to_snake("simple"), "simple");
}
#[test]
fn camel_to_snake_consecutive_uppercase() {
assert_eq!(camel_to_snake("HTMLParser"), "html_parser");
assert_eq!(camel_to_snake("getHTTPResponse"), "get_http_response");
}
#[test]
fn camel_to_snake_single_char() {
assert_eq!(camel_to_snake("A"), "a");
assert_eq!(camel_to_snake("x"), "x");
}
#[test]
fn normalize_camel_to_snake() {
let mut val = json!({"mcpServers": {"testServer": {"apiKey": "k"}}});
normalize_keys(&mut val);
assert!(val.get("mcp_servers").is_some());
assert!(val["mcp_servers"].get("test_server").is_some());
assert!(val["mcp_servers"]["test_server"].get("api_key").is_some());
}
#[test]
fn normalize_preserves_scalar_values() {
let mut val = json!({"maxTokens": 8192, "apiKey": "secret"});
normalize_keys(&mut val);
assert_eq!(val["max_tokens"], 8192);
assert_eq!(val["api_key"], "secret");
}
#[test]
fn normalize_handles_arrays() {
let mut val = json!({"allowFrom": [{"userId": "u1"}]});
normalize_keys(&mut val);
assert!(val.get("allow_from").is_some());
assert_eq!(val["allow_from"][0]["user_id"], "u1");
}
#[test]
fn normalize_empty_object() {
let mut val = json!({});
normalize_keys(&mut val);
assert_eq!(val, json!({}));
}
#[test]
fn normalize_non_object_is_noop() {
let mut val = json!(42);
normalize_keys(&mut val);
assert_eq!(val, json!(42));
}
}