use crate::path::render_keys;
use crate::{Map, Value};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MergeOptions {
pub strict: bool,
pub shallow: bool,
}
impl MergeOptions {
pub const LAST_WINS: Self = Self {
strict: false,
shallow: false,
};
pub const STRICT: Self = Self {
strict: true,
shallow: false,
};
pub const SHALLOW: Self = Self {
strict: false,
shallow: true,
};
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MergeError {
#[error(
"type conflict at `{}`: {expected} would be replaced by {found}",
render_keys(path)
)]
TypeConflict {
path: Vec<String>,
expected: &'static str,
found: &'static str,
},
}
impl MergeError {
pub fn path(&self) -> &[String] {
match self {
Self::TypeConflict { path, .. } => path,
}
}
}
pub fn merge_into(base: &mut Value, over: Value, opts: &MergeOptions) -> Result<(), MergeError> {
let mut path = Vec::new();
merge_at(base, over, opts, &mut path)
}
pub fn merge(
layers: impl IntoIterator<Item = Value>,
opts: &MergeOptions,
) -> Result<Value, MergeError> {
let mut acc = Value::Object(Map::new());
let mut path = Vec::new();
for layer in layers {
merge_at(&mut acc, layer, opts, &mut path)?;
debug_assert!(path.is_empty(), "breadcrumb leaked between layers");
}
Ok(acc)
}
fn merge_at(
base: &mut Value,
over: Value,
opts: &MergeOptions,
path: &mut Vec<String>,
) -> Result<(), MergeError> {
match (base, over) {
(Value::Object(base_map), Value::Object(over_map)) => {
for (k, v) in over_map {
if let Some(slot) = base_map.get_mut(&k) {
path.push(k);
if opts.shallow {
replace(slot, v, opts, path)?;
} else {
merge_at(slot, v, opts, path)?;
}
path.pop();
} else {
base_map.insert(k, v);
}
}
Ok(())
}
(base, over) => replace(base, over, opts, path),
}
}
fn replace(
base: &mut Value,
over: Value,
opts: &MergeOptions,
path: &[String],
) -> Result<(), MergeError> {
if opts.strict {
check_kind(base.kind(), over.kind(), path)?;
}
*base = over;
Ok(())
}
fn check_kind(
expected: &'static str,
found: &'static str,
path: &[String],
) -> Result<(), MergeError> {
if expected == found {
return Ok(());
}
Err(MergeError::TypeConflict {
path: path.to_vec(),
expected,
found,
})
}