Skip to main content

knf/
merge.rs

1//! Generic layered merge over native configuration values.
2
3use crate::glob::KeyGlobPattern;
4use crate::path::render_keys;
5use crate::{ConfigObject, ConfigValue};
6
7/// Merge options.
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct MergeOptions {
10    /// Error when a layer changes the kind of an existing key.
11    pub strict: bool,
12    /// Key paths to replace wholesale: `*` (top level), `foo`, `foo.*`.
13    /// `None` is a deep merge.
14    pub shallow: Option<KeyGlobPattern>,
15}
16
17impl MergeOptions {
18    /// The default: deep merge, last layer wins, no type checking.
19    pub const LAST_WINS: Self = Self {
20        strict: false,
21        shallow: None,
22    };
23    /// Error when a layer changes the kind of an existing key.
24    pub const STRICT: Self = Self {
25        strict: true,
26        shallow: None,
27    };
28
29    /// Top-level keys only: a later layer's value replaces the earlier one whole.
30    pub fn shallow_root() -> Self {
31        Self {
32            strict: false,
33            shallow: Some("*".parse().expect("valid root selector")),
34        }
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
39pub enum MergeError {
40    /// A layer replaced an existing key with a value of a different kind.
41    #[error(
42        "type conflict at `{}`: {expected} would be replaced by {found}",
43        render_keys(path)
44    )]
45    TypeConflict {
46        path: Vec<String>,
47        expected: &'static str,
48        found: &'static str,
49    },
50}
51
52impl MergeError {
53    /// The dotted key path the conflict occurred at.
54    pub fn path(&self) -> &[String] {
55        match self {
56            Self::TypeConflict { path, .. } => path,
57        }
58    }
59}
60
61/// Merges `over` into `base` in place (jq's `a * b`).
62///
63/// Objects recurse per key; everything else, including arrays and null,
64/// replaces. Paths matched by [`MergeOptions::shallow`] replace wholesale.
65pub fn merge_into<V: ConfigValue>(
66    base: &mut V,
67    over: V,
68    opts: &MergeOptions,
69) -> Result<(), MergeError> {
70    let mut path = Vec::new();
71    merge_at(base, over, opts, &mut path)
72}
73
74/// Left-folds layers into one document, starting from an empty object.
75///
76/// The merge is not associative, so never merge subgroups and combine them:
77///
78/// ```text
79/// {a:{b:1}} * {a:5} * {a:{c:2}}
80///   left-assoc  -> {a:{c:2}}
81///   right-assoc -> {a:{b:1,c:2}}
82/// ```
83pub fn merge<V: ConfigValue>(
84    layers: impl IntoIterator<Item = V>,
85    opts: &MergeOptions,
86) -> Result<V, MergeError> {
87    let mut acc = V::object(V::Object::new());
88    let mut path = Vec::new();
89    for layer in layers {
90        merge_at(&mut acc, layer, opts, &mut path)?;
91        debug_assert!(path.is_empty(), "breadcrumb leaked between layers");
92    }
93    Ok(acc)
94}
95
96/// The recursive worker. `path` is the current key path, for errors and
97/// selector matching.
98fn merge_at<V: ConfigValue>(
99    base: &mut V,
100    over: V,
101    opts: &MergeOptions,
102    path: &mut Vec<String>,
103) -> Result<(), MergeError> {
104    if let Some(base_map) = base.as_object_mut() {
105        match over.into_object() {
106            Ok(over_map) => {
107                for (k, v) in over_map {
108                    if let Some(slot) = base_map.get_mut(&k) {
109                        path.push(k);
110                        if opts
111                            .shallow
112                            .as_ref()
113                            .is_some_and(|glob| glob.matches_keys(path))
114                        {
115                            replace(slot, v, opts, path)?;
116                        } else {
117                            merge_at(slot, v, opts, path)?;
118                        }
119                        path.pop();
120                    } else {
121                        base_map.insert(k, v);
122                    }
123                }
124                return Ok(());
125            }
126            Err(over) => return replace(base, over, opts, path),
127        }
128    }
129    replace(base, over, opts, path)
130}
131
132fn replace<V: ConfigValue>(
133    base: &mut V,
134    over: V,
135    opts: &MergeOptions,
136    path: &[String],
137) -> Result<(), MergeError> {
138    if opts.strict {
139        check_kind(base.kind(), over.kind(), path)?;
140    }
141    *base = over;
142    Ok(())
143}
144
145/// Errors if a replacement would change the kind of the existing value.
146fn check_kind(
147    expected: &'static str,
148    found: &'static str,
149    path: &[String],
150) -> Result<(), MergeError> {
151    if expected == found {
152        return Ok(());
153    }
154    Err(MergeError::TypeConflict {
155        path: path.to_vec(),
156        expected,
157        found,
158    })
159}