smart_config/schema/
mod.rs

1//! Configuration schema.
2
3use std::{
4    any,
5    borrow::Cow,
6    collections::{BTreeMap, BTreeSet, HashMap},
7    iter,
8};
9
10use anyhow::Context;
11
12use self::mount::{MountingPoint, MountingPoints};
13use crate::{
14    metadata::{
15        AliasOptions, BasicTypes, ConfigMetadata, ConfigShorthand, ConfigVariant,
16        NestedConfigMetadata, ParamMetadata,
17    },
18    utils::EnumVariant,
19    value::Pointer,
20};
21
22mod mount;
23#[cfg(test)]
24mod tests;
25
26#[derive(Debug, Clone, Copy)]
27struct ParentLink {
28    parent_ty: any::TypeId,
29    this_ref: &'static NestedConfigMetadata,
30}
31
32#[derive(Debug, Clone)]
33pub(crate) struct ConfigData {
34    pub(crate) metadata: &'static ConfigMetadata,
35    parent_link: Option<ParentLink>,
36    pub(crate) is_top_level: bool,
37    pub(crate) coerce_serde_enums: bool,
38    all_paths: Vec<(Cow<'static, str>, AliasOptions)>,
39}
40
41impl ConfigData {
42    pub(crate) fn prefix(&self) -> Pointer<'_> {
43        Pointer(self.all_paths[0].0.as_ref())
44    }
45
46    /// Flattened configs share the location with the parent config, so a shorthand cannot apply to them.
47    pub(crate) fn shorthand(&self) -> Option<ConfigShorthand> {
48        let is_flattened = self
49            .parent_link
50            .as_ref()
51            .is_some_and(|link| link.this_ref.name.is_empty());
52        if is_flattened {
53            return None;
54        }
55        self.metadata.tag.as_ref()?.shorthand
56    }
57
58    pub(crate) fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
59        self.all_paths
60            .iter()
61            .skip(1)
62            .map(|(path, options)| (path.as_ref(), *options))
63    }
64
65    pub(crate) fn all_paths_for_param(
66        &self,
67        param: &'static ParamMetadata,
68    ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
69        self.all_paths_for_child(param.name, param.aliases, param.tag_variant)
70    }
71
72    fn all_paths_for_child(
73        &self,
74        name: &'static str,
75        aliases: &'static [(&'static str, AliasOptions)],
76        tag_variant: Option<&'static ConfigVariant>,
77    ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
78        let local_names =
79            iter::once((name, AliasOptions::default())).chain(aliases.iter().copied());
80
81        let enum_names = if let (true, Some(variant)) = (self.coerce_serde_enums, tag_variant) {
82            let variant_names = iter::once(variant.name)
83                .chain(variant.aliases.iter().copied())
84                .filter_map(|name| Some(EnumVariant::new(name)?.to_snake_case()));
85            let local_names_ = local_names.clone();
86            let paths = variant_names.flat_map(move |variant_name| {
87                local_names_
88                    .clone()
89                    .filter_map(move |(name_or_path, options)| {
90                        if name_or_path.starts_with('.') {
91                            // Only consider simple aliases, not path ones.
92                            return None;
93                        }
94                        let full_path = Pointer(&variant_name).join(name_or_path);
95                        Some((Cow::Owned(full_path), options))
96                    })
97            });
98            Some(paths)
99        } else {
100            None
101        };
102        let enum_names = enum_names.into_iter().flatten();
103        let local_names = local_names
104            .map(|(name, options)| (Cow::Borrowed(name), options))
105            .chain(enum_names);
106
107        self.all_paths
108            .iter()
109            .flat_map(move |(alias, config_options)| {
110                local_names
111                    .clone()
112                    .filter_map(move |(name_or_path, options)| {
113                        let full_path = Pointer(alias).join_path(Pointer(&name_or_path))?;
114                        Some((full_path, options.combine(*config_options)))
115                    })
116            })
117    }
118}
119
120/// Reference to a specific configuration inside [`ConfigSchema`].
121#[derive(Debug, Clone, Copy)]
122pub struct ConfigRef<'a> {
123    schema: &'a ConfigSchema,
124    prefix: &'a str,
125    pub(crate) data: &'a ConfigData,
126}
127
128impl<'a> ConfigRef<'a> {
129    /// Gets the config prefix.
130    pub fn prefix(&self) -> &'a str {
131        self.prefix
132    }
133
134    /// Gets the config metadata.
135    pub fn metadata(&self) -> &'static ConfigMetadata {
136        self.data.metadata
137    }
138
139    /// Checks whether this config is top-level (i.e., was included into the schema directly, rather than as a sub-config).
140    pub fn is_top_level(&self) -> bool {
141        self.data.parent_link.is_none()
142    }
143
144    /// Returns the shorthand applicable to this config location, if any. Unlike [`ConfigTag::shorthand`](crate::metadata::ConfigTag),
145    /// this accounts for the location: a flattened config shares the location with its parent, so a shorthand cannot apply to it.
146    pub fn shorthand(&self) -> Option<ConfigShorthand> {
147        self.data.shorthand()
148    }
149
150    #[doc(hidden)] // not stabilized yet
151    pub fn parent_link(&self) -> Option<(Self, &'static NestedConfigMetadata)> {
152        let link = self.data.parent_link?;
153        let parent_prefix = if link.this_ref.name.is_empty() {
154            // Flattened config
155            self.prefix
156        } else {
157            let (parent, _) = Pointer(self.prefix).split_last().unwrap();
158            parent.0
159        };
160        let parent_ref = Self {
161            schema: self.schema,
162            prefix: parent_prefix,
163            data: self.schema.get_ll(parent_prefix, link.parent_ty)?,
164        };
165        Some((parent_ref, link.this_ref))
166    }
167
168    /// Iterates over all aliases for this config.
169    pub fn aliases(&self) -> impl Iterator<Item = (&'a str, AliasOptions)> + '_ {
170        self.data.aliases()
171    }
172
173    /// Returns a prioritized list of absolute paths to the specified param (higher-priority paths first).
174    /// For the result to make sense, the param must be a part of this config.
175    #[doc(hidden)] // too low-level
176    pub fn all_paths_for_param(
177        &self,
178        param: &'static ParamMetadata,
179    ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
180        self.data.all_paths_for_param(param)
181    }
182}
183
184/// Mutable reference to a specific configuration inside [`ConfigSchema`].
185#[derive(Debug)]
186pub struct ConfigMut<'a> {
187    schema: &'a mut ConfigSchema,
188    prefix: String,
189    type_id: any::TypeId,
190}
191
192impl ConfigMut<'_> {
193    /// Gets the config prefix.
194    pub fn prefix(&self) -> &str {
195        &self.prefix
196    }
197
198    /// Iterates over all aliases for this config.
199    pub fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
200        let data = &self.schema.configs[self.prefix.as_str()].inner[&self.type_id];
201        data.aliases()
202    }
203
204    /// Pushes an additional alias for the config.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if adding a config leads to violations of fundamental invariants
209    /// (same as for [`ConfigSchema::insert()`]).
210    pub fn push_alias(self, alias: &'static str) -> anyhow::Result<Self> {
211        self.push_alias_inner(alias, AliasOptions::new())
212    }
213
214    /// Same as [`Self::push_alias()`], but also marks the alias as deprecated.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if adding a config leads to violations of fundamental invariants
219    /// (same as for [`ConfigSchema::insert()`]).
220    pub fn push_deprecated_alias(self, alias: &'static str) -> anyhow::Result<Self> {
221        self.push_alias_inner(
222            alias,
223            AliasOptions {
224                is_deprecated: true,
225            },
226        )
227    }
228
229    fn push_alias_inner(self, alias: &'static str, options: AliasOptions) -> anyhow::Result<Self> {
230        let mut patched = PatchedSchema::new(self.schema);
231        patched.insert_alias(self.prefix.clone(), self.type_id, Pointer(alias), options)?;
232        patched.commit();
233        Ok(self)
234    }
235}
236
237#[derive(Debug, Clone, Default)]
238struct ConfigsForPrefix {
239    inner: HashMap<any::TypeId, ConfigData>,
240    by_depth: BTreeSet<(usize, any::TypeId)>,
241}
242
243impl ConfigsForPrefix {
244    fn by_depth(&self) -> impl Iterator<Item = &ConfigData> + '_ {
245        self.by_depth.iter().map(|(_, ty)| &self.inner[ty])
246    }
247
248    fn insert(&mut self, ty: any::TypeId, depth: Option<usize>, data: ConfigData) {
249        self.inner.insert(ty, data);
250        if let Some(depth) = depth {
251            self.by_depth.insert((depth, ty));
252        }
253    }
254
255    fn extend(&mut self, other: Self) {
256        self.inner.extend(other.inner);
257        self.by_depth.extend(other.by_depth);
258    }
259}
260
261/// Schema for configuration. Can contain multiple configs bound to different paths.
262// TODO: more docs; e.g., document global aliases
263#[derive(Debug, Clone, Default)]
264pub struct ConfigSchema {
265    // Order configs by canonical prefix for iteration etc. Also, this makes configs iterator topologically
266    // sorted, and makes it easy to query prefix ranges, but these properties aren't used for now.
267    configs: BTreeMap<Cow<'static, str>, ConfigsForPrefix>,
268    mounting_points: MountingPoints,
269    coerce_serde_enums: bool,
270}
271
272impl ConfigSchema {
273    /// Creates a schema consisting of a single configuration at the specified prefix.
274    #[allow(clippy::missing_panics_doc)]
275    pub fn new(metadata: &'static ConfigMetadata, prefix: &'static str) -> Self {
276        let mut this = Self::default();
277        this.insert(metadata, prefix)
278            .expect("internal error: failed inserting first config to the schema");
279        this
280    }
281
282    /// Switches coercing for serde-like enums. Coercion will add path aliases for all tagged params in enum configs
283    /// added to the schema afterward (or until `coerce_serde_enums(false)` is called). Coercion will apply
284    /// to nested enum configs as well.
285    ///
286    /// For example, if a config param named `param` corresponds to the tag `SomeTag`, then alias `.some_tag.param`
287    /// (`snake_cased` tag + param name) will be added for the param. Tag aliases and param aliases will result
288    /// in additional path aliases, as expected. For example, if `param` has alias `alias` and the tag has alias `AliasTag`,
289    /// then the param will have `.alias_tag.param`, `.alias_tag.alias` and `.some_tag.alias` aliases.
290    pub fn coerce_serde_enums(&mut self, coerce: bool) -> &mut Self {
291        self.coerce_serde_enums = coerce;
292        self
293    }
294
295    /// Iterates over all configs with their canonical prefixes.
296    pub(crate) fn iter_ll(&self) -> impl Iterator<Item = (Pointer<'_>, &ConfigData)> + '_ {
297        self.configs
298            .iter()
299            .flat_map(|(prefix, data)| data.inner.values().map(move |data| (Pointer(prefix), data)))
300    }
301
302    pub(crate) fn contains_canonical_param(&self, at: Pointer<'_>) -> bool {
303        self.mounting_points.get(at.0).is_some_and(|mount| {
304            matches!(
305                mount,
306                MountingPoint::Param {
307                    is_canonical: true,
308                    ..
309                }
310            )
311        })
312    }
313
314    pub(crate) fn params_with_kv_path<'s>(
315        &'s self,
316        kv_path: &'s str,
317    ) -> impl Iterator<Item = (Pointer<'s>, BasicTypes)> + 's {
318        self.mounting_points
319            .by_kv_path(kv_path)
320            .filter_map(|(path, mount)| {
321                let expecting = match mount {
322                    MountingPoint::Param { expecting, .. } => *expecting,
323                    MountingPoint::Config { .. } => return None,
324                };
325                Some((path, expecting))
326            })
327    }
328
329    /// Returns locations of enum configs with a shorthand corresponding to the specified key-value path.
330    pub(crate) fn shorthand_configs_with_kv_path<'s>(
331        &'s self,
332        kv_path: &'s str,
333    ) -> impl Iterator<Item = Pointer<'s>> + 's {
334        self.mounting_points
335            .by_kv_path(kv_path)
336            .filter_map(|(path, mount)| {
337                matches!(mount, MountingPoint::Config { shorthand: true }).then_some(path)
338            })
339    }
340
341    /// Iterates over all configs contained in this schema. A unique key for a config is its type + location;
342    /// i.e., multiple returned refs may have the same config type xor same location (never both).
343    pub fn iter(&self) -> impl Iterator<Item = ConfigRef<'_>> + '_ {
344        self.configs.iter().flat_map(move |(prefix, data)| {
345            data.by_depth().map(move |data| ConfigRef {
346                schema: self,
347                prefix: prefix.as_ref(),
348                data,
349            })
350        })
351    }
352
353    /// Lists all prefixes for the specified config. This does not include aliases.
354    pub fn locate(&self, metadata: &'static ConfigMetadata) -> impl Iterator<Item = &str> + '_ {
355        let config_type_id = metadata.ty.id();
356        self.configs.iter().filter_map(move |(prefix, data)| {
357            data.inner
358                .contains_key(&config_type_id)
359                .then_some(prefix.as_ref())
360        })
361    }
362
363    /// Gets a reference to a config by ist unique key (metadata + canonical prefix).
364    pub fn get<'s>(
365        &'s self,
366        metadata: &'static ConfigMetadata,
367        prefix: &'s str,
368    ) -> Option<ConfigRef<'s>> {
369        let data = self.get_ll(prefix, metadata.ty.id())?;
370        Some(ConfigRef {
371            schema: self,
372            prefix,
373            data,
374        })
375    }
376
377    fn get_ll(&self, prefix: &str, ty: any::TypeId) -> Option<&ConfigData> {
378        self.configs.get(prefix)?.inner.get(&ty)
379    }
380
381    /// Gets a reference to a config by ist unique key (metadata + canonical prefix).
382    pub fn get_mut(
383        &mut self,
384        metadata: &'static ConfigMetadata,
385        prefix: &str,
386    ) -> Option<ConfigMut<'_>> {
387        let ty = metadata.ty.id();
388        if !self.configs.get(prefix)?.inner.contains_key(&ty) {
389            return None;
390        }
391
392        Some(ConfigMut {
393            schema: self,
394            prefix: prefix.to_owned(),
395            type_id: ty,
396        })
397    }
398
399    /// Returns a single reference to the specified config.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if the configuration is not registered or has more than one mount point.
404    #[allow(clippy::missing_panics_doc)] // false positive
405    pub fn single(&self, metadata: &'static ConfigMetadata) -> anyhow::Result<ConfigRef<'_>> {
406        let prefixes: Vec<_> = self.locate(metadata).take(2).collect();
407        match prefixes.as_slice() {
408            [] => anyhow::bail!(
409                "configuration `{}` is not registered in schema",
410                metadata.ty.name_in_code()
411            ),
412            &[prefix] => Ok(ConfigRef {
413                schema: self,
414                prefix,
415                data: &self.configs[prefix].inner[&metadata.ty.id()],
416            }),
417            [first, second] => anyhow::bail!(
418                "configuration `{}` is registered in at least 2 locations: {first:?}, {second:?}",
419                metadata.ty.name_in_code()
420            ),
421            _ => unreachable!(),
422        }
423    }
424
425    /// Returns a single mutable reference to the specified config.
426    ///
427    /// # Errors
428    ///
429    /// Returns an error if the configuration is not registered or has more than one mount point.
430    #[allow(clippy::missing_panics_doc)] // false positive
431    pub fn single_mut(
432        &mut self,
433        metadata: &'static ConfigMetadata,
434    ) -> anyhow::Result<ConfigMut<'_>> {
435        let mut it = self.locate(metadata);
436        let first_prefix = it.next().with_context(|| {
437            format!(
438                "configuration `{}` is not registered in schema",
439                metadata.ty.name_in_code()
440            )
441        })?;
442        if let Some(second_prefix) = it.next() {
443            anyhow::bail!(
444                "configuration `{}` is registered in at least 2 locations: {first_prefix:?}, {second_prefix:?}",
445                metadata.ty.name_in_code()
446            );
447        }
448
449        drop(it);
450        let prefix = first_prefix.to_owned();
451        Ok(ConfigMut {
452            schema: self,
453            type_id: metadata.ty.id(),
454            prefix,
455        })
456    }
457
458    /// Inserts a new configuration type at the specified place.
459    ///
460    /// # Errors
461    ///
462    /// Returns an error if adding a config leads to violations of fundamental invariants:
463    ///
464    /// - If a parameter in the new config (taking aliases into account, and params in nested / flattened configs)
465    ///   is mounted at the location of an existing config.
466    /// - Vice versa, if a config or nested config is mounted at the location of an existing param.
467    /// - If a parameter is mounted at the location of a parameter with disjoint [expected types](ParamMetadata.expecting).
468    pub fn insert(
469        &mut self,
470        metadata: &'static ConfigMetadata,
471        prefix: &'static str,
472    ) -> anyhow::Result<ConfigMut<'_>> {
473        let coerce_serde_enums = self.coerce_serde_enums;
474        let mut patched = PatchedSchema::new(self);
475        patched.insert_config(prefix, metadata, coerce_serde_enums)?;
476        patched.commit();
477        Ok(ConfigMut {
478            schema: self,
479            type_id: metadata.ty.id(),
480            prefix: prefix.to_owned(),
481        })
482    }
483}
484
485/// [`ConfigSchema`] together with a patch that can be atomically committed.
486#[derive(Debug)]
487#[must_use = "Should be `commit()`ted"]
488struct PatchedSchema<'a> {
489    base: &'a mut ConfigSchema,
490    patch: ConfigSchema,
491}
492
493impl<'a> PatchedSchema<'a> {
494    fn new(base: &'a mut ConfigSchema) -> Self {
495        Self {
496            base,
497            patch: ConfigSchema::default(),
498        }
499    }
500
501    fn mount(&self, path: &str) -> Option<&MountingPoint> {
502        self.patch
503            .mounting_points
504            .get(path)
505            .or_else(|| self.base.mounting_points.get(path))
506    }
507
508    fn insert_config(
509        &mut self,
510        prefix: &'static str,
511        metadata: &'static ConfigMetadata,
512        coerce_serde_enums: bool,
513    ) -> anyhow::Result<()> {
514        self.insert_recursively(
515            prefix.into(),
516            true,
517            ConfigData {
518                metadata,
519                parent_link: None,
520                is_top_level: true,
521                coerce_serde_enums,
522                all_paths: vec![(prefix.into(), AliasOptions::new())],
523            },
524        )
525    }
526
527    fn insert_recursively(
528        &mut self,
529        prefix: Cow<'static, str>,
530        is_new: bool,
531        data: ConfigData,
532    ) -> anyhow::Result<()> {
533        let depth = is_new.then_some(0_usize);
534        let mut pending_configs = vec![(prefix, data, depth)];
535
536        // Insert / update all nested configs recursively.
537        while let Some((prefix, data, depth)) = pending_configs.pop() {
538            // Check whether the config is already present; if so, no need to insert the config
539            // or any nested configs.
540            if is_new && self.base.get_ll(&prefix, data.metadata.ty.id()).is_some() {
541                continue;
542            }
543
544            let child_depth = depth.map(|d| d + 1);
545            let new_configs = Self::list_nested_configs(Pointer(&prefix), &data)
546                .map(|(prefix, data)| (prefix.into(), data, child_depth));
547            pending_configs.extend(new_configs);
548            self.insert_inner(prefix, depth, data)?;
549        }
550        Ok(())
551    }
552
553    fn insert_alias(
554        &mut self,
555        prefix: String,
556        config_id: any::TypeId,
557        alias: Pointer<'static>,
558        options: AliasOptions,
559    ) -> anyhow::Result<()> {
560        let config_data = &self.base.configs[prefix.as_str()].inner[&config_id];
561        if config_data
562            .all_paths
563            .iter()
564            .any(|(name, _)| name == alias.0)
565        {
566            return Ok(()); // shortcut in the no-op case
567        }
568
569        let metadata = config_data.metadata;
570        self.insert_recursively(
571            prefix.into(),
572            false,
573            ConfigData {
574                metadata,
575                parent_link: config_data.parent_link,
576                is_top_level: config_data.is_top_level,
577                coerce_serde_enums: config_data.coerce_serde_enums,
578                all_paths: vec![(alias.0.into(), options)],
579            },
580        )
581    }
582
583    fn list_nested_configs<'i>(
584        prefix: Pointer<'i>,
585        data: &'i ConfigData,
586    ) -> impl Iterator<Item = (String, ConfigData)> + 'i {
587        data.metadata.nested_configs.iter().map(move |nested| {
588            let all_paths =
589                data.all_paths_for_child(nested.name, nested.aliases, nested.tag_variant);
590            let all_paths = all_paths
591                .map(|(path, options)| (Cow::Owned(path), options))
592                .collect();
593
594            let config_data = ConfigData {
595                metadata: nested.meta,
596                parent_link: Some(ParentLink {
597                    parent_ty: data.metadata.ty.id(),
598                    this_ref: nested,
599                }),
600                is_top_level: false,
601                coerce_serde_enums: data.coerce_serde_enums,
602                all_paths,
603            };
604            (prefix.join(nested.name), config_data)
605        })
606    }
607
608    fn insert_inner(
609        &mut self,
610        prefix: Cow<'static, str>,
611        depth: Option<usize>,
612        mut data: ConfigData,
613    ) -> anyhow::Result<()> {
614        let config_name = data.metadata.ty.name_in_code();
615        let config_paths = data.all_paths.iter().map(|(name, _)| name.as_ref());
616        let config_paths = iter::once(prefix.as_ref()).chain(config_paths);
617        let has_shorthand = data.shorthand().is_some();
618
619        for path in config_paths {
620            let mut shorthand = has_shorthand;
621            if let Some(mount) = self.mount(path) {
622                match mount {
623                    MountingPoint::Config {
624                        shorthand: prev_shorthand,
625                    } => shorthand |= *prev_shorthand,
626                    MountingPoint::Param { .. } => {
627                        anyhow::bail!(
628                            "Cannot mount config `{}` at `{path}` because parameter(s) are already mounted at this path",
629                            data.metadata.ty.name_in_code()
630                        );
631                    }
632                }
633            }
634            self.patch
635                .mounting_points
636                .insert(path.to_owned(), MountingPoint::Config { shorthand });
637        }
638
639        for param in data.metadata.params {
640            let all_paths = data.all_paths_for_param(param);
641
642            for (name_i, (full_name, _)) in all_paths.enumerate() {
643                let mut was_canonical = false;
644                if let Some(mount) = self.mount(&full_name) {
645                    let prev_expecting = match mount {
646                        MountingPoint::Param {
647                            expecting,
648                            is_canonical,
649                        } => {
650                            was_canonical = *is_canonical;
651                            *expecting
652                        }
653                        MountingPoint::Config { .. } => {
654                            anyhow::bail!(
655                                "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
656                                 config(s) are already mounted at this path",
657                                name = param.name,
658                                field = param.rust_field_name
659                            );
660                        }
661                    };
662
663                    if prev_expecting != param.expecting {
664                        anyhow::bail!(
665                            "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
666                             it expects {expecting}, while the existing param(s) mounted at this path expect {prev_expecting}",
667                            name = param.name,
668                            field = param.rust_field_name,
669                            expecting = param.expecting
670                        );
671                    }
672                }
673                let is_canonical = was_canonical || name_i == 0;
674                self.patch.mounting_points.insert(
675                    full_name,
676                    MountingPoint::Param {
677                        expecting: param.expecting,
678                        is_canonical,
679                    },
680                );
681            }
682        }
683
684        // `data` is the new data for the config, so we need to consult `base` for existing data.
685        // Unlike with params, by design we never insert same config entries in the same patch,
686        // so it's safe to *only* consult `base`.
687        let config_id = data.metadata.ty.id();
688        let prev_data = self.base.get_ll(&prefix, config_id);
689        if let Some(prev_data) = prev_data {
690            // Append new aliases to the end since their ordering determines alias priority
691            let mut all_paths = prev_data.all_paths.clone();
692            all_paths.extend_from_slice(&data.all_paths);
693            data.all_paths = all_paths;
694        }
695
696        self.patch
697            .configs
698            .entry(prefix)
699            .or_default()
700            .insert(config_id, depth, data);
701        Ok(())
702    }
703
704    fn commit(self) {
705        for (prefix, data) in self.patch.configs {
706            let prev_data = self.base.configs.entry(prefix).or_default();
707            prev_data.extend(data);
708        }
709        self.base.mounting_points.extend(self.patch.mounting_points);
710    }
711}