use indexmap::IndexMap;
use serde_json::Value;
pub(crate) fn deep_merge(target: &mut IndexMap<String, Value>, source: IndexMap<String, Value>) {
for (key, source_val) in source {
match source_val {
Value::Object(source_obj) => match target.get_mut(&key) {
Some(Value::Object(target_obj)) => {
for (k, v) in source_obj {
deep_merge_json(target_obj, k, v);
}
}
_ => {
target.insert(key, Value::Object(source_obj));
}
},
source_val => {
target.insert(key, source_val);
}
}
}
}
fn deep_merge_json(target: &mut serde_json::Map<String, Value>, key: String, value: Value) {
match value {
Value::Object(source_obj) => match target.get_mut(&key) {
Some(Value::Object(target_obj)) => {
for (k, v) in source_obj {
deep_merge_json(target_obj, k, v);
}
}
_ => {
target.insert(key, Value::Object(source_obj));
}
},
value => {
target.insert(key, value);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json::json;
#[rstest]
fn deep_merge_replaces_top_level_scalar() {
let mut target: IndexMap<String, Value> = IndexMap::new();
target.insert("port".to_string(), json!(8080));
let mut source: IndexMap<String, Value> = IndexMap::new();
source.insert("port".to_string(), json!(9090));
deep_merge(&mut target, source);
assert_eq!(target.get("port").unwrap(), &json!(9090));
}
#[rstest]
fn deep_merge_recurses_into_nested_objects() {
let mut target: IndexMap<String, Value> = IndexMap::new();
target.insert(
"core".to_string(),
json!({"secret_key": "from-base", "debug": false}),
);
let mut source: IndexMap<String, Value> = IndexMap::new();
source.insert("core".to_string(), json!({"debug": true}));
deep_merge(&mut target, source);
let core = target.get("core").unwrap().as_object().unwrap();
assert_eq!(core.get("secret_key").unwrap(), &json!("from-base"));
assert_eq!(core.get("debug").unwrap(), &json!(true));
}
#[rstest]
fn deep_merge_preserves_distinct_top_level_keys() {
let mut target: IndexMap<String, Value> = IndexMap::new();
target.insert("core".to_string(), json!({"debug": false}));
let mut source: IndexMap<String, Value> = IndexMap::new();
source.insert("cache".to_string(), json!({"ttl": 60}));
deep_merge(&mut target, source);
assert!(target.get("core").is_some());
assert!(target.get("cache").is_some());
}
#[rstest]
fn deep_merge_replaces_array_wholesale() {
let mut target: IndexMap<String, Value> = IndexMap::new();
target.insert("hosts".to_string(), json!(["a", "b", "c"]));
let mut source: IndexMap<String, Value> = IndexMap::new();
source.insert("hosts".to_string(), json!(["x"]));
deep_merge(&mut target, source);
assert_eq!(target.get("hosts").unwrap(), &json!(["x"]));
}
#[rstest]
fn deep_merge_replaces_when_shapes_mismatch() {
let mut target: IndexMap<String, Value> = IndexMap::new();
target.insert("core".to_string(), json!({"debug": false}));
let mut source: IndexMap<String, Value> = IndexMap::new();
source.insert("core".to_string(), json!("disabled"));
deep_merge(&mut target, source);
assert_eq!(target.get("core").unwrap(), &json!("disabled"));
}
#[rstest]
fn deep_merge_recurses_through_three_levels() {
let mut target: IndexMap<String, Value> = IndexMap::new();
target.insert(
"core".to_string(),
json!({
"security": {
"secure_ssl_redirect": true,
"session_cookie_secure": false,
}
}),
);
let mut source: IndexMap<String, Value> = IndexMap::new();
source.insert(
"core".to_string(),
json!({
"security": {
"session_cookie_secure": true,
}
}),
);
deep_merge(&mut target, source);
let security = target
.get("core")
.unwrap()
.get("security")
.unwrap()
.as_object()
.unwrap();
assert_eq!(security.get("secure_ssl_redirect").unwrap(), &json!(true));
assert_eq!(security.get("session_cookie_secure").unwrap(), &json!(true));
}
}