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;