use std::collections::BTreeMap;
use serde_json::Value;
use crate::merge::conflict::{Conflict, MergeResult};
use crate::merge::sdl::IdentityRule;
pub fn merge_ordered_unique(
path: &str,
base: &Value,
current: &Value,
proposed: &Value,
identity: &IdentityRule,
) -> MergeResult {
let base_arr = as_array(base);
let current_arr = as_array(current);
let proposed_arr = as_array(proposed);
let base_by_id = index_by_identity(base_arr, identity);
let current_by_id = index_by_identity(current_arr, identity);
let proposed_by_id = index_by_identity(proposed_arr, identity);
let mut conflicts = Vec::new();
for (id, cur_val) in ¤t_by_id {
if let Some(pro_val) = proposed_by_id.get(id)
&& cur_val != pro_val
{
let base_val = base_by_id.get(id);
let cur_matches_base = base_val == Some(cur_val);
let pro_matches_base = base_val == Some(pro_val);
if !cur_matches_base && !pro_matches_base {
let sub_path = format!("{path}[{id}]");
conflicts.push(Conflict::value_mismatch(
sub_path,
base_val.cloned().unwrap_or(Value::Null),
cur_val.clone(),
pro_val.clone(),
));
}
}
}
if !conflicts.is_empty() {
return MergeResult::Conflicts(conflicts);
}
let mut seen: BTreeMap<String, bool> = BTreeMap::new();
let mut result: Vec<Value> = Vec::new();
let mut add_item = |val: &Value, id: &str| {
if !seen.contains_key(id) {
seen.insert(id.to_string(), true);
result.push(val.clone());
}
};
for val in base_arr {
let id = extract_identity(val, identity);
add_item(val, &id);
}
for val in current_arr {
let id = extract_identity(val, identity);
if !base_by_id.contains_key(&id) {
add_item(val, &id);
}
}
for val in proposed_arr {
let id = extract_identity(val, identity);
if !base_by_id.contains_key(&id) && !current_by_id.contains_key(&id) {
add_item(val, &id);
}
}
apply_modifications(
&mut result,
base_arr,
¤t_by_id,
&proposed_by_id,
identity,
);
MergeResult::Merged(Value::Array(result))
}
fn apply_modifications(
result: &mut [Value],
base_arr: &[Value],
current_by_id: &BTreeMap<String, Value>,
proposed_by_id: &BTreeMap<String, Value>,
identity: &IdentityRule,
) {
for (idx, val) in base_arr.iter().enumerate() {
let id = extract_identity(val, identity);
if let Some(cur_val) = current_by_id.get(&id)
&& cur_val != val
{
result[idx] = cur_val.clone();
}
if let Some(pro_val) = proposed_by_id.get(&id)
&& pro_val != &result[idx]
&& !current_by_id.contains_key(&id)
{
result[idx] = pro_val.clone();
}
}
}
fn extract_identity(val: &Value, identity: &IdentityRule) -> String {
match identity {
IdentityRule::PrimitiveValue => match val {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => {
serde_json::to_string(val).unwrap_or_else(|_| "unknown".to_string())
}
},
IdentityRule::Key { key } => val
.get(key)
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
_ => None,
})
.unwrap_or_else(|| {
"unknown".to_string()
}),
}
}
fn index_by_identity(arr: &[Value], identity: &IdentityRule) -> BTreeMap<String, Value> {
let mut map = BTreeMap::new();
for val in arr {
let id = extract_identity(val, identity);
map.insert(id, val.clone());
}
map
}
fn as_array(val: &Value) -> &[Value] {
match val {
Value::Array(arr) => arr.as_slice(),
_ => &[],
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn primitive_identity() -> IdentityRule {
IdentityRule::PrimitiveValue
}
fn key_identity() -> IdentityRule {
IdentityRule::Key {
key: "id".to_string(),
}
}
#[test]
fn test_ordered_unique_base_example() {
let base = json!([{"id": "A"}]);
let current = json!([{"id": "A"}, {"id": "B"}]);
let proposed = json!([{"id": "A"}, {"id": "C"}]);
let result = merge_ordered_unique("items", &base, ¤t, &proposed, &key_identity());
assert!(result.is_merged());
let merged = result.unwrap_merged();
let arr = merged.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].get("id"), Some(&json!("A")));
assert_eq!(arr[1].get("id"), Some(&json!("B")));
assert_eq!(arr[2].get("id"), Some(&json!("C")));
}
#[test]
fn test_ordered_unique_primitive() {
let base = json!(["a", "b"]);
let current = json!(["a", "b", "c"]);
let proposed = json!(["a", "b", "d"]);
let result =
merge_ordered_unique("tags", &base, ¤t, &proposed, &primitive_identity());
assert!(result.is_merged());
let merged = result.unwrap_merged();
let arr = merged.as_array().unwrap();
assert_eq!(arr, &[json!("a"), json!("b"), json!("c"), json!("d")]);
}
#[test]
fn test_ordered_unique_deduplicates() {
let base = json!(["a"]);
let current = json!(["a", "b", "b"]); let proposed = json!(["a", "c"]);
let result =
merge_ordered_unique("tags", &base, ¤t, &proposed, &primitive_identity());
assert!(result.is_merged());
let merged = result.unwrap_merged();
let arr = merged.as_array().unwrap();
assert_eq!(arr, &[json!("a"), json!("b"), json!("c")]);
}
#[test]
fn test_ordered_unique_conflict_same_id_different_values() {
let base = json!([{"id": "X", "val": 1}]);
let current = json!([{"id": "X", "val": 2}]);
let proposed = json!([{"id": "X", "val": 3}]);
let result = merge_ordered_unique("items", &base, ¤t, &proposed, &key_identity());
assert!(result.is_conflict());
}
#[test]
fn test_ordered_unique_no_conflict_one_side_unchanged() {
let base = json!([{"id": "X", "val": 1}]);
let current = json!([{"id": "X", "val": 2}]);
let proposed = json!([{"id": "X", "val": 1}]);
let result = merge_ordered_unique("items", &base, ¤t, &proposed, &key_identity());
assert!(result.is_merged());
let merged = result.unwrap_merged();
assert_eq!(merged[0].get("val"), Some(&json!(2)));
}
#[test]
fn test_ordered_unique_both_add_same_no_conflict() {
let base = json!([{"id": "A"}]);
let current = json!([{"id": "A"}, {"id": "B"}]);
let proposed = json!([{"id": "A"}, {"id": "B"}]);
let result = merge_ordered_unique("items", &base, ¤t, &proposed, &key_identity());
assert!(result.is_merged());
let merged = result.unwrap_merged();
assert_eq!(merged.as_array().unwrap().len(), 2);
}
#[test]
fn test_ordered_unique_preserves_base_order() {
let base = json!([{"id": "A"}, {"id": "B"}]);
let current = json!([{"id": "A"}, {"id": "B"}, {"id": "C"}]);
let proposed = json!([{"id": "B"}, {"id": "A"}, {"id": "D"}]);
let result = merge_ordered_unique("items", &base, ¤t, &proposed, &key_identity());
assert!(result.is_merged());
let merged = result.unwrap_merged();
let arr = merged.as_array().unwrap();
assert_eq!(arr.len(), 4);
assert_eq!(arr[0].get("id"), Some(&json!("A")));
assert_eq!(arr[1].get("id"), Some(&json!("B")));
}
#[test]
fn test_ordered_unique_empty_arrays() {
let base = json!([]);
let current = json!([]);
let proposed = json!([]);
let result = merge_ordered_unique("items", &base, ¤t, &proposed, &key_identity());
assert!(result.is_merged());
assert_eq!(result.unwrap_merged(), json!([]));
}
#[test]
fn test_ordered_unique_current_adds_proposed_empty() {
let base = json!(["a"]);
let current = json!(["a", "b"]);
let proposed = json!(["a"]);
let result =
merge_ordered_unique("tags", &base, ¤t, &proposed, &primitive_identity());
assert!(result.is_merged());
assert_eq!(result.unwrap_merged(), json!(["a", "b"]));
}
}