pagetop_macros/lib.rs
1/*!
2<div align="center">
3
4<h1>PageTop Macros</h1>
5
6<p>Una colección de macros que mejoran la experiencia de desarrollo con <strong>PageTop</strong>.</p>
7
8[](#-licencia)
9[](https://docs.rs/pagetop-macros)
10[](https://crates.io/crates/pagetop-macros)
11[](https://crates.io/crates/pagetop-macros)
12
13</div>
14
15## Sobre PageTop
16
17[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
18clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
19configurables, basadas en HTML, CSS y JavaScript.
20
21## Créditos
22
23Esta librería incluye entre sus macros una adaptación de
24[maud-macros](https://crates.io/crates/maud_macros)
25([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de
26[Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de
27[SmartDefault](https://crates.io/crates/smart_default) (0.7.1) de
28[Jane Doe](https://crates.io/users/jane-doe), llamada `AutoDefault`. Estas macros eliminan la
29necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de
30cada proyecto PageTop.
31*/
32
33#![doc(
34 html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
35)]
36
37mod maud;
38mod smart_default;
39
40use proc_macro::TokenStream;
41use quote::{quote, quote_spanned};
42use syn::{parse_macro_input, spanned::Spanned, DeriveInput};
43
44/// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)).
45#[proc_macro]
46pub fn html(input: TokenStream) -> TokenStream {
47 maud::expand(input.into()).into()
48}
49
50/// Deriva [`Default`] con atributos personalizados (basada en
51/// [SmartDefault](https://docs.rs/smart-default)).
52///
53/// Al derivar una estructura con *AutoDefault* se genera automáticamente la implementación de
54/// [`Default`]. Aunque, a diferencia de un simple `#[derive(Default)]`, el atributo
55/// `#[derive(AutoDefault)]` permite usar anotaciones en los campos como `#[default = "..."]`,
56/// funcionando incluso en estructuras con campos que no implementan [`Default`] o en *enums*.
57#[proc_macro_derive(AutoDefault, attributes(default))]
58pub fn derive_auto_default(input: TokenStream) -> TokenStream {
59 let input = parse_macro_input!(input as DeriveInput);
60 match smart_default::body_impl::impl_my_derive(&input) {
61 Ok(output) => output.into(),
62 Err(error) => error.to_compile_error().into(),
63 }
64}
65
66/// Macro (*attribute*) que asocia un método *builder* `with_` con un método `alter_`.
67///
68/// La macro añade automáticamente un método `alter_` para modificar la instancia actual usando
69/// `&mut self`, y redefine el método *builder* `with_`, que consume la instancia (`mut self`), para
70/// delegar la lógica de la modificación al nuevo método `alter_`, reutilizando así la misma
71/// implementación.
72///
73/// Esta macro emitirá un error en tiempo de compilación si la función anotada no cumple con la
74/// firma esperada para el método *builder*: `pub fn with_...(mut self, ...) -> Self`.
75///
76/// # Ejemplos
77///
78/// Si defines un método `with_` como este:
79///
80/// ```rust,ignore
81/// #[builder_fn]
82/// pub fn with_example(mut self, value: impl Into<String>) -> Self {
83/// self.value = Some(value.into());
84/// self
85/// }
86/// ```
87///
88/// la macro generará automáticamente el siguiente método `alter_`:
89///
90/// ```rust,ignore
91/// pub fn alter_example(&mut self, value: impl Into<String>) -> &mut Self {
92/// self.value = Some(value.into());
93/// self
94/// }
95/// ```
96///
97/// y reescribirá el método `with_` para delegar la modificación al método `alter_`:
98///
99/// ```rust,ignore
100/// pub fn with_example(mut self, value: impl Into<String>) -> Self {
101/// self.alter_example(value);
102/// self
103/// }
104/// ```
105///
106/// Así, cada método *builder* `with_...()` generará automáticamente su correspondiente método
107/// `alter_...()`, que permitirá más adelante modificar instancias existentes.
108#[proc_macro_attribute]
109pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
110 use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
111
112 let ts: proc_macro2::TokenStream = item.clone().into();
113
114 enum Kind {
115 Impl(ImplItemFn),
116 Trait(TraitItemFn),
117 }
118
119 // Detecta si estamos en `impl` o `trait`.
120 let kind = if let Ok(it) = parse2::<ImplItemFn>(ts.clone()) {
121 Kind::Impl(it)
122 } else if let Ok(tt) = parse2::<TraitItemFn>(ts.clone()) {
123 Kind::Trait(tt)
124 } else {
125 return quote! {
126 compile_error!("#[builder_fn] only supports methods in `impl` blocks or `trait` items");
127 }
128 .into();
129 };
130
131 // Extrae piezas comunes (sig, attrs, vis, bloque?, es_trait?).
132 let (sig, attrs, vis, body_opt, is_trait) = match &kind {
133 Kind::Impl(m) => (&m.sig, &m.attrs, Some(&m.vis), Some(&m.block), false),
134 Kind::Trait(t) => (&t.sig, &t.attrs, None, t.default.as_ref(), true),
135 };
136
137 let with_name = sig.ident.clone();
138 let with_name_str = sig.ident.to_string();
139
140 // Valida el nombre del método.
141 if !with_name_str.starts_with("with_") {
142 return quote_spanned! {
143 sig.ident.span() => compile_error!("expected a named `with_...()` method");
144 }
145 .into();
146 }
147
148 // Sólo se exige `pub` en `impl` (en `trait` no aplica).
149 let vis_pub = match (is_trait, vis) {
150 (false, Some(v)) => quote! { #v },
151 _ => quote! {},
152 };
153
154 // Validaciones comunes.
155 if sig.asyncness.is_some() {
156 return quote_spanned! {
157 sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`");
158 }
159 .into();
160 }
161 if sig.constness.is_some() {
162 return quote_spanned! {
163 sig.constness.span() => compile_error!("`with_...()` cannot be `const`");
164 }
165 .into();
166 }
167 if sig.abi.is_some() {
168 return quote_spanned! {
169 sig.abi.span() => compile_error!("`with_...()` cannot be `extern`");
170 }
171 .into();
172 }
173 if sig.unsafety.is_some() {
174 return quote_spanned! {
175 sig.unsafety.span() => compile_error!("`with_...()` cannot be `unsafe`");
176 }
177 .into();
178 }
179
180 // En `impl` se exige exactamente `mut self`; y en `trait` se exige `self` (sin &).
181 let receiver_ok = match sig.inputs.first() {
182 Some(FnArg::Receiver(r)) => {
183 // Rechaza `self: SomeType`.
184 if r.colon_token.is_some() {
185 false
186 } else if is_trait {
187 // Exactamente `self` (sin &, sin mut).
188 r.reference.is_none() && r.mutability.is_none()
189 } else {
190 // Exactamente `mut self`.
191 r.reference.is_none() && r.mutability.is_some()
192 }
193 }
194 _ => false,
195 };
196 if !receiver_ok {
197 let msg = if is_trait {
198 "expected `self` (not `mut self`, `&self` or `&mut self`) in trait method"
199 } else {
200 "expected first argument to be exactly `mut self`"
201 };
202 let err = sig
203 .inputs
204 .first()
205 .map(|a| a.span())
206 .unwrap_or(sig.ident.span());
207 return quote_spanned! {
208 err => compile_error!(#msg);
209 }
210 .into();
211 }
212
213 // Valida que el método devuelve exactamente `Self`.
214 match &sig.output {
215 ReturnType::Type(_, ty) => match ty.as_ref() {
216 Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => {}
217 _ => {
218 return quote_spanned! {
219 ty.span() => compile_error!("expected return type to be exactly `Self`");
220 }
221 .into();
222 }
223 },
224 _ => {
225 return quote_spanned! {
226 sig.output.span() => compile_error!("expected return type to be exactly `Self`");
227 }
228 .into();
229 }
230 }
231
232 // Genera el nombre del método alter_...().
233 let stem = with_name_str.strip_prefix("with_").expect("validated");
234 let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
235
236 // Extrae genéricos y cláusulas where.
237 let generics = &sig.generics;
238 let where_clause = &sig.generics.where_clause;
239
240 // Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos).
241 let args: Vec<_> = sig.inputs.iter().skip(1).collect();
242 let call_idents: Vec<Ident> = {
243 let mut v = Vec::new();
244 for arg in sig.inputs.iter().skip(1) {
245 match arg {
246 FnArg::Typed(pat) => {
247 if let Pat::Ident(pat_ident) = pat.pat.as_ref() {
248 v.push(pat_ident.ident.clone());
249 } else {
250 return quote_spanned! {
251 pat.pat.span() => compile_error!(
252 "each parameter must be a simple identifier, e.g. `value: T`"
253 );
254 }
255 .into();
256 }
257 }
258 _ => {
259 return quote_spanned! {
260 arg.span() => compile_error!("unexpected receiver in parameter list");
261 }
262 .into();
263 }
264 }
265 }
266 v
267 };
268
269 // Extrae atributos descartando la documentación para incluir en `alter_...()`.
270 let non_doc_attrs: Vec<_> = attrs
271 .iter()
272 .filter(|&a| !a.path().is_ident("doc"))
273 .cloned()
274 .collect();
275
276 // Documentación del método alter_...().
277 let alter_doc =
278 format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
279
280 // Genera el código final.
281 let expanded = match body_opt {
282 None => {
283 quote! {
284 #(#attrs)*
285 fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
286
287 #(#non_doc_attrs)*
288 #[doc = #alter_doc]
289 fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
290 }
291 }
292 Some(body) => {
293 let with_fn = if is_trait {
294 quote! {
295 #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause {
296 let mut s = self;
297 s.#alter_ident(#(#call_idents),*);
298 s
299 }
300 }
301 } else {
302 quote! {
303 #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause {
304 self.#alter_ident(#(#call_idents),*);
305 self
306 }
307 }
308 };
309 quote! {
310 #(#attrs)*
311 #with_fn
312
313 #(#non_doc_attrs)*
314 #[doc = #alter_doc]
315 #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
316 #body
317 }
318 }
319 }
320 };
321 expanded.into()
322}
323
324/// Define una función `main` asíncrona como punto de entrada de PageTop.
325///
326/// # Ejemplo
327///
328/// ```rust,ignore
329/// #[pagetop::main]
330/// async fn main() {
331/// async { println!("Hello world!"); }.await
332/// }
333/// ```
334#[proc_macro_attribute]
335pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
336 let mut output: TokenStream = (quote! {
337 #[::pagetop::service::rt::main(system = "::pagetop::service::rt::System")]
338 })
339 .into();
340
341 output.extend(item);
342 output
343}
344
345/// Define funciones de prueba asíncronas para usar con PageTop.
346///
347/// # Ejemplo
348///
349/// ```rust,ignore
350/// #[pagetop::test]
351/// async fn test() {
352/// assert_eq!(async { "Hello world" }.await, "Hello world");
353/// }
354/// ```
355#[proc_macro_attribute]
356pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
357 let mut output: TokenStream = (quote! {
358 #[::pagetop::service::rt::test(system = "::pagetop::service::rt::System")]
359 })
360 .into();
361
362 output.extend(item);
363 output
364}