Skip to main content

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, &params)
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}