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](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-licencia)
9[![Doc API](https://img.shields.io/docsrs/pagetop-macros?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-macros)
10[![Crates.io](https://img.shields.io/crates/v/pagetop-macros.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-macros)
11[![Descargas](https://img.shields.io/crates/d/pagetop-macros.svg?label=Descargas&style=for-the-badge&logo=transmission)](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}