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}