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;