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;