use std::collections::BTreeMap;
use crate::merge::{ConflictKind, MergeContext, MergeWithContext};
pub(crate) struct PathGuard<'a> {
path: &'a mut String,
original_len: usize,
}
impl<'a> PathGuard<'a> {
pub(crate) fn new(path: &'a mut String, segment: &str) -> Self {
let original_len = path.len();
path.push_str(segment);
PathGuard { path, original_len }
}
pub(crate) fn path_mut(&mut self) -> &mut String {
self.path
}
}
impl Drop for PathGuard<'_> {
fn drop(&mut self) {
self.path.truncate(self.original_len);
}
}
#[inline]
pub(crate) fn with_segment<R>(
path: &mut String,
segment: &str,
f: impl FnOnce(&mut String) -> R,
) -> R {
let mut guard = PathGuard::new(path, segment);
f(guard.path_mut())
}
pub(crate) fn merge_map<K, V>(
base: &mut BTreeMap<K, V>,
other: BTreeMap<K, V>,
ctx: &mut MergeContext,
path: &mut String,
mut recurse: impl FnMut(&mut V, V, &mut MergeContext, &mut String),
fmt_key: impl Fn(&K, &mut String),
) where
K: Ord,
{
if ctx.errored {
return;
}
for (k, incoming) in other {
if let Some(base_v) = base.get_mut(&k) {
let mut guard = PathGuard::new(path, ".");
fmt_key(&k, guard.path_mut());
recurse(base_v, incoming, ctx, guard.path_mut());
drop(guard);
if ctx.errored {
return;
}
} else {
base.insert(k, incoming);
}
}
}
pub(crate) fn merge_opt_map<K, V>(
base: &mut Option<BTreeMap<K, V>>,
other: Option<BTreeMap<K, V>>,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
recurse: impl FnMut(&mut V, V, &mut MergeContext, &mut String),
fmt_key: impl Fn(&K, &mut String),
) where
K: Ord,
{
if ctx.errored {
return;
}
let Some(other) = other else { return };
match base {
Some(base_map) => {
let mut guard = PathGuard::new(path, segment);
merge_map(base_map, other, ctx, guard.path_mut(), recurse, fmt_key);
}
None => *base = Some(other),
}
}
pub(crate) fn merge_vec_by_key<V, K>(
base: &mut Vec<V>,
other: Vec<V>,
ctx: &mut MergeContext,
path: &mut String,
key_of: impl Fn(&V) -> K,
mut recurse: impl FnMut(&mut V, V, &mut MergeContext, &mut String),
fmt_key: impl Fn(&K, &mut String),
) where
K: Ord + Clone,
{
if ctx.errored {
return;
}
use std::collections::BTreeMap as Map;
let mut index: Map<K, usize> = Map::new();
for (i, v) in base.iter().enumerate() {
index.insert(key_of(v), i);
}
for incoming in other {
let k = key_of(&incoming);
if let Some(&i) = index.get(&k) {
let mut guard = PathGuard::new(path, "[");
fmt_key(&k, guard.path_mut());
guard.path_mut().push(']');
recurse(&mut base[i], incoming, ctx, guard.path_mut());
drop(guard);
if ctx.errored {
return;
}
} else {
index.insert(k, base.len());
base.push(incoming);
}
}
}
#[allow(clippy::too_many_arguments)] pub(crate) fn merge_opt_vec_by_key<V, K>(
base: &mut Option<Vec<V>>,
other: Option<Vec<V>>,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
key_of: impl Fn(&V) -> K,
recurse: impl FnMut(&mut V, V, &mut MergeContext, &mut String),
fmt_key: impl Fn(&K, &mut String),
) where
K: Ord + Clone,
{
if ctx.errored {
return;
}
let Some(other) = other else { return };
match base {
Some(base_v) => {
let mut guard = PathGuard::new(path, segment);
merge_vec_by_key(
base_v,
other,
ctx,
guard.path_mut(),
key_of,
recurse,
fmt_key,
);
}
None => *base = Some(other),
}
}
pub(crate) fn merge_opt_vec_set_union<V>(
base: &mut Option<Vec<V>>,
other: Option<Vec<V>>,
ctx: &mut MergeContext,
_path: &mut String,
_segment: &str,
) where
V: Eq,
{
if ctx.errored {
return;
}
let Some(other) = other else { return };
match base {
Some(base_v) => {
for v in other {
if !base_v.contains(&v) {
base_v.push(v);
}
}
}
None => *base = Some(other),
}
}
pub(crate) fn merge_opt_scalar<V>(
base: &mut Option<V>,
other: Option<V>,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
kind: ConflictKind,
) where
V: PartialEq,
{
if ctx.errored {
return;
}
match (base.as_mut(), other) {
(_, None) => {}
(None, Some(v)) => *base = Some(v),
(Some(a), Some(b)) => {
if *a != b {
let take = with_segment(path, segment, |p| ctx.should_take_incoming(p, kind));
if take {
*a = b;
}
}
}
}
}
pub(crate) fn merge_required_scalar<V>(
base: &mut V,
other: V,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
kind: ConflictKind,
) where
V: PartialEq,
{
if ctx.errored {
return;
}
if *base != other {
let take = with_segment(path, segment, |p| ctx.should_take_incoming(p, kind));
if take {
*base = other;
}
}
}
pub(crate) fn merge_replace_list_when_nonempty<V>(
base: &mut Option<Vec<V>>,
other: Option<Vec<V>>,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
) {
if ctx.errored {
return;
}
use crate::merge::MergeOptions;
let replace_when_empty = ctx.is_option(MergeOptions::ReplaceListsWhenEmpty);
let Some(other) = other else { return };
if other.is_empty() && !replace_when_empty {
return;
}
match base {
Some(b) if !b.is_empty() => {
let take = with_segment(path, segment, |p| {
ctx.should_take_incoming(p, ConflictKind::ListReplaced)
});
if take {
*b = other;
}
}
_ => *base = Some(other),
}
}
pub(crate) fn merge_extensions(
base: &mut Option<BTreeMap<String, serde_json::Value>>,
other: Option<BTreeMap<String, serde_json::Value>>,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
) {
if ctx.errored {
return;
}
let Some(other) = other else { return };
let base_map = base.get_or_insert_with(BTreeMap::new);
let mut guard = PathGuard::new(path, segment);
for (k, incoming) in other {
match base_map.get_mut(&k) {
None => {
base_map.insert(k, incoming);
}
Some(existing) => {
if *existing != incoming {
let take = with_segment(guard.path_mut(), ".", |p| {
p.push_str(&k);
ctx.should_take_incoming(p, ConflictKind::ScalarOverridden)
});
if take {
*existing = incoming;
}
}
}
}
if ctx.errored {
return;
}
}
}
pub(crate) fn merge_opt_struct<S>(
base: &mut Option<S>,
other: Option<S>,
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
) where
S: MergeWithContext,
{
if ctx.errored {
return;
}
match (base.as_mut(), other) {
(_, None) => {}
(None, Some(v)) => *base = Some(v),
(Some(a), Some(b)) => {
let mut guard = PathGuard::new(path, segment);
a.merge_with_context(b, ctx, guard.path_mut());
}
}
}
pub(crate) fn record_kept_base_or_error(
ctx: &mut MergeContext,
path: &mut String,
segment: &str,
kind: ConflictKind,
) {
use crate::merge::{MergeOptions, Resolution};
let original_len = path.len();
path.push_str(segment);
if ctx.is_option(MergeOptions::ErrorOnConflict) {
ctx.record(path.clone(), kind, Resolution::Errored);
ctx.errored = true;
} else {
ctx.record(path.clone(), kind, Resolution::Base);
}
path.truncate(original_len);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merge::{ConflictKind, MergeOptions};
fn root_path() -> String {
"#".to_owned()
}
#[test]
fn merge_opt_scalar_none_incoming_no_op() {
let mut base: Option<String> = Some("a".into());
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_opt_scalar(
&mut base,
None,
&mut ctx,
&mut path,
".x",
ConflictKind::ScalarOverridden,
);
assert_eq!(base.as_deref(), Some("a"));
assert!(ctx.conflicts.is_empty());
assert_eq!(path, "#", "path stack must be balanced");
}
#[test]
fn merge_opt_scalar_none_base_takes_incoming() {
let mut base: Option<String> = None;
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_opt_scalar(
&mut base,
Some("b".into()),
&mut ctx,
&mut path,
".x",
ConflictKind::ScalarOverridden,
);
assert_eq!(base.as_deref(), Some("b"));
assert!(ctx.conflicts.is_empty());
}
#[test]
fn merge_opt_scalar_same_value_no_conflict() {
let mut base: Option<String> = Some("a".into());
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_opt_scalar(
&mut base,
Some("a".into()),
&mut ctx,
&mut path,
".x",
ConflictKind::ScalarOverridden,
);
assert_eq!(base.as_deref(), Some("a"));
assert!(ctx.conflicts.is_empty());
}
#[test]
fn merge_opt_scalar_differing_takes_incoming_by_default() {
let mut base: Option<String> = Some("a".into());
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_opt_scalar(
&mut base,
Some("b".into()),
&mut ctx,
&mut path,
".x",
ConflictKind::ScalarOverridden,
);
assert_eq!(base.as_deref(), Some("b"));
assert_eq!(ctx.conflicts.len(), 1);
assert_eq!(ctx.conflicts[0].path, "#.x", "conflict path materialised");
assert_eq!(path, "#", "path stack balanced after conflict");
}
#[test]
fn merge_opt_vec_set_union_dedupes_and_preserves_base_order() {
let mut base: Option<Vec<i32>> = Some(vec![1, 2]);
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_opt_vec_set_union(&mut base, Some(vec![2, 3]), &mut ctx, &mut path, ".x");
assert_eq!(base.unwrap(), vec![1, 2, 3]);
assert!(ctx.conflicts.is_empty());
}
#[test]
fn merge_extensions_new_key_inserts() {
let mut base: Option<BTreeMap<String, serde_json::Value>> = Some({
let mut m = BTreeMap::new();
m.insert("x-a".into(), serde_json::json!(1));
m
});
let mut other = BTreeMap::new();
other.insert("x-b".into(), serde_json::json!(2));
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_extensions(&mut base, Some(other), &mut ctx, &mut path, ".ext");
let b = base.unwrap();
assert_eq!(b.get("x-a"), Some(&serde_json::json!(1)));
assert_eq!(b.get("x-b"), Some(&serde_json::json!(2)));
assert_eq!(path, "#", "path balanced");
}
#[test]
fn merge_replace_list_keeps_populated_base_when_incoming_empty() {
let mut base: Option<Vec<i32>> = Some(vec![1, 2]);
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_replace_list_when_nonempty(&mut base, Some(vec![]), &mut ctx, &mut path, ".servers");
assert_eq!(base, Some(vec![1, 2]));
assert!(ctx.conflicts.is_empty());
}
#[test]
fn merge_replace_list_replaces_when_both_non_empty() {
let mut base: Option<Vec<i32>> = Some(vec![1, 2]);
let mut ctx: MergeContext = MergeContext::new(MergeOptions::new());
let mut path = root_path();
merge_replace_list_when_nonempty(
&mut base,
Some(vec![9, 10]),
&mut ctx,
&mut path,
".servers",
);
assert_eq!(base, Some(vec![9, 10]));
assert_eq!(ctx.conflicts.len(), 1);
assert_eq!(ctx.conflicts[0].kind, ConflictKind::ListReplaced);
assert_eq!(ctx.conflicts[0].path, "#.servers");
}
#[test]
fn path_guard_restores_path_on_drop() {
let mut path = "#.foo".to_owned();
{
let mut guard = PathGuard::new(&mut path, ".bar");
assert_eq!(guard.path_mut(), "#.foo.bar");
}
assert_eq!(path, "#.foo");
}
#[test]
fn path_guard_restores_path_on_drop_with_nested_guards() {
let mut path = "#".to_owned();
{
let mut g1 = PathGuard::new(&mut path, ".a");
{
let mut g2 = PathGuard::new(g1.path_mut(), ".b");
assert_eq!(g2.path_mut(), "#.a.b");
}
assert_eq!(g1.path_mut(), "#.a");
}
assert_eq!(path, "#");
}
}