use crate::json::{Map, Value};
pub type JsonPath = String;
#[derive(Debug, Clone)]
pub struct MergeConflict {
pub path: JsonPath,
pub base: Value,
pub ours: Value,
pub theirs: Value,
}
#[derive(Debug, Clone)]
pub struct MergeResult {
pub merged: Value,
pub conflicts: Vec<MergeConflict>,
}
impl MergeResult {
pub fn is_clean(&self) -> bool {
self.conflicts.is_empty()
}
}
pub fn three_way_merge(base: &Value, ours: &Value, theirs: &Value) -> MergeResult {
let mut conflicts = Vec::new();
let merged = merge_at(base, ours, theirs, "", &mut conflicts);
MergeResult { merged, conflicts }
}
fn merge_at(
base: &Value,
ours: &Value,
theirs: &Value,
path: &str,
conflicts: &mut Vec<MergeConflict>,
) -> Value {
if ours == theirs {
return ours.clone();
}
if ours == base {
return theirs.clone();
}
if theirs == base {
return ours.clone();
}
match (ours, theirs) {
(Value::Object(o), Value::Object(t)) => {
let empty = Map::new();
let b = match base {
Value::Object(m) => m,
_ => &empty,
};
merge_objects(b, o, t, path, conflicts)
}
(Value::Array(o), Value::Array(t)) => {
let empty: Vec<Value> = Vec::new();
let b: &Vec<Value> = match base {
Value::Array(a) => a,
_ => &empty,
};
merge_arrays(b, o, t, path, conflicts)
}
_ => {
conflicts.push(MergeConflict {
path: path.to_string(),
base: base.clone(),
ours: ours.clone(),
theirs: theirs.clone(),
});
ours.clone()
}
}
}
fn merge_objects(
base: &Map<String, Value>,
ours: &Map<String, Value>,
theirs: &Map<String, Value>,
path: &str,
conflicts: &mut Vec<MergeConflict>,
) -> Value {
let mut out = Map::new();
let null = Value::Null;
let mut keys: Vec<&str> = Vec::new();
for k in ours.keys() {
keys.push(k.as_str());
}
for k in theirs.keys() {
if !ours.contains_key(k) {
keys.push(k.as_str());
}
}
for k in base.keys() {
if !ours.contains_key(k) && !theirs.contains_key(k) {
keys.push(k.as_str());
}
}
for k in keys {
let b = base.get(k).unwrap_or(&null);
let o = ours.get(k).unwrap_or(&null);
let t = theirs.get(k).unwrap_or(&null);
let child_path = if path.is_empty() {
k.to_string()
} else {
format!("{path}.{k}")
};
let ours_deleted = !ours.contains_key(k) && base.contains_key(k);
let theirs_deleted = !theirs.contains_key(k) && base.contains_key(k);
if ours_deleted && theirs_deleted {
continue; }
if ours_deleted && t == b {
continue; }
if theirs_deleted && o == b {
continue; }
if ours_deleted && t != b {
conflicts.push(MergeConflict {
path: child_path,
base: b.clone(),
ours: Value::Null,
theirs: t.clone(),
});
out.insert(k.to_string(), t.clone());
continue;
}
if theirs_deleted && o != b {
conflicts.push(MergeConflict {
path: child_path,
base: b.clone(),
ours: o.clone(),
theirs: Value::Null,
});
out.insert(k.to_string(), o.clone());
continue;
}
let merged = merge_at(b, o, t, &child_path, conflicts);
out.insert(k.to_string(), merged);
}
Value::Object(out)
}
fn merge_arrays(
base: &[Value],
ours: &[Value],
theirs: &[Value],
path: &str,
conflicts: &mut Vec<MergeConflict>,
) -> Value {
let ours_same_len = ours.len() == base.len();
let theirs_same_len = theirs.len() == base.len();
if ours_same_len && theirs_same_len {
let mut out = Vec::with_capacity(ours.len());
for i in 0..ours.len() {
let child = format!("{path}[{i}]");
out.push(merge_at(&base[i], &ours[i], &theirs[i], &child, conflicts));
}
return Value::Array(out);
}
if theirs_same_len && !ours_same_len {
return Value::Array(ours.to_vec());
}
if ours_same_len && !theirs_same_len {
return Value::Array(theirs.to_vec());
}
conflicts.push(MergeConflict {
path: path.to_string(),
base: Value::Array(base.to_vec()),
ours: Value::Array(ours.to_vec()),
theirs: Value::Array(theirs.to_vec()),
});
Value::Array(ours.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::json;
#[test]
fn identical_trivial() {
let v = json!({"a": 1});
let r = three_way_merge(&v, &v, &v);
assert!(r.is_clean());
assert_eq!(r.merged, v);
}
#[test]
fn one_sided_change_ours() {
let base = json!({"a": 1});
let ours = json!({"a": 2});
let theirs = json!({"a": 1});
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, ours);
}
#[test]
fn one_sided_change_theirs() {
let base = json!({"a": 1});
let ours = json!({"a": 1});
let theirs = json!({"a": 9});
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, theirs);
}
#[test]
fn disjoint_object_edits_merge_clean() {
let base = json!({"a": 1, "b": 2});
let ours = json!({"a": 10, "b": 2});
let theirs = json!({"a": 1, "b": 20});
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, json!({"a": 10, "b": 20}));
}
#[test]
fn conflicting_object_value() {
let base = json!({"a": 1});
let ours = json!({"a": 2});
let theirs = json!({"a": 3});
let r = three_way_merge(&base, &ours, &theirs);
assert_eq!(r.conflicts.len(), 1);
assert_eq!(r.conflicts[0].path, "a");
}
#[test]
fn nested_disjoint_edits() {
let base = json!({"user": json!({"name": "a", "age": 30})});
let ours = json!({"user": json!({"name": "b", "age": 30})});
let theirs = json!({"user": json!({"name": "a", "age": 31})});
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, json!({"user": json!({"name": "b", "age": 31})}));
}
#[test]
fn both_added_same_key_conflict() {
let base = json!({});
let ours = json!({"x": 1});
let theirs = json!({"x": 2});
let r = three_way_merge(&base, &ours, &theirs);
assert_eq!(r.conflicts.len(), 1);
}
#[test]
fn both_added_same_key_equal() {
let base = json!({});
let ours = json!({"x": 1});
let theirs = json!({"x": 1});
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
}
#[test]
fn array_elementwise_disjoint() {
let base = json!([1, 2, 3]);
let ours = json!([10, 2, 3]);
let theirs = json!([1, 2, 30]);
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, json!([10, 2, 30]));
}
#[test]
fn array_elementwise_conflict() {
let base = json!([1]);
let ours = json!([2]);
let theirs = json!([3]);
let r = three_way_merge(&base, &ours, &theirs);
assert_eq!(r.conflicts.len(), 1);
assert_eq!(r.conflicts[0].path, "[0]");
}
#[test]
fn array_one_sided_length_change() {
let base = json!([1, 2]);
let ours = json!([1, 2, 3]);
let theirs = json!([1, 2]);
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, ours);
}
#[test]
fn array_both_extended_conflict() {
let base = json!([1]);
let ours = json!([1, 2]);
let theirs = json!([1, 3]);
let r = three_way_merge(&base, &ours, &theirs);
assert_eq!(r.conflicts.len(), 1);
}
#[test]
fn deletion_both_sides_clean() {
let base = json!({"a": 1, "b": 2});
let ours = json!({"a": 1});
let theirs = json!({"a": 1});
let r = three_way_merge(&base, &ours, &theirs);
assert!(r.is_clean());
assert_eq!(r.merged, json!({"a": 1}));
}
#[test]
fn deletion_vs_modification_conflict() {
let base = json!({"a": 1, "b": 2});
let ours = json!({"a": 1});
let theirs = json!({"a": 1, "b": 99});
let r = three_way_merge(&base, &ours, &theirs);
assert_eq!(r.conflicts.len(), 1);
assert_eq!(r.conflicts[0].path, "b");
}
#[test]
fn type_mismatch_conflict() {
let base = json!(null);
let ours = json!({"a": 1});
let theirs = json!([1, 2]);
let r = three_way_merge(&base, &ours, &theirs);
assert_eq!(r.conflicts.len(), 1);
}
}