mod rules;
mod strict;
mod value;
pub use rules::{RuleError, RuleErrors, Rules, Strategy};
pub use value::{Map, Number, Value};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MergeOptions {
pub strict: bool,
pub rules: Rules,
}
impl MergeOptions {
pub const LAST_WINS: Self = Self {
strict: false,
rules: Rules::EMPTY,
};
pub const STRICT: Self = Self {
strict: true,
rules: Rules::EMPTY,
};
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MergeError {
#[error(
"type conflict at `{}`: {expected} would be replaced by {found}",
render_path(path)
)]
TypeConflict {
path: Vec<String>,
expected: &'static str,
found: &'static str,
},
#[error("`{}` is locked: an earlier layer already set it", render_path(path))]
Locked { path: Vec<String> },
#[error("cannot append {found} to {base} at `{}`", render_path(path))]
AppendKind {
path: Vec<String>,
base: &'static str,
found: &'static str,
},
}
impl MergeError {
pub fn path(&self) -> &[String] {
match self {
Self::TypeConflict { path, .. }
| Self::Locked { path }
| Self::AppendKind { path, .. } => path,
}
}
}
fn render_path(path: &[String]) -> String {
if path.is_empty() {
"<root>".to_string()
} else {
path.join(".")
}
}
pub fn merge_into(base: &mut Value, over: Value, opts: &MergeOptions) -> Result<(), MergeError> {
let mut path = Vec::new();
apply(base, over, opts, &mut path, Some(&opts.rules))
}
pub fn merge(layers: impl IntoIterator<Item = Value>) -> Result<Value, MergeError> {
merge_with(layers, &MergeOptions::LAST_WINS)
}
pub fn merge_with(
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 {
apply(&mut acc, layer, opts, &mut path, Some(&opts.rules))?;
debug_assert!(path.is_empty(), "breadcrumb leaked between layers");
}
Ok(acc)
}
fn apply(
base: &mut Value,
over: Value,
opts: &MergeOptions,
path: &mut Vec<String>,
rules: Option<&Rules>,
) -> Result<(), MergeError> {
match rules.and_then(Rules::strategy) {
None => merge_at(base, over, opts, path, rules),
Some(Strategy::Replace) => replace(base, over, opts, path),
Some(Strategy::Append) => append(base, over, path),
Some(Strategy::Fail) => Err(MergeError::Locked { path: path.clone() }),
}
}
fn merge_at(
base: &mut Value,
over: Value,
opts: &MergeOptions,
path: &mut Vec<String>,
rules: Option<&Rules>,
) -> Result<(), MergeError> {
match (base, over) {
(Value::Object(base_map), Value::Object(over_map)) => {
for (k, v) in over_map {
let child = rules.and_then(|r| r.child(&k));
if let Some(slot) = base_map.get_mut(&k) {
path.push(k);
apply(slot, v, opts, path, child)?;
path.pop();
} else {
base_map.insert(k, v);
}
}
Ok(())
}
(base, over) => {
if let Some(rules) = rules
&& let Some(locked) = locked_path(base, rules)
{
let mut path = path.clone();
path.extend(locked);
return Err(MergeError::Locked { path });
}
replace(base, over, opts, path)
}
}
}
fn locked_path(base: &Value, rules: &Rules) -> Option<Vec<String>> {
if rules.strategy() == Some(Strategy::Fail) {
return Some(Vec::new());
}
let Value::Object(map) = base else {
return None;
};
rules.children().find_map(|(key, child)| {
let mut rest = locked_path(map.get(key)?, child)?;
rest.insert(0, key.to_string());
Some(rest)
})
}
fn replace(
base: &mut Value,
over: Value,
opts: &MergeOptions,
path: &[String],
) -> Result<(), MergeError> {
if opts.strict {
strict::check(base.kind(), over.kind(), path)?;
}
*base = over;
Ok(())
}
fn append(base: &mut Value, over: Value, path: &[String]) -> Result<(), MergeError> {
match (base, over) {
(Value::Array(base_items), Value::Array(over_items)) => {
base_items.extend(over_items);
Ok(())
}
(base, over) => Err(MergeError::AppendKind {
path: path.to_vec(),
base: base.kind(),
found: over.kind(),
}),
}
}