anvil_liquid_derive/
lib.rs1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, DeriveInput, Error, Expr, ExprLit, Lit, Meta};
4
5#[proc_macro_derive(Template, attributes(template))]
55pub fn derive_template(input: TokenStream) -> TokenStream {
56 let input = parse_macro_input!(input as DeriveInput);
57
58 let name = &input.ident;
60
61 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 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 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 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 let expanded = quote! {
100 #template_init
101 #water_impl
102 };
103
104 TokenStream::from(expanded)
105}
106
107fn extract_template_attributes(input: &DeriveInput) -> Result<(String, Option<Expr>), Error> {
113 for attr in &input.attrs {
114 if attr.path().is_ident("template") {
115 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 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 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 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}