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
33impl ConfigTreeOptions {
34    /// Sets the sibling include traversal order.
35    ///
36    /// # Arguments
37    ///
38    /// - `include_order`: Order used when visiting sibling include paths.
39    ///
40    /// # Returns
41    ///
42    /// Returns the updated options value.
43    pub fn include_order(mut self, include_order: IncludeOrder) -> Self {
44        self.include_order = include_order;
45        self
46    }
47
48    /// Loads a config tree from `root_path` with a custom source loader.
49    ///
50    /// The loader returns both the source value and the include paths declared
51    /// by that source. Relative include paths are resolved from the source path.
52    ///
53    /// # Type Parameters
54    ///
55    /// - `T`: Loaded value type stored for each config source.
56    /// - `E`: Error type returned by `load`.
57    /// - `F`: Source loader callback type.
58    ///
59    /// # Arguments
60    ///
61    /// - `root_path`: Root config path to load first.
62    /// - `load`: Callback that receives each normalized absolute source path
63    ///   and returns the source value with its declared include paths.
64    ///
65    /// # Returns
66    ///
67    /// Returns a [`ConfigTree`] containing loaded nodes in traversal order.
68    pub fn load<T, E, F>(&self, root_path: impl AsRef<Path>, mut load: F) -> Result<ConfigTree<T>>
69    where
70        E: Into<BoxError>,
71        F: FnMut(&Path) -> std::result::Result<ConfigSource<T>, E>,
72    {
73        let mut state = TraversalState::default();
74        let mut nodes = Vec::new();
75        self.collect(root_path.as_ref(), &mut load, &mut state, &mut nodes)?;
76        Ok(ConfigTree { nodes })
77    }
78
79    fn collect<T, E, F>(
80        &self,
81        path: &Path,
82        load: &mut F,
83        state: &mut TraversalState,
84        nodes: &mut Vec<ConfigNode<T>>,
85    ) -> Result<()>
86    where
87        E: Into<BoxError>,
88        F: FnMut(&Path) -> std::result::Result<ConfigSource<T>, E>,
89    {
90        let path = absolutize_lexical(path)?;
91        if !state.enter(&path)? {
92            return Ok(());
93        }
94
95        let source = load(&path).map_err(|source| ConfigTreeError::load(&path, source))?;
96        validate_include_paths(&path, &source.includes)?;
97
98        let includes = source.includes;
99        nodes.push(ConfigNode {
100            path: path.clone(),
101            value: source.value,
102            includes: includes.clone(),
103        });
104
105        match self.include_order {
106            IncludeOrder::Declared => {
107                for include_path in &includes {
108                    let include_path = resolve_include_path(&path, include_path);
109                    self.collect(&include_path, load, state, nodes)?;
110                }
111            }
112            IncludeOrder::Reverse => {
113                for include_path in includes.iter().rev() {
114                    let include_path = resolve_include_path(&path, include_path);
115                    self.collect(&include_path, load, state, nodes)?;
116                }
117            }
118        }
119
120        state.leave();
121        Ok(())
122    }
123}
124
125/// Value and includes returned by a config source loader.
126///
127/// # Type Parameters
128///
129/// - `T`: Loaded source value type.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ConfigSource<T> {
132    value: T,
133    includes: Vec<PathBuf>,
134}
135
136impl<T> ConfigSource<T> {
137    /// Creates a source from a loaded value and its declared include paths.
138    ///
139    /// # Arguments
140    ///
141    /// - `value`: Loaded source value.
142    /// - `includes`: Include paths declared by the source.
143    ///
144    /// # Returns
145    ///
146    /// Returns a new [`ConfigSource`].
147    pub fn new(value: T, includes: Vec<PathBuf>) -> Self {
148        Self { value, includes }
149    }
150
151    /// Returns the loaded source value.
152    ///
153    /// # Returns
154    ///
155    /// Returns a shared reference to the loaded source value.
156    pub fn value(&self) -> &T {
157        &self.value
158    }
159
160    /// Returns include paths declared by the source.
161    ///
162    /// # Returns
163    ///
164    /// Returns the include paths declared by the source.
165    pub fn includes(&self) -> &[PathBuf] {
166        &self.includes
167    }
168
169    /// Decomposes the source into its value and include paths.
170    ///
171    /// # Returns
172    ///
173    /// Returns `(value, includes)`.
174    pub fn into_parts(self) -> (T, Vec<PathBuf>) {
175        (self.value, self.includes)
176    }
177}
178
179impl<T> From<(T, Vec<PathBuf>)> for ConfigSource<T> {
180    fn from((value, includes): (T, Vec<PathBuf>)) -> Self {
181        Self::new(value, includes)
182    }
183}
184
185/// A loaded config tree in traversal order.
186///
187/// # Type Parameters
188///
189/// - `T`: Loaded source value type stored by each node.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct ConfigTree<T> {
192    nodes: Vec<ConfigNode<T>>,
193}
194
195impl<T> ConfigTree<T> {
196    /// Returns loaded tree nodes in traversal order.
197    ///
198    /// # Returns
199    ///
200    /// Returns loaded nodes in traversal order.
201    pub fn nodes(&self) -> &[ConfigNode<T>] {
202        &self.nodes
203    }
204
205    /// Decomposes the tree into its nodes.
206    ///
207    /// # Returns
208    ///
209    /// Returns the loaded nodes, preserving traversal order.
210    pub fn into_nodes(self) -> Vec<ConfigNode<T>> {
211        self.nodes
212    }
213
214    /// Decomposes the tree into loaded values, discarding paths and includes.
215    ///
216    /// # Returns
217    ///
218    /// Returns loaded source values in traversal order.
219    pub fn into_values(self) -> Vec<T> {
220        self.nodes.into_iter().map(|node| node.value).collect()
221    }
222}
223
224/// One loaded config source in a tree.
225///
226/// # Type Parameters
227///
228/// - `T`: Loaded source value type stored by this node.
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct ConfigNode<T> {
231    path: PathBuf,
232    value: T,
233    includes: Vec<PathBuf>,
234}
235
236impl<T> ConfigNode<T> {
237    /// Returns the normalized absolute source path.
238    ///
239    /// # Returns
240    ///
241    /// Returns the normalized absolute source path.
242    pub fn path(&self) -> &Path {
243        &self.path
244    }
245
246    /// Returns the loaded source value.
247    ///
248    /// # Returns
249    ///
250    /// Returns a shared reference to the loaded source value.
251    pub fn value(&self) -> &T {
252        &self.value
253    }
254
255    /// Returns include paths declared by this source.
256    ///
257    /// # Returns
258    ///
259    /// Returns the include paths declared by this source.
260    pub fn includes(&self) -> &[PathBuf] {
261        &self.includes
262    }
263
264    /// Decomposes the node into its loaded value.
265    ///
266    /// # Returns
267    ///
268    /// Returns the loaded source value.
269    pub fn into_value(self) -> T {
270        self.value
271    }
272}
273
274/// Loads a config tree with default traversal options.
275///
276/// # Type Parameters
277///
278/// - `T`: Loaded value type stored for each config source.
279/// - `E`: Error type returned by `load`.
280/// - `F`: Source loader callback type.
281///
282/// # Arguments
283///
284/// - `root_path`: Root config path to load first.
285/// - `load`: Callback that receives each normalized absolute source path and
286///   returns the source value with its declared include paths.
287///
288/// # Returns
289///
290/// Returns a [`ConfigTree`] containing loaded nodes in traversal order.
291pub fn load_config_tree<T, E, F>(root_path: impl AsRef<Path>, load: F) -> Result<ConfigTree<T>>
292where
293    E: Into<BoxError>,
294    F: FnMut(&Path) -> std::result::Result<ConfigSource<T>, E>,
295{
296    ConfigTreeOptions::default().load(root_path, load)
297}
298
299/// Tracks paths currently being visited and paths already loaded.
300#[derive(Default)]
301pub(crate) struct TraversalState {
302    visiting: Vec<PathBuf>,
303    loaded: HashSet<PathBuf>,
304}
305
306impl TraversalState {
307    /// Enters a normalized source path during traversal.
308    ///
309    /// # Arguments
310    ///
311    /// - `path`: Normalized absolute source path.
312    ///
313    /// # Returns
314    ///
315    /// Returns `Ok(true)` when traversal should load the path, `Ok(false)` when
316    /// it was already loaded, or an include-cycle error when the path is already
317    /// in the active traversal stack.
318    pub(crate) fn enter(&mut self, path: &Path) -> Result<bool> {
319        if let Some(pos) = self.visiting.iter().position(|existing| existing == path) {
320            let mut chain = self.visiting[pos..].to_vec();
321            chain.push(path.to_path_buf());
322            return Err(ConfigTreeError::IncludeCycle { chain });
323        }
324
325        if !self.loaded.insert(path.to_path_buf()) {
326            return Ok(false);
327        }
328
329        self.visiting.push(path.to_path_buf());
330        Ok(true)
331    }
332
333    /// Leaves the current traversal path.
334    ///
335    /// # Returns
336    ///
337    /// This function mutates the traversal stack and returns no value.
338    pub(crate) fn leave(&mut self) {
339        self.visiting.pop();
340    }
341}
342
343/// Validates include paths declared by a source.
344///
345/// # Arguments
346///
347/// - `path`: Source path whose include list is being validated.
348/// - `paths`: Include paths declared by `path`.
349///
350/// # Returns
351///
352/// Returns `Ok(())` when every include path is non-empty.
353pub(crate) fn validate_include_paths(path: &Path, paths: &[PathBuf]) -> Result<()> {
354    for (index, include_path) in paths.iter().enumerate() {
355        if include_path.as_os_str().is_empty() {
356            return Err(ConfigTreeError::EmptyIncludePath {
357                path: path.to_path_buf(),
358                index,
359            });
360        }
361    }
362
363    Ok(())
364}
365
366#[cfg(test)]
367#[path = "unit_tests/tree.rs"]
368mod unit_tests;