Skip to main content

rust_config_tree/
template.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
25impl TemplateTarget {
26    /// Returns the config source path used to discover this target's includes.
27    ///
28    /// # Returns
29    ///
30    /// Returns the source path for this template target.
31    pub fn source_path(&self) -> &Path {
32        &self.source_path
33    }
34
35    /// Returns the output path that should receive this target's template.
36    ///
37    /// # Returns
38    ///
39    /// Returns the output path for this template target.
40    pub fn target_path(&self) -> &Path {
41        &self.target_path
42    }
43
44    /// Returns include paths declared by this source target.
45    ///
46    /// # Returns
47    ///
48    /// Returns the include paths declared by the target source.
49    pub fn include_paths(&self) -> &[PathBuf] {
50        &self.include_paths
51    }
52
53    /// Decomposes the target into its source path, target path, and include paths.
54    ///
55    /// # Returns
56    ///
57    /// Returns `(source_path, target_path, include_paths)`.
58    pub fn into_parts(self) -> (PathBuf, PathBuf, Vec<PathBuf>) {
59        (self.source_path, self.target_path, self.include_paths)
60    }
61}
62
63/// Chooses the source file used when generating templates.
64///
65/// Existing config files are preferred. If the config file does not exist, an
66/// existing output template is used as the source. If neither exists, the output
67/// path is returned so generation can start from an empty template tree.
68///
69/// # Arguments
70///
71/// - `config_path`: Preferred config source path.
72/// - `output_path`: Output template path used as the fallback source.
73///
74/// # Returns
75///
76/// Returns the path that should be used as the root template source.
77pub fn select_template_source(
78    config_path: impl AsRef<Path>,
79    output_path: impl AsRef<Path>,
80) -> PathBuf {
81    let config_path = config_path.as_ref();
82    let output_path = output_path.as_ref();
83
84    if config_path.exists() {
85        return config_path.to_path_buf();
86    }
87
88    if output_path.exists() {
89        return output_path.to_path_buf();
90    }
91
92    output_path.to_path_buf()
93}
94
95/// Collects template targets by recursively following include paths.
96///
97/// `read_includes` receives each absolute source path and returns the include
98/// paths declared by that source. Relative include paths are resolved from the
99/// source file and mirrored under the output file's parent directory. Absolute
100/// include paths remain absolute targets. The callback is also called for source
101/// paths that do not exist yet, so callers can treat missing template sources as
102/// empty or synthesize default includes.
103///
104/// # Type Parameters
105///
106/// - `E`: Error type returned by `read_includes`.
107/// - `F`: Include reader callback type.
108///
109/// # Arguments
110///
111/// - `config_path`: Preferred config source path.
112/// - `output_path`: Root output template path.
113/// - `read_includes`: Callback that receives each normalized source path and
114///   returns include paths declared by that source.
115///
116/// # Returns
117///
118/// Returns all collected template targets in traversal order.
119pub fn collect_template_targets<E, F>(
120    config_path: impl AsRef<Path>,
121    output_path: impl AsRef<Path>,
122    mut read_includes: F,
123) -> Result<Vec<TemplateTarget>>
124where
125    E: Into<BoxError>,
126    F: FnMut(&Path) -> std::result::Result<Vec<PathBuf>, E>,
127{
128    let source_path = select_template_source(config_path, output_path.as_ref());
129    let mut state = TraversalState::default();
130    let mut targets = Vec::new();
131    collect_template_target(
132        &source_path,
133        output_path.as_ref(),
134        &mut read_includes,
135        &mut state,
136        &mut targets,
137    )?;
138    Ok(targets)
139}
140
141fn collect_template_target<E, F>(
142    source_path: &Path,
143    target_path: &Path,
144    read_includes: &mut F,
145    state: &mut TraversalState,
146    targets: &mut Vec<TemplateTarget>,
147) -> Result<()>
148where
149    E: Into<BoxError>,
150    F: FnMut(&Path) -> std::result::Result<Vec<PathBuf>, E>,
151{
152    let source_path = absolutize_lexical(source_path)?;
153    if !state.enter(&source_path)? {
154        return Ok(());
155    }
156
157    let include_paths = read_includes(&source_path)
158        .map_err(|source| crate::ConfigTreeError::load(&source_path, source))?;
159    validate_include_paths(&source_path, &include_paths)?;
160
161    targets.push(TemplateTarget {
162        source_path: source_path.clone(),
163        target_path: target_path.to_path_buf(),
164        include_paths: include_paths.clone(),
165    });
166
167    let target_base_dir = target_path.parent().unwrap_or_else(|| Path::new("."));
168    for include_path in &include_paths {
169        let source_child = resolve_include_path(&source_path, include_path);
170        let target_child = if include_path.is_absolute() {
171            include_path.clone()
172        } else {
173            target_base_dir.join(include_path)
174        };
175        collect_template_target(&source_child, &target_child, read_includes, state, targets)?;
176    }
177
178    state.leave();
179    Ok(())
180}
181
182#[cfg(test)]
183#[path = "unit_tests/template.rs"]
184mod unit_tests;