Skip to main content

rust_config_tree/
tree.rs

1//! Recursive include tree traversal primitives.
2//!
3//! This module provides the format-agnostic tree loader used by the high-level
4//! `confique` API. Callers supply a loader that returns a source value and the
5//! include paths declared by that source.
6
7use std::{
8    collections::HashSet,
9    path::{Path, PathBuf},
10};
11
12use crate::{BoxError, ConfigTreeError, Result, absolutize_lexical, resolve_include_path};
13
14/// Controls the order in which sibling include paths are traversed.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
16pub enum IncludeOrder {
17    /// Visit include paths in the order they were declared.
18    #[default]
19    Declared,
20    /// Visit sibling include paths in reverse declaration order.
21    Reverse,
22}
23
24/// Options for loading a recursive config tree.
25///
26/// Use this type when the default traversal behavior is not enough, for example
27/// when sibling includes should be visited in reverse declaration order.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct ConfigTreeOptions {
30    include_order: IncludeOrder,
31}
32
33/// Builder-style configuration for include tree traversal.
34impl ConfigTreeOptions {
35    /// Sets the sibling include traversal order.
36    ///
37    /// # Arguments
38    ///
39    /// - `include_order`: Order used when visiting sibling include paths.
40    ///
41    /// # Returns
42    ///
43    /// Returns the updated options value.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use rust_config_tree::{ConfigTreeOptions, IncludeOrder};
49    ///
50    /// let options = ConfigTreeOptions::default().include_order(IncludeOrder::Reverse);
51    /// # let _ = options;
52    /// ```
53    pub fn include_order(mut self, include_order: IncludeOrder) -> Self {
54        self.include_order = include_order;
55        self
56    }
57
58    /// Loads a config tree from `root_path` with a custom source loader.
59    ///
60    /// The loader returns both the source value and the include paths declared
61    /// by that source. Relative include paths are resolved from the source path.
62    ///
63    /// # Type Parameters
64    ///
65    /// - `T`: Loaded value type stored for each config source.
66    /// - `E`: Error type returned by `load`.
67    /// - `F`: Source loader callback type.
68    ///
69    /// # Arguments
70    ///
71    /// - `root_path`: Root config path to load first.
72    /// - `load`: Callback that receives each normalized absolute source path
73    ///   and returns the source value with its declared include paths.
74    ///
75    /// # Returns
76    ///
77    /// Returns a [`ConfigTree`] containing loaded nodes in traversal order.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use std::{io, path::{Path, PathBuf}};
83    /// use rust_config_tree::{ConfigSource, ConfigTreeOptions};
84    ///
85    /// let tree = ConfigTreeOptions::default().load(
86    ///     "root.yaml",
87    ///     |path: &Path| -> io::Result<ConfigSource<&'static str>> {
88    ///         if path.ends_with("root.yaml") {
89    ///             Ok(ConfigSource::new("root", vec![PathBuf::from("child.yaml")]))
90    ///         } else {
91    ///             Ok(ConfigSource::new("child", Vec::new()))
92    ///         }
93    ///     },
94    /// )?;
95    ///
96    /// assert_eq!(tree.into_values(), vec!["root", "child"]);
97    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
98    /// ```
99    pub fn load<T, E, F>(&self, root_path: impl AsRef<Path>, mut load: F) -> Result<ConfigTree<T>>
100    where
101        E: Into<BoxError>,
102        F: FnMut(&Path) -> std::result::Result<ConfigSource<T>, E>,
103    {
104        let mut state = TraversalState::default();
105        let mut nodes = Vec::new();
106        self.collect(root_path.as_ref(), &mut load, &mut state, &mut nodes)?;
107        Ok(ConfigTree { nodes })
108    }
109
110    /// Recursively loads one source path and its declared includes.
111    ///
112    /// # Type Parameters
113    ///
114    /// - `T`: Loaded value type stored for each config source.
115    /// - `E`: Error type returned by `load`.
116    /// - `F`: Source loader callback type.
117    ///
118    /// # Arguments
119    ///
120    /// - `self`: Traversal options controlling sibling include order.
121    /// - `path`: Source path to load.
122    /// - `load`: Source loader callback.
123    /// - `state`: Traversal state used for cycle detection and deduplication.
124    /// - `nodes`: Output list receiving loaded nodes.
125    ///
126    /// # Returns
127    ///
128    /// Returns `Ok(())` after this path and its includes are collected.
129    ///
130    /// # Examples
131    ///
132    /// ```no_run
133    /// let _ = ();
134    /// ```
135    fn collect<T, E, F>(
136        &self,
137        path: &Path,
138        load: &mut F,
139        state: &mut TraversalState,
140        nodes: &mut Vec<ConfigNode<T>>,
141    ) -> Result<()>
142    where
143        E: Into<BoxError>,
144        F: FnMut(&Path) -> std::result::Result<ConfigSource<T>, E>,
145    {
146        let path = absolutize_lexical(path)?;
147        if !state.enter(&path)? {
148            return Ok(());
149        }
150
151        let source = load(&path).map_err(|source| ConfigTreeError::load(&path, source))?;
152        validate_include_paths(&path, &source.includes)?;
153
154        let includes = source.includes;
155        nodes.push(ConfigNode {
156            path: path.clone(),
157            value: source.value,
158            includes: includes.clone(),
159        });
160
161        match self.include_order {
162            IncludeOrder::Declared => {
163                for include_path in &includes {
164                    let include_path = resolve_include_path(&path, include_path);
165                    self.collect(&include_path, load, state, nodes)?;
166                }
167            }
168            IncludeOrder::Reverse => {
169                for include_path in includes.iter().rev() {
170                    let include_path = resolve_include_path(&path, include_path);
171                    self.collect(&include_path, load, state, nodes)?;
172                }
173            }
174        }
175
176        state.leave();
177        Ok(())
178    }
179}
180
181/// Value and includes returned by a config source loader.
182///
183/// # Type Parameters
184///
185/// - `T`: Loaded source value type.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct ConfigSource<T> {
188    value: T,
189    includes: Vec<PathBuf>,
190}
191
192/// Constructors and accessors for values returned by source loaders.
193impl<T> ConfigSource<T> {
194    /// Creates a source from a loaded value and its declared include paths.
195    ///
196    /// # Arguments
197    ///
198    /// - `value`: Loaded source value.
199    /// - `includes`: Include paths declared by the source.
200    ///
201    /// # Returns
202    ///
203    /// Returns a new [`ConfigSource`].
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use std::path::PathBuf;
209    /// use rust_config_tree::ConfigSource;
210    ///
211    /// let source = ConfigSource::new("value", vec![PathBuf::from("child.yaml")]);
212    ///
213    /// assert_eq!(source.value(), &"value");
214    /// assert_eq!(source.includes(), &[PathBuf::from("child.yaml")]);
215    /// ```
216    pub fn new(value: T, includes: Vec<PathBuf>) -> Self {
217        Self { value, includes }
218    }
219
220    /// Returns the loaded source value.
221    ///
222    /// # Arguments
223    ///
224    /// - `self`: Source value being inspected.
225    ///
226    /// # Returns
227    ///
228    /// Returns a shared reference to the loaded source value.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use rust_config_tree::ConfigSource;
234    ///
235    /// let source = ConfigSource::new("value", Vec::new());
236    ///
237    /// assert_eq!(source.value(), &"value");
238    /// ```
239    pub fn value(&self) -> &T {
240        &self.value
241    }
242
243    /// Returns include paths declared by the source.
244    ///
245    /// # Arguments
246    ///
247    /// - `self`: Source value being inspected.
248    ///
249    /// # Returns
250    ///
251    /// Returns the include paths declared by the source.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use std::path::PathBuf;
257    /// use rust_config_tree::ConfigSource;
258    ///
259    /// let source = ConfigSource::new("value", vec![PathBuf::from("child.yaml")]);
260    ///
261    /// assert_eq!(source.includes(), &[PathBuf::from("child.yaml")]);
262    /// ```
263    pub fn includes(&self) -> &[PathBuf] {
264        &self.includes
265    }
266
267    /// Decomposes the source into its value and include paths.
268    ///
269    /// # Arguments
270    ///
271    /// - `self`: Source value to decompose.
272    ///
273    /// # Returns
274    ///
275    /// Returns `(value, includes)`.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use std::path::PathBuf;
281    /// use rust_config_tree::ConfigSource;
282    ///
283    /// let source = ConfigSource::new("value", vec![PathBuf::from("child.yaml")]);
284    ///
285    /// assert_eq!(source.into_parts(), ("value", vec![PathBuf::from("child.yaml")]));
286    /// ```
287    pub fn into_parts(self) -> (T, Vec<PathBuf>) {
288        (self.value, self.includes)
289    }
290}
291
292/// Converts the common `(value, includes)` loader shape into a source value.
293impl<T> From<(T, Vec<PathBuf>)> for ConfigSource<T> {
294    /// Builds a source value from a tuple.
295    ///
296    /// # Arguments
297    ///
298    /// - `(value, includes)`: Loaded value and declared include paths.
299    ///
300    /// # Returns
301    ///
302    /// Returns a [`ConfigSource`] containing the tuple parts.
303    ///
304    /// # Examples
305    ///
306    /// ```no_run
307    /// let _ = ();
308    /// ```
309    fn from((value, includes): (T, Vec<PathBuf>)) -> Self {
310        Self::new(value, includes)
311    }
312}
313
314/// A loaded config tree in traversal order.
315///
316/// # Type Parameters
317///
318/// - `T`: Loaded source value type stored by each node.
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct ConfigTree<T> {
321    nodes: Vec<ConfigNode<T>>,
322}
323
324/// Accessors for a loaded config tree.
325impl<T> ConfigTree<T> {
326    /// Returns loaded tree nodes in traversal order.
327    ///
328    /// # Arguments
329    ///
330    /// - `self`: Loaded config tree being inspected.
331    ///
332    /// # Returns
333    ///
334    /// Returns loaded nodes in traversal order.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use std::{io, path::Path};
340    /// use rust_config_tree::{ConfigSource, load_config_tree};
341    ///
342    /// let tree = load_config_tree(
343    ///     "root.yaml",
344    ///     |_path: &Path| -> io::Result<ConfigSource<&'static str>> {
345    ///         Ok(ConfigSource::new("root", Vec::new()))
346    ///     },
347    /// )?;
348    ///
349    /// assert_eq!(tree.nodes().len(), 1);
350    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
351    /// ```
352    pub fn nodes(&self) -> &[ConfigNode<T>] {
353        &self.nodes
354    }
355
356    /// Decomposes the tree into its nodes.
357    ///
358    /// # Arguments
359    ///
360    /// - `self`: Loaded config tree to decompose.
361    ///
362    /// # Returns
363    ///
364    /// Returns the loaded nodes, preserving traversal order.
365    ///
366    /// # Examples
367    ///
368    /// ```
369    /// use std::{io, path::Path};
370    /// use rust_config_tree::{ConfigSource, load_config_tree};
371    ///
372    /// let tree = load_config_tree(
373    ///     "root.yaml",
374    ///     |_path: &Path| -> io::Result<ConfigSource<&'static str>> {
375    ///         Ok(ConfigSource::new("root", Vec::new()))
376    ///     },
377    /// )?;
378    ///
379    /// assert_eq!(tree.into_nodes().len(), 1);
380    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
381    /// ```
382    pub fn into_nodes(self) -> Vec<ConfigNode<T>> {
383        self.nodes
384    }
385
386    /// Decomposes the tree into loaded values, discarding paths and includes.
387    ///
388    /// # Arguments
389    ///
390    /// - `self`: Loaded config tree to decompose.
391    ///
392    /// # Returns
393    ///
394    /// Returns loaded source values in traversal order.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use std::{io, path::Path};
400    /// use rust_config_tree::{ConfigSource, load_config_tree};
401    ///
402    /// let tree = load_config_tree(
403    ///     "root.yaml",
404    ///     |_path: &Path| -> io::Result<ConfigSource<&'static str>> {
405    ///         Ok(ConfigSource::new("root", Vec::new()))
406    ///     },
407    /// )?;
408    ///
409    /// assert_eq!(tree.into_values(), vec!["root"]);
410    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
411    /// ```
412    pub fn into_values(self) -> Vec<T> {
413        self.nodes.into_iter().map(|node| node.value).collect()
414    }
415}
416
417/// One loaded config source in a tree.
418///
419/// # Type Parameters
420///
421/// - `T`: Loaded source value type stored by this node.
422#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct ConfigNode<T> {
424    path: PathBuf,
425    value: T,
426    includes: Vec<PathBuf>,
427}
428
429/// Accessors for one loaded config tree node.
430impl<T> ConfigNode<T> {
431    /// Returns the normalized absolute source path.
432    ///
433    /// # Arguments
434    ///
435    /// - `self`: Loaded config node being inspected.
436    ///
437    /// # Returns
438    ///
439    /// Returns the normalized absolute source path.
440    ///
441    /// # Examples
442    ///
443    /// ```
444    /// use std::{io, path::Path};
445    /// use rust_config_tree::{ConfigSource, load_config_tree};
446    ///
447    /// let tree = load_config_tree(
448    ///     "root.yaml",
449    ///     |_path: &Path| -> io::Result<ConfigSource<&'static str>> {
450    ///         Ok(ConfigSource::new("root", Vec::new()))
451    ///     },
452    /// )?;
453    ///
454    /// assert!(tree.nodes()[0].path().ends_with("root.yaml"));
455    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
456    /// ```
457    pub fn path(&self) -> &Path {
458        &self.path
459    }
460
461    /// Returns the loaded source value.
462    ///
463    /// # Arguments
464    ///
465    /// - `self`: Loaded config node being inspected.
466    ///
467    /// # Returns
468    ///
469    /// Returns a shared reference to the loaded source value.
470    ///
471    /// # Examples
472    ///
473    /// ```
474    /// use std::{io, path::Path};
475    /// use rust_config_tree::{ConfigSource, load_config_tree};
476    ///
477    /// let tree = load_config_tree(
478    ///     "root.yaml",
479    ///     |_path: &Path| -> io::Result<ConfigSource<&'static str>> {
480    ///         Ok(ConfigSource::new("root", Vec::new()))
481    ///     },
482    /// )?;
483    ///
484    /// assert_eq!(tree.nodes()[0].value(), &"root");
485    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
486    /// ```
487    pub fn value(&self) -> &T {
488        &self.value
489    }
490
491    /// Returns include paths declared by this source.
492    ///
493    /// # Arguments
494    ///
495    /// - `self`: Loaded config node being inspected.
496    ///
497    /// # Returns
498    ///
499    /// Returns the include paths declared by this source.
500    ///
501    /// # Examples
502    ///
503    /// ```
504    /// use std::{io, path::{Path, PathBuf}};
505    /// use rust_config_tree::{ConfigSource, load_config_tree};
506    ///
507    /// let tree = load_config_tree(
508    ///     "root.yaml",
509    ///     |path: &Path| -> io::Result<ConfigSource<&'static str>> {
510    ///         if path.ends_with("root.yaml") {
511    ///             Ok(ConfigSource::new("root", vec![PathBuf::from("child.yaml")]))
512    ///         } else {
513    ///             Ok(ConfigSource::new("child", Vec::new()))
514    ///         }
515    ///     },
516    /// )?;
517    ///
518    /// assert_eq!(tree.nodes()[0].includes(), &[PathBuf::from("child.yaml")]);
519    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
520    /// ```
521    pub fn includes(&self) -> &[PathBuf] {
522        &self.includes
523    }
524
525    /// Decomposes the node into its loaded value.
526    ///
527    /// # Arguments
528    ///
529    /// - `self`: Loaded config node to decompose.
530    ///
531    /// # Returns
532    ///
533    /// Returns the loaded source value.
534    ///
535    /// # Examples
536    ///
537    /// ```
538    /// use std::{io, path::Path};
539    /// use rust_config_tree::{ConfigSource, load_config_tree};
540    ///
541    /// let tree = load_config_tree(
542    ///     "root.yaml",
543    ///     |_path: &Path| -> io::Result<ConfigSource<&'static str>> {
544    ///         Ok(ConfigSource::new("root", Vec::new()))
545    ///     },
546    /// )?;
547    ///
548    /// let mut nodes = tree.into_nodes();
549    /// assert_eq!(nodes.remove(0).into_value(), "root");
550    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
551    /// ```
552    pub fn into_value(self) -> T {
553        self.value
554    }
555}
556
557/// Loads a config tree with default traversal options.
558///
559/// # Type Parameters
560///
561/// - `T`: Loaded value type stored for each config source.
562/// - `E`: Error type returned by `load`.
563/// - `F`: Source loader callback type.
564///
565/// # Arguments
566///
567/// - `root_path`: Root config path to load first.
568/// - `load`: Callback that receives each normalized absolute source path and
569///   returns the source value with its declared include paths.
570///
571/// # Returns
572///
573/// Returns a [`ConfigTree`] containing loaded nodes in traversal order.
574///
575/// # Examples
576///
577/// ```
578/// use std::{io, path::{Path, PathBuf}};
579/// use rust_config_tree::{ConfigSource, load_config_tree};
580///
581/// let tree = load_config_tree(
582///     "root.yaml",
583///     |path: &Path| -> io::Result<ConfigSource<&'static str>> {
584///         if path.ends_with("root.yaml") {
585///             Ok(ConfigSource::new("root", vec![PathBuf::from("child.yaml")]))
586///         } else {
587///             Ok(ConfigSource::new("child", Vec::new()))
588///         }
589///     },
590/// )?;
591///
592/// assert_eq!(tree.into_values(), vec!["root", "child"]);
593/// # Ok::<(), rust_config_tree::ConfigTreeError>(())
594/// ```
595pub fn load_config_tree<T, E, F>(root_path: impl AsRef<Path>, load: F) -> Result<ConfigTree<T>>
596where
597    E: Into<BoxError>,
598    F: FnMut(&Path) -> std::result::Result<ConfigSource<T>, E>,
599{
600    ConfigTreeOptions::default().load(root_path, load)
601}
602
603/// Tracks paths currently being visited and paths already loaded.
604#[derive(Default)]
605pub(crate) struct TraversalState {
606    visiting: Vec<PathBuf>,
607    loaded: HashSet<PathBuf>,
608}
609
610/// Include traversal state transitions.
611impl TraversalState {
612    /// Enters a normalized source path during traversal.
613    ///
614    /// # Arguments
615    ///
616    /// - `path`: Normalized absolute source path.
617    ///
618    /// # Returns
619    ///
620    /// Returns `Ok(true)` when traversal should load the path, `Ok(false)` when
621    /// it was already loaded, or an include-cycle error when the path is already
622    /// in the active traversal stack.
623    ///
624    /// # Examples
625    ///
626    /// ```no_run
627    /// let _ = ();
628    /// ```
629    pub(crate) fn enter(&mut self, path: &Path) -> Result<bool> {
630        if let Some(pos) = self.visiting.iter().position(|existing| existing == path) {
631            let mut chain = self.visiting[pos..].to_vec();
632            chain.push(path.to_path_buf());
633            return Err(ConfigTreeError::IncludeCycle { chain });
634        }
635
636        if !self.loaded.insert(path.to_path_buf()) {
637            return Ok(false);
638        }
639
640        self.visiting.push(path.to_path_buf());
641        Ok(true)
642    }
643
644    /// Leaves the current traversal path.
645    ///
646    /// # Arguments
647    ///
648    /// - `self`: Traversal state whose current path should be popped.
649    ///
650    /// # Returns
651    ///
652    /// This function mutates the traversal stack and returns no value.
653    ///
654    /// # Examples
655    ///
656    /// ```no_run
657    /// let _ = ();
658    /// ```
659    pub(crate) fn leave(&mut self) {
660        self.visiting.pop();
661    }
662}
663
664/// Validates include paths declared by a source.
665///
666/// # Arguments
667///
668/// - `path`: Source path whose include list is being validated.
669/// - `paths`: Include paths declared by `path`.
670///
671/// # Returns
672///
673/// Returns `Ok(())` when every include path is non-empty.
674///
675/// # Examples
676///
677/// ```no_run
678/// let _ = ();
679/// ```
680pub(crate) fn validate_include_paths(path: &Path, paths: &[PathBuf]) -> Result<()> {
681    for (index, include_path) in paths.iter().enumerate() {
682        if include_path.as_os_str().is_empty() {
683            return Err(ConfigTreeError::EmptyIncludePath {
684                path: path.to_path_buf(),
685                index,
686            });
687        }
688    }
689
690    Ok(())
691}
692
693#[cfg(test)]
694#[path = "unit_tests/tree.rs"]
695mod unit_tests;