tomplate_build/builder.rs
1use crate::{amalgamator, discovery, types::{Engine, Result}};
2use std::env;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6/// Build mode for template amalgamation.
7///
8/// Determines how the builder handles existing template files in the output directory.
9#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
10pub enum BuildMode {
11 /// Overwrite existing templates (default).
12 ///
13 /// This mode will completely replace any existing amalgamated template file
14 /// with the newly discovered templates.
15 #[default]
16 Overwrite,
17
18 /// Append to existing templates, merging with what's already there.
19 ///
20 /// This mode will merge newly discovered templates with any existing
21 /// amalgamated file. Note: Duplicate template names will cause an error.
22 Append,
23}
24
25/// Builder for discovering and processing template files.
26///
27/// The `Builder` is the main entry point for the build-time template discovery system.
28/// It finds template files matching specified patterns, validates them, and amalgamates
29/// them into a single TOML file for the macro system to use at compile time.
30///
31/// # Examples
32///
33/// ## Basic Usage
34///
35/// ```rust,ignore
36/// fn main() {
37/// tomplate_build::Builder::new()
38/// .add_pattern("**/*.tomplate.toml")
39/// .build()
40/// .expect("Failed to build templates");
41/// }
42/// ```
43///
44/// ## Advanced Configuration
45///
46/// ```rust,ignore
47/// use tomplate_build::{Builder, Engine};
48///
49/// fn main() {
50/// Builder::new()
51/// .add_patterns([
52/// "templates/*.toml",
53/// "sql/**/*.tomplate.toml",
54/// "config/*.toml"
55/// ])
56/// .default_engine(Engine::Handlebars)
57/// .build()
58/// .expect("Failed to build templates");
59/// }
60/// ```
61#[derive(Default)]
62pub struct Builder {
63 patterns: Vec<String>,
64 output_dir: Option<PathBuf>,
65 mode: BuildMode,
66 default_engine: Option<Engine>,
67}
68
69impl Builder {
70 /// Creates a new `Builder` with default settings.
71 ///
72 /// # Examples
73 ///
74 /// ```rust,ignore
75 /// use tomplate_build::Builder;
76 ///
77 /// let builder = Builder::new();
78 /// ```
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 /// Adds a single glob pattern for discovering template files.
84 ///
85 /// The pattern follows standard glob syntax:
86 /// - `*` matches any sequence of characters within a path segment
87 /// - `**` matches zero or more path segments
88 /// - `?` matches any single character
89 /// - `[...]` matches any character within the brackets
90 ///
91 /// # Examples
92 ///
93 /// ```rust,ignore
94 /// Builder::new()
95 /// .add_pattern("templates/*.toml")
96 /// .add_pattern("**/*.tomplate.toml")
97 /// .build()?;
98 /// ```
99 pub fn add_pattern<S: AsRef<str>>(mut self, pattern: S) -> Self {
100 self.patterns.push(pattern.as_ref().to_string());
101 self
102 }
103
104 /// Adds multiple glob patterns for discovering template files.
105 ///
106 /// This is a convenience method for adding multiple patterns at once.
107 /// It accepts any iterator of string-like items.
108 ///
109 /// # Examples
110 ///
111 /// ```rust,ignore
112 /// // Using an array
113 /// Builder::new()
114 /// .add_patterns(["*.toml", "templates/*.toml"])
115 /// .build()?;
116 ///
117 /// // Using a vector
118 /// let patterns = vec!["sql/*.toml", "queries/*.toml"];
119 /// Builder::new()
120 /// .add_patterns(patterns)
121 /// .build()?;
122 ///
123 /// // Using an iterator
124 /// let patterns = (1..=3).map(|i| format!("v{}/**.toml", i));
125 /// Builder::new()
126 /// .add_patterns(patterns)
127 /// .build()?;
128 /// ```
129 pub fn add_patterns<I, S>(mut self, patterns: I) -> Self
130 where
131 I: IntoIterator<Item = S>,
132 S: AsRef<str>,
133 {
134 self.patterns.extend(patterns.into_iter().map(|s| s.as_ref().to_string()));
135 self
136 }
137
138 /// Sets a custom output directory for the amalgamated template file.
139 ///
140 /// By default, the builder uses the `OUT_DIR` environment variable set by Cargo.
141 /// This method allows overriding that behavior for special use cases.
142 ///
143 /// # Examples
144 ///
145 /// ```rust,ignore
146 /// Builder::new()
147 /// .add_pattern("**/*.toml")
148 /// .output_dir("target/templates")
149 /// .build()?;
150 /// ```
151 pub fn output_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
152 self.output_dir = Some(dir.as_ref().to_path_buf());
153 self
154 }
155
156 /// Sets the build mode for template amalgamation.
157 ///
158 /// See [`BuildMode`] for available modes and their behavior.
159 ///
160 /// # Examples
161 ///
162 /// ```rust,ignore
163 /// use tomplate_build::{Builder, BuildMode};
164 ///
165 /// Builder::new()
166 /// .add_pattern("**/*.toml")
167 /// .mode(BuildMode::Append)
168 /// .build()?;
169 /// ```
170 pub fn mode(mut self, mode: BuildMode) -> Self {
171 self.mode = mode;
172 self
173 }
174
175 /// Sets a default template engine for templates without an explicit engine.
176 ///
177 /// When a template in a TOML file doesn't specify an `engine` field,
178 /// this default will be used. If no default is set, "simple" is used.
179 ///
180 /// # Examples
181 ///
182 /// ```rust,ignore
183 /// use tomplate_build::{Builder, Engine};
184 ///
185 /// Builder::new()
186 /// .add_pattern("**/*.toml")
187 /// .default_engine(Engine::Handlebars)
188 /// .build()?;
189 /// ```
190 ///
191 /// With this configuration, any template like:
192 /// ```toml
193 /// [my_template]
194 /// template = "Hello {{name}}"
195 /// # No engine specified
196 /// ```
197 /// Will use Handlebars instead of the default simple engine.
198 pub fn default_engine(mut self, engine: Engine) -> Self {
199 self.default_engine = Some(engine);
200 self
201 }
202
203 /// Builds and processes all discovered templates.
204 ///
205 /// This method:
206 /// 1. Discovers all template files matching the configured patterns
207 /// 2. Parses and validates the TOML files
208 /// 3. Applies the default engine if configured
209 /// 4. Checks for duplicate template names
210 /// 5. Amalgamates all templates into a single TOML file
211 /// 6. Writes the result to `OUT_DIR/tomplate_amalgamated.toml`
212 ///
213 /// # Errors
214 ///
215 /// Returns an error if:
216 /// - No output directory is configured and `OUT_DIR` is not set
217 /// - Template files contain invalid TOML
218 /// - Duplicate template names are found
219 /// - File I/O operations fail
220 ///
221 /// # Examples
222 ///
223 /// ```rust,ignore
224 /// fn main() {
225 /// if let Err(e) = Builder::new()
226 /// .add_pattern("**/*.tomplate.toml")
227 /// .build()
228 /// {
229 /// eprintln!("Build failed: {}", e);
230 /// std::process::exit(1);
231 /// }
232 /// }
233 /// ```
234 pub fn build(self) -> Result<()> {
235 let out_dir = self
236 .output_dir
237 .or_else(|| env::var_os("OUT_DIR").map(PathBuf::from))
238 .expect("OUT_DIR not set and no output_dir specified");
239
240 // Tell Cargo to rerun if any tomplate files change
241 for pattern in &self.patterns {
242 println!("cargo:rerun-if-changed={}", pattern);
243 }
244
245 // Discover all template files
246 let template_files = discovery::discover_templates(&self.patterns)?;
247
248 if template_files.is_empty() {
249 // No templates found, create empty constants
250 Self::write_empty_templates(&out_dir)?;
251 return Ok(());
252 }
253
254 // Amalgamate all templates into a single TOML structure
255 let amalgamated = amalgamator::amalgamate_templates(&template_files, self.default_engine)?;
256
257 // Write the amalgamated TOML file
258 let toml_path = out_dir.join("tomplate_amalgamated.toml");
259 fs::write(&toml_path, &amalgamated)?;
260
261 println!(
262 "cargo:rustc-env=TOMPLATE_TEMPLATES_PATH={}",
263 toml_path.display()
264 );
265
266 Ok(())
267 }
268
269 fn write_empty_templates(out_dir: &Path) -> Result<()> {
270 // Write empty TOML file
271 let toml_path = out_dir.join("tomplate_amalgamated.toml");
272 fs::write(&toml_path, "")?;
273
274 Ok(())
275 }
276}