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