use crate::glob::KeyGlobPattern;
use crate::path::render_keys;
use crate::{ConfigObject, ConfigValue};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MergeOptions {
pub strict: bool,
pub shallow: Option<KeyGlobPattern>,
}
impl MergeOptions {
pub const LAST_WINS: Self = Self {
strict: false,
shallow: None,
};
pub const STRICT: Self = Self {
strict: true,
shallow: None,
};
pub fn shallow_root() -> Self {
Self {
strict: false,
shallow: Some("*".parse().expect("valid root selector")),
}
}
}
#[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<V: ConfigValue>(
base: &mut V,
over: V,
opts: &MergeOptions,
) -> Result<(), MergeError> {
let mut path = Vec::new();
merge_at(base, over, opts, &mut path)
}
pub fn merge<V: ConfigValue>(
layers: impl IntoIterator<Item = V>,
opts: &MergeOptions,
) -> Result<V, MergeError> {
let mut acc = V::object(V::Object::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<V: ConfigValue>(
base: &mut V,
over: V,
opts: &MergeOptions,
path: &mut Vec<String>,
) -> Result<(), MergeError> {
if let Some(base_map) = base.as_object_mut() {
match over.into_object() {
Ok(over_map) => {
for (k, v) in over_map {
if let Some(slot) = base_map.get_mut(&k) {
path.push(k);
if opts
.shallow
.as_ref()
.is_some_and(|glob| glob.matches_keys(path))
{
replace(slot, v, opts, path)?;
} else {
merge_at(slot, v, opts, path)?;
}
path.pop();
} else {
base_map.insert(k, v);
}
}
return Ok(());
}
Err(over) => return replace(base, over, opts, path),
}
}
replace(base, over, opts, path)
}
fn replace<V: ConfigValue>(
base: &mut V,
over: V,
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,
})
}