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}