Skip to main content

anvil_liquid_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, DeriveInput, Error, Expr, ExprLit, Lit, Meta};
4
5/// Derives the `anvil_liquid::Water` trait for a struct.
6///
7/// Requires the struct to also derive `serde::Serialize`.
8///
9/// # Attributes
10///
11/// This macro supports three formats:
12///
13/// 1. `#[template("path/to/template")]` - Uses the default parser.
14/// 2. `#[template(path = "path/to/template")]` - Uses the default parser.
15/// 3. `#[template(path = "path/to/template", parser = PARSER)]` - Uses the specified parser.
16///
17/// Template paths are relative to the `CARGO_MANIFEST_DIR`, which is the directory containing
18/// the `Cargo.toml` file of your project.
19///
20/// # Examples
21///
22/// ```rust,ignore
23/// use serde::Serialize;
24/// use anvil_liquid::Water;
25/// use anvil_liquid_derive::Template;
26///
27/// // Example 1: Using the default parser
28/// #[derive(Serialize, Template)]
29/// #[template("templates/greeting.liquid")]
30/// struct Greeting {
31///     name: String,
32/// }
33///
34/// // Example 2: Using the default parser with key-value syntax
35/// #[derive(Serialize, Template)]
36/// #[template(path = "templates/greeting.liquid")]
37/// struct AnotherGreeting {
38///     name: String,
39/// }
40///
41/// // Example 3: Using a custom parser
42/// use std::sync::LazyLock;
43/// use liquid::ParserBuilder;
44///
45/// static PARSER: LazyLock<liquid::Parser> =
46///     LazyLock::new(|| ParserBuilder::with_stdlib().build().unwrap());
47///
48/// #[derive(Serialize, Template)]
49/// #[template(path = "templates/greeting.liquid", parser = PARSER)]
50/// struct CustomGreeting {
51///     name: String,
52/// }
53/// ```
54#[proc_macro_derive(Template, attributes(template))]
55pub fn derive_template(input: TokenStream) -> TokenStream {
56    let input = parse_macro_input!(input as DeriveInput);
57
58    // Extract the name of the struct
59    let name = &input.ident;
60
61    // Find the template attribute arguments
62    let (template_path, parser) = match extract_template_attributes(&input) {
63        Ok(attrs) => attrs,
64        Err(err) => return err.to_compile_error().into(),
65    };
66
67    // Generate a unique template name based on the struct name
68    let template_name = format!("_LIQUID_TEMPLATE_{}", name).to_uppercase();
69    let template_ident = syn::Ident::new(&template_name, name.span());
70
71    let include_str_relative_to_manifest_dir = quote! {
72        include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #template_path))
73    };
74
75    // Generate the static template initialization
76    let template_init = if let Some(parser) = parser {
77        quote! {
78            static #template_ident: ::std::sync::LazyLock<::liquid::Template> =
79                ::std::sync::LazyLock::new(|| #parser.parse(#include_str_relative_to_manifest_dir).unwrap());
80        }
81    } else {
82        quote! {
83            static #template_ident: ::std::sync::LazyLock<::liquid::Template> =
84                ::std::sync::LazyLock::new(|| ::liquid::ParserBuilder::with_stdlib().build().unwrap().parse(#include_str_relative_to_manifest_dir).unwrap());
85        }
86    };
87
88    // Generate the Water trait implementation
89    let water_impl = quote! {
90        impl ::anvil_liquid::Water for #name {
91            fn liquid(&self, writer: &mut dyn ::std::io::Write) -> ::std::result::Result<(), ::liquid::Error> {
92                let object = ::liquid::to_object(self)?;
93                #template_ident.render_to(writer, &object)
94            }
95        }
96    };
97
98    // Combine the template initialization and the trait implementation
99    let expanded = quote! {
100        #template_init
101        #water_impl
102    };
103
104    TokenStream::from(expanded)
105}
106
107/// Extracts the template path and optional parser expression from the attributes of a struct.
108/// Supports three formats:
109/// 1. #[template("path/to/template")]
110/// 2. #[template(path = "path/to/template")]
111/// 3. #[template(path = "path/to/template", parser = PARSER)]
112fn extract_template_attributes(input: &DeriveInput) -> Result<(String, Option<Expr>), Error> {
113    for attr in &input.attrs {
114        if attr.path().is_ident("template") {
115            // Try to parse as simple string literal with optional parser: #[template("path/to/template", parser = PARSER)]
116            if let Ok(expr) = attr.parse_args::<Expr>() {
117                if let Expr::Lit(ExprLit {
118                    lit: Lit::Str(lit_str),
119                    ..
120                }) = &expr
121                {
122                    return Ok((lit_str.value(), None));
123                } else if let Expr::Tuple(expr_tuple) = &expr {
124                    // Process tuple format: #[template("path/to/template", parser = PARSER)]
125                    if !expr_tuple.elems.is_empty() {
126                        if let Some(Expr::Lit(ExprLit {
127                            lit: Lit::Str(lit_str),
128                            ..
129                        })) = expr_tuple.elems.first()
130                        {
131                            let mut parser = None;
132
133                            // Look for parser = PARSER in the remaining elements
134                            for elem in expr_tuple.elems.iter().skip(1) {
135                                if let Expr::Assign(assign) = elem {
136                                    if let Expr::Path(path) = &*assign.left {
137                                        if path.path.is_ident("parser") {
138                                            parser = Some(*assign.right.clone());
139                                            break;
140                                        }
141                                    }
142                                }
143                            }
144
145                            return Ok((lit_str.value(), parser));
146                        }
147                    }
148                }
149            }
150
151            // Try to parse as key-value pairs: #[template(path = "...", parser = ...)]
152            if let Meta::List(_) = &attr.meta {
153                let mut template_path = None;
154                let mut parser_expr = None;
155
156                attr.parse_nested_meta(|meta| {
157                    if meta.path.is_ident("path") {
158                        if let Ok(Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. })) = meta.value()?.parse::<Expr>() {
159                            if template_path.is_some() {
160                                return Err(meta.error("Duplicate 'path' attribute"));
161                            }
162                            template_path = Some(lit_str.value());
163                            return Ok(());
164                        }
165                        Err(meta.error("Expected a string literal for 'path' attribute"))
166                    } else if meta.path.is_ident("parser") {
167                        if let Ok(expr) = meta.value()?.parse::<Expr>() {
168                            if parser_expr.is_some() {
169                                return Err(meta.error("Duplicate 'parser' attribute"));
170                            }
171                            parser_expr = Some(expr);
172                            return Ok(());
173                        }
174                        Err(meta.error("Expected an expression for 'parser' attribute"))
175                    } else {
176                        Err(meta.error("Unsupported attribute key inside #[template(...)]. Expected 'path' or 'parser'."))
177                    }
178                })?;
179
180                if let Some(path) = template_path {
181                    return Ok((path, parser_expr));
182                }
183            }
184
185            return Err(Error::new_spanned(
186                attr,
187                "Expected template attribute to be in one of these formats:\n\
188                 1. #[template(\"path/to/template\")]\n\
189                 2. #[template(\"path/to/template\", parser = PARSER)]\n\
190                 3. #[template(path = \"path/to/template\")]\n\
191                 4. #[template(path = \"path/to/template\", parser = PARSER)]\n\
192                 \n\
193                 Note: Template paths are relative to CARGO_MANIFEST_DIR",
194            ));
195        }
196    }
197
198    Err(Error::new_spanned(
199        input,
200        "Missing #[template(...)] attribute. Expected one of these formats:\n\
201         1. #[template(\"path/to/template\")]\n\
202         2. #[template(\"path/to/template\", parser = PARSER)]\n\
203         3. #[template(path = \"path/to/template\")]\n\
204         4. #[template(path = \"path/to/template\", parser = PARSER)]\n\
205         \n\
206         Note: Template paths are relative to CARGO_MANIFEST_DIR",
207    ))
208}