tera_template_macro/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use proc_macro2::Ident;
5use quote::quote;
6use syn::{parse_macro_input, Attribute, DeriveInput, Expr, ExprLit, Lit, LitStr, Meta};
7
8struct Struct<'a> {
9    name: &'a Ident,
10    path: LitStr,
11}
12
13#[inline]
14#[must_use]
15fn is_template_attr(attr: &&Attribute) -> bool {
16    attr.path().is_ident("template")
17}
18
19fn parse_args(input: &Attribute) -> syn::Result<Meta> {
20    input.parse_args().map_err(|_| {
21        syn::Error::new_spanned(
22            input,
23            "Expected attribute argument `path` in parentheses: \
24        `#[template(path = \"index.html\")]`",
25        )
26    })
27}
28
29fn get_attr(input: &DeriveInput) -> syn::Result<&Attribute> {
30    input.attrs.iter().find(is_template_attr).ok_or_else(|| {
31        syn::Error::new_spanned(input, "The #[template(path = \"...\")] is required.")
32    })
33}
34
35fn get_path_name_value(meta: Meta) -> syn::Result<ExprLit> {
36    let Meta::NameValue(nv) = meta else {
37        return Err(syn::Error::new_spanned(
38            meta,
39            "Expected `path = \"...\"`. E.g. `#[template(path = \"index.html\")]`",
40        ));
41    };
42
43    if !nv.path.is_ident("path") {
44        return Err(syn::Error::new_spanned(&nv.path, "Expected `path`."));
45    }
46
47    match nv.value {
48        Expr::Lit(lit) => Ok(lit),
49        _ => Err(syn::Error::new_spanned(
50            nv,
51            "The assignment to `path` must be a string literal.",
52        )),
53    }
54}
55
56fn get_lit_str(lit: ExprLit) -> syn::Result<LitStr> {
57    match lit.lit {
58        Lit::Str(res) => Ok(res),
59        _ => Err(syn::Error::new_spanned(
60            lit,
61            "The assignment to `path` must be a string literal.",
62        )),
63    }
64}
65
66fn get_path_value(input: &Attribute) -> syn::Result<LitStr> {
67    parse_args(input)
68        .and_then(get_path_name_value)
69        .and_then(get_lit_str)
70}
71
72impl<'a> Struct<'a> {
73    fn from_syn(input: &'a DeriveInput) -> syn::Result<Self> {
74        let name = &input.ident;
75        get_attr(input)
76            .and_then(get_path_value)
77            .map(move |path| Self { name, path })
78    }
79}
80
81fn expand_struct(s: Struct) -> proc_macro2::TokenStream {
82    let Struct { name, path } = s;
83
84    quote! {
85        #[automatically_derived]
86        impl #name {
87            fn render(&self, tera: &tera::Tera) -> String {
88                let context = tera::Context::from_serialize(self).expect("Failed to create context");
89                let rendered =  tera
90                    .render(#path, &context)
91                    .expect("Failed to render template");
92                rendered
93            }
94        }
95    }
96}
97
98/// Create a template from some struct
99///
100/// # Attribute
101///
102/// This macro requires the #[template(path = "...")] to be provided, this tells the derived
103/// template where to look for the file it renders.
104///
105/// ## Named Arguments
106///
107/// * path - A string literal ("hello world"), which contains the path to the file which will
108///   be rendered. E.g. path = "index.html".
109///
110/// # Example
111///
112/// 
113/// ```rust
114/// use tera_hot_reload::TeraTemplate;
115/// 
116/// #[derive(serde::Serialize, TeraTemplate)]
117/// #[template(path = "index.html")]
118/// struct MyTemplate {
119///     hello: String,
120///     world: String
121/// }
122/// ```
123#[proc_macro_derive(TeraTemplate, attributes(template))]
124pub fn tera_template_derive(input: TokenStream) -> TokenStream {
125    let input = parse_macro_input!(input as DeriveInput);
126
127    Struct::from_syn(&input)
128        .map_or_else(syn::Error::into_compile_error, expand_struct)
129        .into()
130}