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