use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Error, Expr, ExprLit, Lit, Meta};
#[proc_macro_derive(Template, attributes(template))]
pub fn derive_template(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (template_name, tera_instance) = match extract_template_attributes(&input) {
Ok(attrs) => attrs,
Err(err) => return err.to_compile_error().into(),
};
let expanded = quote! {
impl ::anvil_tera::Earth for #name {
fn tera(&self, writer: &mut (impl ::std::io::Write + ?Sized)) -> ::tera::Result<()> {
let context = ::tera::Context::from_serialize(self)?;
#tera_instance.render_to(#template_name, &context, writer)
}
}
};
TokenStream::from(expanded)
}
fn extract_template_attributes(input: &DeriveInput) -> Result<(String, Expr), Error> {
let mut template_path = None;
let mut tera_instance = None;
for attr in &input.attrs {
if attr.path().is_ident("template") {
if let Meta::List(_meta_list) = &attr.meta {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("path") {
if let Ok(Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. })) = meta.value()?.parse::<Expr>() {
if template_path.is_some() {
return Err(meta.error("Duplicate 'path' attribute"));
}
template_path = Some(lit_str.value());
return Ok(());
}
Err(meta.error("Expected a string literal for 'path' attribute"))
} else if meta.path.is_ident("tera") {
if let Ok(expr) = meta.value()?.parse::<Expr>() {
if tera_instance.is_some() {
return Err(meta.error("Duplicate 'tera' attribute"));
}
tera_instance = Some(expr);
return Ok(());
}
Err(meta.error("Expected an expression for 'tera' attribute"))
} else {
Err(meta.error("Unsupported attribute key inside #[template(...)]. Expected 'path' or 'tera'."))
}
})?;
} else {
return Err(Error::new_spanned(
attr,
"Expected #[template(...)] attribute list format, e.g., #[template(path = \"...\")]",
));
}
break;
}
}
match (template_path, tera_instance) {
(Some(path), Some(tera)) => Ok((path, tera)),
(None, _) => Err(Error::new_spanned(
input,
"Missing 'path' attribute within #[template(...)]. Example: #[template(path = \"my_template.html\", ...)]",
)),
(_, None) => Err(Error::new_spanned(
input,
"Missing 'tera' attribute within #[template(...)]. Example: #[template(..., tera = MY_TERA_INSTANCE)]",
)),
}
}