From c30c4cdf66c42efbefdcc3a56ebe0eb759ed14dc Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 12 Jul 2025 12:04:37 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[macros]=20A=C3=B1ade=20macro=20`bu?= =?UTF-8?q?ilder=5Ffn`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 4 +- Cargo.toml | 2 +- helpers/pagetop-macros/Cargo.toml | 2 +- helpers/pagetop-macros/src/lib.rs | 157 +++++++++++++++++++++++++++++- src/lib.rs | 2 +- src/prelude.rs | 2 +- 6 files changed, 161 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 659ed18..49fe007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1407,7 +1407,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pagetop" -version = "0.0.9" +version = "0.0.10" dependencies = [ "actix-files", "actix-web", @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "pagetop-macros" -version = "0.0.3" +version = "0.0.4" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 851c620..8212f62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pagetop" -version = "0.0.9" +version = "0.0.10" edition = "2021" description = """\ diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index 9f914bf..070e778 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pagetop-macros" -version = "0.0.3" +version = "0.0.4" edition = "2021" description = """\ diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 776903c..25105c2 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -18,8 +18,8 @@ mod maud; mod smart_default; use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use quote::{quote, quote_spanned}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, ItemFn}; /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)). #[proc_macro] @@ -43,6 +43,159 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { } } +/// Macro (*attribute*) que asocia un método *builder* `with_` con un método `alter_`. +/// +/// La macro añade automáticamente un método `alter_` para modificar la instancia actual usando +/// `&mut self`, y redefine el método *builder* `with_`, que consume la instancia (`mut self`), para +/// delegar la lógica de la modificación al nuevo método `alter_`, reutilizando así la misma +/// implementación. +/// +/// Esta macro emitirá un error en tiempo de compilación si la función anotada no cumple con la +/// firma esperada para el método *builder*: `pub fn with_...(mut self, ...) -> Self`. +/// +/// # Ejemplos +/// +/// Si defines un método `with_` como este: +/// +/// ```rust,ignore +/// #[builder_fn] +/// pub fn with_example(mut self, value: impl Into) -> Self { +/// self.value = Some(value.into()); +/// self +/// } +/// ``` +/// +/// la macro generará automáticamente el siguiente método `alter_`: +/// +/// ```rust,ignore +/// pub fn alter_example(&mut self, value: impl Into) -> &mut Self { +/// self.value = Some(value.into()); +/// self +/// } +/// ``` +/// +/// y reescribirá el método `with_` para delegar la modificación al método `alter_`: +/// +/// ```rust,ignore +/// pub fn with_example(mut self, value: impl Into) -> Self { +/// self.alter_example(value); +/// self +/// } +/// ``` +/// +/// Así, cada método *builder* `with_...()` generará automáticamente su correspondiente método +/// `alter_...()`, que permitirá más adelante modificar instancias existentes. +#[proc_macro_attribute] +pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { + let fn_with = parse_macro_input!(item as ItemFn); + let fn_with_name = fn_with.sig.ident.clone(); + let fn_with_name_str = fn_with.sig.ident.to_string(); + + // Valida el nombre del método. + if !fn_with_name_str.starts_with("with_") { + let expanded = quote_spanned! { + fn_with.sig.ident.span() => + compile_error!("expected a \"pub fn with_...(mut self, ...) -> Self\" method"); + }; + return expanded.into(); + } + // Valida que el método sea público. + if !matches!(fn_with.vis, syn::Visibility::Public(_)) { + return quote_spanned! { + fn_with.sig.ident.span() => compile_error!("expected method to be `pub`"); + } + .into(); + } + // Valida que el método devuelva el tipo `Self`. + if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output { + if let syn::Type::Path(type_path) = &**ty { + let ident = &type_path.path.segments.last().unwrap().ident; + if ident != "Self" { + return quote_spanned! { + fn_with.sig.output.span() => compile_error!("expected return type to be `Self`"); + }.into(); + } + } + } else { + return quote_spanned! { + fn_with.sig.output.span() => compile_error!("expected method to return `Self`"); + } + .into(); + } + // Valida que el primer argumento sea `mut self`. + if let Some(syn::FnArg::Receiver(receiver)) = fn_with.sig.inputs.first() { + if receiver.mutability.is_none() { + return quote_spanned! { + receiver.span() => compile_error!("expected `mut self` as the first argument"); + } + .into(); + } + } else { + return quote_spanned! { + fn_with.sig.ident.span() => compile_error!("expected `mut self` as the first argument"); + } + .into(); + } + + // Genera el nombre del método alter_...(). + let fn_alter_name_str = fn_with_name_str.replace("with_", "alter_"); + let fn_alter_name = syn::Ident::new(&fn_alter_name_str, fn_with.sig.ident.span()); + + // Extrae genéricos y cláusulas where. + let fn_generics = &fn_with.sig.generics; + let where_clause = &fn_with.sig.generics.where_clause; + + // Extrae argumentos y parámetros de llamada. + let args: Vec<_> = fn_with.sig.inputs.iter().skip(1).collect(); + let params: Vec<_> = fn_with + .sig + .inputs + .iter() + .skip(1) + .map(|arg| match arg { + syn::FnArg::Typed(pat) => &pat.pat, + _ => panic!("unexpected argument type"), + }) + .collect(); + + // Extrae bloque del método. + let fn_with_block = &fn_with.block; + + // Extrae documentación y otros atributos del método. + let fn_with_attrs = &fn_with.attrs; + + // Genera el método alter_...() con el código del método with_...(). + let fn_alter_doc = format!( + "Modifica la instancia en los mismos términos que para el patrón builder hace el \ + método asociado `{}()`.", + fn_with_name_str, + ); + let fn_alter = quote! { + #[doc = #fn_alter_doc] + pub fn #fn_alter_name #fn_generics(&mut self, #(#args),*) -> &mut Self #where_clause { + #fn_with_block + } + }; + + // Redefine el método with_...() para que llame a alter_...(). + let fn_with = quote! { + #(#fn_with_attrs)* + #[inline] + pub fn #fn_with_name #fn_generics(mut self, #(#args),*) -> Self #where_clause { + self.#fn_alter_name(#(#params),*); + self + } + }; + + // Genera el código final. + let expanded = quote! { + #fn_with + #[inline] + #fn_alter + }; + expanded.into() +} + /// Define una función `main` asíncrona como punto de entrada de `PageTop`. /// /// # Ejemplo diff --git a/src/lib.rs b/src/lib.rs index 800f783..d64cf55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ // RE-EXPORTED ************************************************************************************* -pub use pagetop_macros::{html, main, test, AutoDefault}; +pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault}; /// Representa un conjunto de recursos asociados a `$STATIC` en [`include_files!`]. pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>; diff --git a/src/prelude.rs b/src/prelude.rs index c27b997..e4aabf5 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,7 +2,7 @@ // RE-EXPORTED. -pub use crate::{html, main, test}; +pub use crate::{builder_fn, html, main, test}; pub use crate::{AutoDefault, StaticResources};