tomplate_macros/lib.rs
1//! # Tomplate Procedural Macros
2//!
3//! This crate provides the procedural macros that power Tomplate's compile-time
4//! template processing. These macros expand at compile time to produce static strings,
5//! ensuring zero runtime overhead for template processing.
6//!
7//! ## Macros Provided
8//!
9//! ### `tomplate!` - Main Template Macro
10//!
11//! The `tomplate!` macro is the primary interface for template processing. It supports
12//! two distinct modes of operation:
13//!
14//! #### Mode 1: Direct Template Invocation
15//!
16//! Process a single template with parameters:
17//!
18//! ```rust,ignore
19//! // File-based template from registry
20//! const QUERY: &str = tomplate!("user_query",
21//! fields = "id, name",
22//! condition = "active = true"
23//! );
24//!
25//! // Inline template (when not found in registry)
26//! const GREETING: &str = tomplate!("Hello {name}!",
27//! name = "World"
28//! );
29//! ```
30//!
31//! #### Mode 2: Composition Block
32//!
33//! Define multiple templates with local variables:
34//!
35//! ```rust,ignore
36//! tomplate! {
37//! // Local variables (not exported)
38//! let common_fields = tomplate!("id, name, email");
39//! let active_filter = tomplate!("status = 'active'");
40//!
41//! // Exported constants (available outside block)
42//! const USER_QUERY = tomplate!(
43//! "SELECT {fields} FROM users WHERE {filter}",
44//! fields = common_fields,
45//! filter = active_filter
46//! );
47//!
48//! const COUNT_QUERY = tomplate!(
49//! "SELECT COUNT(*) FROM users WHERE {filter}",
50//! filter = active_filter
51//! );
52//! }
53//! ```
54//!
55//! ### `tomplate_eager!` - Eager Macro Expansion
56//!
57//! Eagerly expands nested `tomplate!` and `concat!` macros before passing to outer macros:
58//!
59//! ```rust,ignore
60//! // Problem: sqlx expects a string literal
61//! // This won't work:
62//! // sqlx::query!(tomplate!("select_user", id = "5"))
63//!
64//! // Solution: Use tomplate_eager!
65//! tomplate_eager! {
66//! sqlx::query!(tomplate!("select_user", id = "5"))
67//! .fetch_one(&pool)
68//! .await?
69//! }
70//! ```
71//!
72//! ## How Template Resolution Works
73//!
74//! The `tomplate!` macro uses a two-step resolution process:
75//!
76//! 1. **Registry Lookup**: First checks if the string matches a template name in the
77//! amalgamated template registry (created by `tomplate-build` at build time)
78//!
79//! 2. **Inline Fallback**: If not found in registry, treats the string itself as an
80//! inline template with the simple engine
81//!
82//! This allows seamless mixing of pre-defined and ad-hoc templates:
83//!
84//! ```rust,ignore
85//! // If "header" exists in registry, uses that template
86//! const HEADER: &str = tomplate!("header", title = "My App");
87//!
88//! // If "Welcome {user}!" doesn't exist in registry, uses it as inline template
89//! const WELCOME: &str = tomplate!("Welcome {user}!", user = "Alice");
90//! ```
91//!
92//! ## Parameter Types
93//!
94//! Templates accept various parameter types:
95//!
96//! - **String literals**: `"value"`
97//! - **Numbers**: `42`, `3.14`
98//! - **Booleans**: `true`, `false`
99//! - **Nested templates**: `tomplate!("other_template", ...)`
100//!
101//! ```rust,ignore
102//! const EXAMPLE: &str = tomplate!("template_name",
103//! text = "Hello",
104//! count = 42,
105//! pi = 3.14,
106//! enabled = true,
107//! nested = tomplate!("inner", value = "data")
108//! );
109//! ```
110//!
111//! ## Template Engines
112//!
113//! Templates can use different engines based on the `engine` field in TOML:
114//!
115//! - **simple** (default): Basic `{variable}` substitution
116//! - **handlebars**: Full Handlebars with conditionals, loops, helpers
117//! - **tera**: Jinja2-like with filters and control structures
118//! - **minijinja**: Lightweight Jinja2 implementation
119//!
120//! The engine is determined at build time from the template definition.
121//!
122//! ## Compile-Time Processing
123//!
124//! All template processing happens at compile time:
125//!
126//! 1. Build script discovers and amalgamates templates
127//! 2. Macro reads amalgamated templates at compile time
128//! 3. Templates are processed and expanded to string literals
129//! 4. Final binary contains only static strings
130//!
131//! This ensures zero runtime overhead and compile-time validation of templates.
132
133mod block;
134mod eager;
135mod engines;
136mod parser;
137mod scope;
138mod templates;
139
140use proc_macro::TokenStream;
141use quote::quote;
142use syn::{punctuated::Punctuated, Expr, Lit, Token, ExprMacro};
143
144/// Process templates at compile time with zero runtime overhead.
145///
146/// This macro can be used in two ways:
147///
148/// ## Direct Template Invocation
149///
150/// Process a single template with parameters:
151///
152/// ```rust,ignore
153/// // From template registry (defined in .tomplate.toml files)
154/// const QUERY: &str = tomplate!("user_query",
155/// fields = "id, name, email",
156/// table = "users"
157/// );
158///
159/// // Inline template (when not found in registry)
160/// const MSG: &str = tomplate!("Hello {name}!", name = "World");
161/// ```
162///
163/// ## Composition Block
164///
165/// Define multiple templates with shared local variables:
166///
167/// ```rust,ignore
168/// tomplate! {
169/// // Local variables - reusable within block
170/// let base_fields = tomplate!("id, created_at, updated_at");
171///
172/// // Export constants - available outside block
173/// const USER_FIELDS = tomplate!("{base}, name, email",
174/// base = base_fields
175/// );
176///
177/// const POST_FIELDS = tomplate!("{base}, title, content",
178/// base = base_fields
179/// );
180/// }
181///
182/// // Use the exported constants
183/// println!("{}", USER_FIELDS);
184/// ```
185///
186/// ## Parameters
187///
188/// - First argument: Template name (from registry) or inline template string
189/// - Named parameters: `key = value` pairs for template variables
190/// - Values can be literals or nested `tomplate!` calls
191///
192/// ## Template Resolution
193///
194/// 1. Checks if first argument matches a template name in registry
195/// 2. If found, uses that template with its configured engine
196/// 3. If not found, treats the string as an inline template using simple engine
197///
198/// ## Examples
199///
200/// ```rust,ignore
201/// // Using different parameter types
202/// const EXAMPLE: &str = tomplate!("my_template",
203/// string = "text",
204/// number = 42,
205/// float = 3.14,
206/// boolean = true,
207/// nested = tomplate!("other", value = "data")
208/// );
209///
210/// // Inline templates for quick use
211/// const QUICK: &str = tomplate!(
212/// "User: {name}, Status: {status}",
213/// name = "Alice",
214/// status = "active"
215/// );
216/// ```
217#[proc_macro]
218pub fn tomplate(input: TokenStream) -> TokenStream {
219 // Try to parse as a composition block first
220 let input_clone = input.clone();
221 match syn::parse::<parser::CompositionBlock>(input_clone) {
222 Ok(block) => {
223 // Successfully parsed as a block
224 match block::process_block(block) {
225 Ok(output) => output.into(),
226 Err(err) => err.to_compile_error().into(),
227 }
228 }
229 Err(_block_err) => {
230 // Not a block, try as direct template call
231 match syn::parse::<TomplateInput>(input) {
232 Ok(direct) => {
233 match process_template(direct) {
234 Ok(output) => output.into(),
235 Err(err) => err.to_compile_error().into(),
236 }
237 }
238 Err(direct_err) => {
239 // Failed both parsers, return the direct error as it's more common
240 direct_err.to_compile_error().into()
241 }
242 }
243 }
244 }
245}
246
247struct TomplateInput {
248 template_name: String,
249 params: Vec<(String, ParamValue)>,
250}
251
252enum ParamValue {
253 Literal(String),
254 Macro(ExprMacro),
255}
256
257impl syn::parse::Parse for TomplateInput {
258 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
259 // Parse template name
260 let template_name = match input.parse::<Expr>()? {
261 Expr::Lit(lit) => match lit.lit {
262 Lit::Str(s) => s.value(),
263 _ => return Err(syn::Error::new_spanned(lit, "Expected string literal")),
264 },
265 _ => return Err(input.error("Expected template name as string literal")),
266 };
267
268 let mut params = Vec::new();
269
270 // Parse optional parameters
271 if input.peek(Token![,]) {
272 input.parse::<Token![,]>()?;
273
274 let args = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
275
276 for arg in args {
277 match arg {
278 Expr::Assign(assign) => {
279 // Extract parameter name
280 let param_name = match &*assign.left {
281 Expr::Path(path) if path.path.segments.len() == 1 => {
282 path.path.segments[0].ident.to_string()
283 }
284 _ => {
285 return Err(syn::Error::new_spanned(
286 assign.left,
287 "Expected simple identifier",
288 ))
289 }
290 };
291
292 // Extract parameter value (literal or macro)
293 let param_value = match &*assign.right {
294 Expr::Lit(lit) => match &lit.lit {
295 Lit::Str(s) => ParamValue::Literal(s.value()),
296 Lit::Int(i) => ParamValue::Literal(i.to_string()),
297 Lit::Float(f) => ParamValue::Literal(f.to_string()),
298 Lit::Bool(b) => ParamValue::Literal(b.value.to_string()),
299 _ => {
300 return Err(syn::Error::new_spanned(
301 lit,
302 "Unsupported literal type",
303 ))
304 }
305 },
306 Expr::Macro(macro_expr) => {
307 // Check if it's a tomplate! macro call
308 if let Some(ident) = macro_expr.mac.path.get_ident() {
309 if ident == "tomplate" {
310 ParamValue::Macro(macro_expr.clone())
311 } else {
312 return Err(syn::Error::new_spanned(
313 macro_expr,
314 "Only tomplate! macro calls are supported in parameters",
315 ))
316 }
317 } else {
318 return Err(syn::Error::new_spanned(
319 macro_expr,
320 "Expected tomplate! macro call",
321 ))
322 }
323 },
324 _ => {
325 return Err(syn::Error::new_spanned(
326 assign.right,
327 "Expected literal value or tomplate! macro call",
328 ))
329 }
330 };
331
332 params.push((param_name, param_value));
333 }
334 _ => {
335 return Err(syn::Error::new_spanned(
336 arg,
337 "Expected key = value syntax",
338 ))
339 }
340 }
341 }
342 }
343
344 Ok(TomplateInput {
345 template_name,
346 params,
347 })
348 }
349}
350
351fn process_template(input: TomplateInput) -> syn::Result<proc_macro2::TokenStream> {
352 // Get a clone of the cached templates
353 let templates = templates::load_templates();
354
355 // Try to find the template in registry, or use as inline template
356 let (template_string, engine_name) = if let Some(template) = templates.get(&input.template_name) {
357 // Found in registry
358 (template.template.clone(), template.engine.as_deref().unwrap_or("simple"))
359 } else {
360 // Not in registry, treat as inline template
361 (input.template_name.clone(), "simple")
362 };
363
364 // Process parameters, expanding any nested macros
365 let mut params = std::collections::HashMap::new();
366 for (key, value) in input.params {
367 let expanded_value = match value {
368 ParamValue::Literal(s) => s,
369 ParamValue::Macro(macro_expr) => {
370 // Recursively expand the nested tomplate! macro
371 let tokens = macro_expr.mac.tokens.clone();
372 let nested_input = syn::parse2::<TomplateInput>(tokens)?;
373 let nested_result = process_template(nested_input)?;
374
375 // Extract the string literal from the nested result
376 // The nested result is a quote! { "string" }, so we need to extract the string
377 let token_string = nested_result.to_string();
378 // Remove the quotes from the token string
379 token_string.trim_matches('"').to_string()
380 }
381 };
382 params.insert(key, expanded_value);
383 }
384
385 // Process the template with the appropriate engine
386 let processed = engines::process(engine_name, &template_string, ¶ms)
387 .map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e))?;
388
389 // Return the processed template as a string literal
390 Ok(quote! {
391 #processed
392 })
393}
394
395/// Eagerly expand `tomplate!` and `concat!` macros within a token stream.
396///
397/// This macro solves the problem where outer macros expect string literals but
398/// receive unexpanded macro calls. It walks the token tree and expands inner
399/// macros first, allowing seamless integration with other macro systems.
400///
401/// ## Problem It Solves
402///
403/// Many macros (like `sqlx::query!`) require string literals as arguments.
404/// They cannot accept unexpanded macro calls:
405///
406/// ```rust,ignore
407/// // ❌ This fails - sqlx::query! sees the macro call, not the string
408/// sqlx::query!(tomplate!("select_user", id = user_id))
409/// .fetch_one(&pool)
410/// .await?;
411/// ```
412///
413/// ## Solution
414///
415/// `tomplate_eager!` pre-expands the inner macros:
416///
417/// ```rust,ignore
418/// // ✅ This works - tomplate! is expanded first, then passed to sqlx
419/// tomplate_eager! {
420/// sqlx::query!(tomplate!("select_user", id = user_id))
421/// .fetch_one(&pool)
422/// .await?
423/// }
424/// ```
425///
426/// ## Supported Inner Macros
427///
428/// - `tomplate!` - Expands template macros
429/// - `concat!` - Expands string concatenation
430///
431/// ## Examples
432///
433/// ### With SQL Query Builders
434///
435/// ```rust,ignore
436/// // Using with sqlx
437/// tomplate_eager! {
438/// let user = sqlx::query_as!(
439/// User,
440/// tomplate!("get_user_by_id",
441/// fields = "id, name, email",
442/// table = "users"
443/// ),
444/// user_id
445/// )
446/// .fetch_one(&pool)
447/// .await?;
448/// }
449/// ```
450///
451/// ### With String Concatenation
452///
453/// ```rust,ignore
454/// tomplate_eager! {
455/// const FULL_QUERY: &str = concat!(
456/// tomplate!("select_part"),
457/// " UNION ALL ",
458/// tomplate!("select_other_part")
459/// );
460/// }
461/// ```
462///
463/// ### Multiple Expansions
464///
465/// ```rust,ignore
466/// tomplate_eager! {
467/// // Multiple statements with macro expansions
468/// let query1 = sqlx::query!(tomplate!("query1"))
469/// .fetch_all(&pool)
470/// .await?;
471///
472/// let query2 = sqlx::query!(tomplate!("query2"))
473/// .fetch_optional(&pool)
474/// .await?;
475/// }
476/// ```
477///
478/// ## How It Works
479///
480/// 1. Recursively walks through the provided token stream
481/// 2. Finds any `tomplate!` or `concat!` invocations
482/// 3. Evaluates them at compile time
483/// 4. Replaces them with their resulting string literals
484/// 5. Returns the modified token stream
485///
486/// All processing happens at compile time with zero runtime overhead.
487#[proc_macro]
488pub fn tomplate_eager(input: TokenStream) -> TokenStream {
489 let input = proc_macro2::TokenStream::from(input);
490
491 match eager::process_eager(input) {
492 Ok(output) => output.into(),
493 Err(err) => err.to_compile_error().into(),
494 }
495}