Skip to main content

anvil_tera_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_tera::Earth` trait for a struct.
6///
7/// Requires the struct to also derive `serde::Serialize`.
8///
9/// # Attributes
10///
11/// - `#[template(path = "template_name.html", tera = MY_TERA_INSTANCE)]`
12///   - `path`: (Required) The string literal name of the Tera template file.
13///   - `tera`: (Required) The path identifier of the `tera::Tera` instance to use for rendering.
14///       - This instance should typically be a static or lazy_static variable.
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use tera::Tera;
20/// use std::sync::LazyLock;
21/// use serde::Serialize;
22/// use anvil_tera::Earth;
23/// use anvil_tera_derive::Template;
24///
25/// static TEMPLATES: LazyLock<Tera> = LazyLock::new(|| {
26///     let mut tera = Tera::default();
27///     // Assume "greeting.html" exists and contains "Hello {{ name }}!"
28///     tera.add_template_file("templates/greeting.html", Some("greeting.html")).unwrap();
29///     tera
30/// });
31///
32/// #[derive(Serialize, Template)]
33/// #[template(path = "greeting.html", tera = TEMPLATES)]
34/// struct Greeting {
35///     name: String,
36/// }
37///
38/// // The macro expands to:
39/// /*
40/// impl ::anvil_tera::Earth for Greeting {
41///     fn tera(&self, writer: &mut (impl std::io::Write + ?Sized)) -> ::tera::Result<()> {
42///         let context = ::tera::Context::from_serialize(self)
43///             .map_err(|e| ::tera::Error::chain(e, "Failed to serialize context for Tera template"))?;
44///         TEMPLATES.render_to("greeting.html", &context, writer)
45///     }
46/// }
47/// */
48/// ```
49#[proc_macro_derive(Template, attributes(template))]
50pub fn derive_template(input: TokenStream) -> TokenStream {
51    let input = parse_macro_input!(input as DeriveInput);
52
53    // Extract the name of the struct
54    let name = &input.ident;
55
56    // Find the template attribute arguments
57    let (template_name, tera_instance) = match extract_template_attributes(&input) {
58        Ok(attrs) => attrs,
59        Err(err) => return err.to_compile_error().into(),
60    };
61
62    // Generate the implementation
63    let expanded = quote! {
64        impl ::anvil_tera::Earth for #name {
65            fn tera(&self, writer: &mut (impl ::std::io::Write + ?Sized)) -> ::tera::Result<()> {
66                let context = ::tera::Context::from_serialize(self)?;
67                // Use the extracted tera instance expression
68                #tera_instance.render_to(#template_name, &context, writer)
69            }
70        }
71    };
72
73    TokenStream::from(expanded)
74}
75
76/// Extracts the template path and tera instance expression from the attributes of a struct.
77/// Expects the format #[template(path = "template_name.ext", tera = TERA_INSTANCE_EXPR)]
78fn extract_template_attributes(input: &DeriveInput) -> Result<(String, Expr), Error> {
79    let mut template_path = None;
80    let mut tera_instance = None;
81
82    for attr in &input.attrs {
83        if attr.path().is_ident("template") {
84            // Expecting #[template(key = value, key = value)]
85            if let Meta::List(_meta_list) = &attr.meta {
86                // Use parse_nested_meta for robust parsing of key = value pairs
87                attr.parse_nested_meta(|meta| {
88                    if meta.path.is_ident("path") {
89                        // Collapsed nested if let
90                        if let Ok(Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. })) = meta.value()?.parse::<Expr>() {
91                            if template_path.is_some() {
92                                return Err(meta.error("Duplicate 'path' attribute"));
93                            }
94                            template_path = Some(lit_str.value());
95                            return Ok(());
96                        }
97                        Err(meta.error("Expected a string literal for 'path' attribute"))
98                    } else if meta.path.is_ident("tera") {
99                        if let Ok(expr) = meta.value()?.parse::<Expr>() {
100                             if tera_instance.is_some() {
101                                 return Err(meta.error("Duplicate 'tera' attribute"));
102                             }
103                             tera_instance = Some(expr);
104                             return Ok(());
105                         }
106                         Err(meta.error("Expected an expression for 'tera' attribute"))
107                    } else {
108                        Err(meta.error("Unsupported attribute key inside #[template(...)]. Expected 'path' or 'tera'."))
109                    }
110                })?;
111            } else {
112                return Err(Error::new_spanned(
113                    attr,
114                    "Expected #[template(...)] attribute list format, e.g., #[template(path = \"...\")]",
115                ));
116            }
117            // Found the #[template] attribute, no need to check others
118            break;
119        }
120    }
121
122    match (template_path, tera_instance) {
123        (Some(path), Some(tera)) => Ok((path, tera)),
124        (None, _) => Err(Error::new_spanned(
125            input,
126            "Missing 'path' attribute within #[template(...)]. Example: #[template(path = \"my_template.html\", ...)]",
127        )),
128        (_, None) => Err(Error::new_spanned(
129            input,
130            "Missing 'tera' attribute within #[template(...)]. Example: #[template(..., tera = MY_TERA_INSTANCE)]",
131        )),
132    }
133}