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}