Skip to main content

knf/
merge.rs

1//! The layered merge over [`Value`].
2//!
3//! One walk, one value type. Every format parses into [`Value`] before merging,
4//! so JSON and TOML layers stack without a conversion in the middle; nothing in
5//! this module knows about files, formats or the command line.
6
7use crate::path::render_keys;
8use crate::{Map, Value};
9
10/// Knobs on the merge itself. Passed by reference rather than encoded as cargo
11/// features: features are additive and unify across a dependency graph, so a
12/// `strict` feature would silently change behaviour for one consumer the moment
13/// a second consumer enabled it.
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct MergeOptions {
16    /// Error when a layer changes the kind of an existing key.
17    pub strict: bool,
18    /// Replace top-level keys wholesale instead of recursing into them — jq's
19    /// `a + b` rather than `a * b`.
20    pub shallow: bool,
21}
22
23impl MergeOptions {
24    /// The default: deep merge, last layer wins, no type checking.
25    pub const LAST_WINS: Self = Self {
26        strict: false,
27        shallow: false,
28    };
29    /// Error when a layer changes the kind of an existing key.
30    pub const STRICT: Self = Self {
31        strict: true,
32        shallow: false,
33    };
34    /// Top-level keys only: a later layer's value replaces the earlier one whole.
35    pub const SHALLOW: Self = Self {
36        strict: false,
37        shallow: true,
38    };
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum MergeError {
43    /// A layer replaced an existing key with a value of a different kind.
44    ///
45    /// Carries a key path and nothing else — no filenames, no layer indices.
46    #[error(
47        "type conflict at `{}`: {expected} would be replaced by {found}",
48        render_keys(path)
49    )]
50    TypeConflict {
51        path: Vec<String>,
52        expected: &'static str,
53        found: &'static str,
54    },
55}
56
57impl MergeError {
58    /// The dotted key path the conflict occurred at.
59    pub fn path(&self) -> &[String] {
60        match self {
61            Self::TypeConflict { path, .. } => path,
62        }
63    }
64}
65
66/// Merges `over` into `base` in place.
67///
68/// Objects recurse per key. Arrays, scalars, datetimes and null all replace
69/// wholesale — notably arrays are never index-merged or concatenated, and null
70/// is an ordinary value that overwrites rather than a delete instruction. This
71/// is jq's `a * b`.
72///
73/// Under [`MergeOptions::shallow`] only the top level is merged key by key;
74/// every colliding value is replaced whole, objects included — jq's `a + b`.
75pub fn merge_into(base: &mut Value, over: Value, opts: &MergeOptions) -> Result<(), MergeError> {
76    let mut path = Vec::new();
77    merge_at(base, over, opts, &mut path)
78}
79
80/// Folds a list of layers into one document, seeded with an empty object.
81///
82/// The fold must be strictly left over the *flat* layer list. The deep merge is
83/// not associative — any scalar shadowing an object breaks it:
84///
85/// ```text
86/// {a:{b:1}} * {a:5} * {a:{c:2}}
87///   left-assoc  -> {a:{c:2}}
88///   right-assoc -> {a:{b:1,c:2}}
89/// ```
90///
91/// So callers must never merge subgroups and then combine the results.
92/// Flatten first, fold second. (The shallow merge happens to be associative,
93/// but the fold does not rely on it.)
94pub fn merge(
95    layers: impl IntoIterator<Item = Value>,
96    opts: &MergeOptions,
97) -> Result<Value, MergeError> {
98    let mut acc = Value::Object(Map::new());
99    let mut path = Vec::new();
100    for layer in layers {
101        merge_at(&mut acc, layer, opts, &mut path)?;
102        debug_assert!(path.is_empty(), "breadcrumb leaked between layers");
103    }
104    Ok(acc)
105}
106
107/// The recursive worker. `path` is a breadcrumb threaded by push/pop so that a
108/// conflict can report where it happened without every frame allocating.
109///
110/// Shallow mode needs no depth counter: it replaces at the first collision
111/// below the root, so the walk never gets deeper than one level.
112fn merge_at(
113    base: &mut Value,
114    over: Value,
115    opts: &MergeOptions,
116    path: &mut Vec<String>,
117) -> Result<(), MergeError> {
118    match (base, over) {
119        (Value::Object(base_map), Value::Object(over_map)) => {
120            for (k, v) in over_map {
121                if let Some(slot) = base_map.get_mut(&k) {
122                    path.push(k);
123                    if opts.shallow {
124                        replace(slot, v, opts, path)?;
125                    } else {
126                        merge_at(slot, v, opts, path)?;
127                    }
128                    path.pop();
129                } else {
130                    base_map.insert(k, v);
131                }
132            }
133            Ok(())
134        }
135        (base, over) => replace(base, over, opts, path),
136    }
137}
138
139fn replace(
140    base: &mut Value,
141    over: Value,
142    opts: &MergeOptions,
143    path: &[String],
144) -> Result<(), MergeError> {
145    if opts.strict {
146        check_kind(base.kind(), over.kind(), path)?;
147    }
148    *base = over;
149    Ok(())
150}
151
152/// Errors if a replacement would change the kind of the existing value.
153///
154/// Strict mode catches the class of mistake where a leaf accidentally shadows a
155/// subtree. Pleasant side effect: it rejects exactly the type changes that break
156/// associativity, so under strict mode the merge *is* associative.
157fn check_kind(
158    expected: &'static str,
159    found: &'static str,
160    path: &[String],
161) -> Result<(), MergeError> {
162    if expected == found {
163        return Ok(());
164    }
165    Err(MergeError::TypeConflict {
166        path: path.to_vec(),
167        expected,
168        found,
169    })
170}