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;