Skip to main content

rust_config_tree/
template_tree.rs

1//! Low-level template target discovery.
2//!
3//! This module maps source config files to output template files by following
4//! include paths. It does not render template content; callers provide include
5//! discovery and decide how each target should be rendered.
6
7use std::path::{Path, PathBuf};
8
9use crate::{
10    BoxError, Result, absolutize_lexical, resolve_include_path,
11    tree::{TraversalState, validate_include_paths},
12};
13
14/// A source-to-output mapping for one generated config template.
15///
16/// The source path is used to discover includes. The target path is the output
17/// file that should receive the rendered template content.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct TemplateTarget {
20    source_path: PathBuf,
21    target_path: PathBuf,
22    include_paths: Vec<PathBuf>,
23}
24
25/// Accessors for generated template target metadata.
26impl TemplateTarget {
27    /// Returns the config source path used to discover this target's includes.
28    ///
29    /// # Arguments
30    ///
31    /// - `self`: Template target being inspected.
32    ///
33    /// # Returns
34    ///
35    /// Returns the source path for this template target.
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use std::{io, path::{Path, PathBuf}};
41    /// use rust_config_tree::collect_template_targets;
42    ///
43    /// let targets = collect_template_targets(
44    ///     "config.yaml",
45    ///     "config.example.yaml",
46    ///     |_path: &Path| -> io::Result<Vec<PathBuf>> { Ok(Vec::new()) },
47    /// )?;
48    ///
49    /// assert!(targets[0].source_path().ends_with("config.example.yaml"));
50    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
51    /// ```
52    pub fn source_path(&self) -> &Path {
53        &self.source_path
54    }
55
56    /// Returns the output path that should receive this target's template.
57    ///
58    /// # Arguments
59    ///
60    /// - `self`: Template target being inspected.
61    ///
62    /// # Returns
63    ///
64    /// Returns the output path for this template target.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use std::{io, path::{Path, PathBuf}};
70    /// use rust_config_tree::collect_template_targets;
71    ///
72    /// let targets = collect_template_targets(
73    ///     "config.yaml",
74    ///     "config.example.yaml",
75    ///     |_path: &Path| -> io::Result<Vec<PathBuf>> { Ok(Vec::new()) },
76    /// )?;
77    ///
78    /// assert_eq!(targets[0].target_path(), Path::new("config.example.yaml"));
79    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
80    /// ```
81    pub fn target_path(&self) -> &Path {
82        &self.target_path
83    }
84
85    /// Returns include paths declared by this source target.
86    ///
87    /// # Arguments
88    ///
89    /// - `self`: Template target being inspected.
90    ///
91    /// # Returns
92    ///
93    /// Returns the include paths declared by the target source.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use std::{io, path::{Path, PathBuf}};
99    /// use rust_config_tree::collect_template_targets;
100    ///
101    /// let targets = collect_template_targets(
102    ///     "config.yaml",
103    ///     "config.example.yaml",
104    ///     |path: &Path| -> io::Result<Vec<PathBuf>> {
105    ///         if path.ends_with("config.example.yaml") {
106    ///             Ok(vec![PathBuf::from("child.yaml")])
107    ///         } else {
108    ///             Ok(Vec::new())
109    ///         }
110    ///     },
111    /// )?;
112    ///
113    /// assert_eq!(targets[0].include_paths(), &[PathBuf::from("child.yaml")]);
114    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
115    /// ```
116    pub fn include_paths(&self) -> &[PathBuf] {
117        &self.include_paths
118    }
119
120    /// Decomposes the target into its source path, target path, and include paths.
121    ///
122    /// # Arguments
123    ///
124    /// - `self`: Template target to decompose.
125    ///
126    /// # Returns
127    ///
128    /// Returns `(source_path, target_path, include_paths)`.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use std::{io, path::{Path, PathBuf}};
134    /// use rust_config_tree::collect_template_targets;
135    ///
136    /// let mut targets = collect_template_targets(
137    ///     "config.yaml",
138    ///     "config.example.yaml",
139    ///     |_path: &Path| -> io::Result<Vec<PathBuf>> { Ok(Vec::new()) },
140    /// )?;
141    ///
142    /// let (_source_path, target_path, include_paths) = targets.remove(0).into_parts();
143    /// assert_eq!(target_path, PathBuf::from("config.example.yaml"));
144    /// assert!(include_paths.is_empty());
145    /// # Ok::<(), rust_config_tree::ConfigTreeError>(())
146    /// ```
147    pub fn into_parts(self) -> (PathBuf, PathBuf, Vec<PathBuf>) {
148        (self.source_path, self.target_path, self.include_paths)
149    }
150}
151
152/// Chooses the source file used when generating templates.
153///
154/// Existing config files are preferred. If the config file does not exist, an
155/// existing output template is used as the source. If neither exists, the output
156/// path is returned so generation can start from an empty template tree.
157///
158/// # Arguments
159///
160/// - `config_path`: Preferred config source path.
161/// - `output_path`: Output template path used as the fallback source.
162///
163/// # Returns
164///
165/// Returns the path that should be used as the root template source.
166///
167/// # Examples
168///
169/// ```
170/// use std::path::PathBuf;
171/// use rust_config_tree::select_template_source;
172///
173/// let source = select_template_source("missing-config.yaml", "config.example.yaml");
174///
175/// assert_eq!(source, PathBuf::from("config.example.yaml"));
176/// ```
177pub fn select_template_source(
178    config_path: impl AsRef<Path>,
179    output_path: impl AsRef<Path>,
180) -> PathBuf {
181    let config_path = config_path.as_ref();
182    let output_path = output_path.as_ref();
183
184    if config_path.exists() {
185        return config_path.to_path_buf();
186    }
187
188    if output_path.exists() {
189        return output_path.to_path_buf();
190    }
191
192    output_path.to_path_buf()
193}
194
195/// Collects template targets by recursively following include paths.
196///
197/// `read_includes` receives each absolute source path and returns the include
198/// paths declared by that source. Relative include paths are resolved from the
199/// source file and mirrored under the output file's parent directory. Absolute
200/// include paths remain absolute targets. The callback is also called for source
201/// paths that do not exist yet, so callers can treat missing template sources as
202/// empty or synthesize default includes.
203///
204/// # Type Parameters
205///
206/// - `E`: Error type returned by `read_includes`.
207/// - `F`: Include reader callback type.
208///
209/// # Arguments
210///
211/// - `config_path`: Preferred config source path.
212/// - `output_path`: Root output template path.
213/// - `read_includes`: Callback that receives each normalized source path and
214///   returns include paths declared by that source.
215///
216/// # Returns
217///
218/// Returns all collected template targets in traversal order.
219///
220/// # Examples
221///
222/// ```
223/// use std::{io, path::{Path, PathBuf}};
224/// use rust_config_tree::collect_template_targets;
225///
226/// let targets = collect_template_targets(
227///     "config.yaml",
228///     "examples/config.example.yaml",
229///     |path: &Path| -> io::Result<Vec<PathBuf>> {
230///         if path.ends_with("config.example.yaml") {
231///             Ok(vec![PathBuf::from("child.yaml")])
232///         } else {
233///             Ok(Vec::new())
234///         }
235///     },
236/// )?;
237///
238/// assert_eq!(targets.len(), 2);
239/// assert_eq!(targets[1].target_path(), Path::new("examples/child.yaml"));
240/// # Ok::<(), rust_config_tree::ConfigTreeError>(())
241/// ```
242pub fn collect_template_targets<E, F>(
243    config_path: impl AsRef<Path>,
244    output_path: impl AsRef<Path>,
245    mut read_includes: F,
246) -> Result<Vec<TemplateTarget>>
247where
248    E: Into<BoxError>,
249    F: FnMut(&Path) -> std::result::Result<Vec<PathBuf>, E>,
250{
251    let source_path = select_template_source(config_path, output_path.as_ref());
252    let mut state = TraversalState::default();
253    let mut targets = Vec::new();
254    collect_template_target(
255        &source_path,
256        output_path.as_ref(),
257        &mut read_includes,
258        &mut state,
259        &mut targets,
260    )?;
261    Ok(targets)
262}
263
264/// Recursively maps one source template path to one output template path.
265///
266/// # Type Parameters
267///
268/// - `E`: Error type returned by `read_includes`.
269/// - `F`: Include reader callback type.
270///
271/// # Arguments
272///
273/// - `source_path`: Source template path used to read includes.
274/// - `target_path`: Output path mapped to `source_path`.
275/// - `read_includes`: Callback that reads include paths from a source path.
276/// - `state`: Traversal state used for cycle detection and deduplication.
277/// - `targets`: Output list receiving collected template targets.
278///
279/// # Returns
280///
281/// Returns `Ok(())` after this target and its children are collected.
282///
283/// # Examples
284///
285/// ```no_run
286/// let _ = ();
287/// ```
288fn collect_template_target<E, F>(
289    source_path: &Path,
290    target_path: &Path,
291    read_includes: &mut F,
292    state: &mut TraversalState,
293    targets: &mut Vec<TemplateTarget>,
294) -> Result<()>
295where
296    E: Into<BoxError>,
297    F: FnMut(&Path) -> std::result::Result<Vec<PathBuf>, E>,
298{
299    let source_path = absolutize_lexical(source_path)?;
300    if !state.enter(&source_path)? {
301        return Ok(());
302    }
303
304    let include_paths = read_includes(&source_path)
305        .map_err(|source| crate::ConfigTreeError::load(&source_path, source))?;
306    validate_include_paths(&source_path, &include_paths)?;
307
308    targets.push(TemplateTarget {
309        source_path: source_path.clone(),
310        target_path: target_path.to_path_buf(),
311        include_paths: include_paths.clone(),
312    });
313
314    let target_base_dir = target_path.parent().unwrap_or_else(|| Path::new("."));
315    for include_path in &include_paths {
316        let source_child = resolve_include_path(&source_path, include_path);
317        let target_child = if include_path.is_absolute() {
318            include_path.clone()
319        } else {
320            target_base_dir.join(include_path)
321        };
322        collect_template_target(&source_child, &target_child, read_includes, state, targets)?;
323    }
324
325    state.leave();
326    Ok(())
327}
328
329#[cfg(test)]
330#[path = "unit_tests/template.rs"]
331mod unit_tests;