From c34dc02357adc06550d30e75d7a5400f6c857930 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 29 Aug 2026 19:19:58 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(macros):=20A=C3=B1ade=20#[builder?= =?UTF-8?q?=5Fimpl]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aplica `#[builder_fn]` a todos los métodos `with_...()` de un bloque `impl` o de una definición de trait de una vez, sin anotarlos uno a uno. --- helpers/pagetop-macros/src/builder.rs | 449 ++++++++++++++++++++++++++ helpers/pagetop-macros/src/lib.rs | 351 +++++--------------- src/lib.rs | 2 +- src/prelude.rs | 2 +- 4 files changed, 529 insertions(+), 275 deletions(-) create mode 100644 helpers/pagetop-macros/src/builder.rs diff --git a/helpers/pagetop-macros/src/builder.rs b/helpers/pagetop-macros/src/builder.rs new file mode 100644 index 00000000..8c6bf696 --- /dev/null +++ b/helpers/pagetop-macros/src/builder.rs @@ -0,0 +1,449 @@ +//! Núcleo compartido de `#[builder_fn]` y `#[builder_impl]`. + +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; +use syn::{ + Attribute, Block, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, ItemTrait, Pat, ReturnType, + Signature, TraitItem, TraitItemFn, Type, Visibility, WhereClause, parse_quote, parse2, +}; + +// Genera el código común a `#[builder_fn]` y `#[builder_impl]` para un único método `with_...()`. +// +// Recibe las piezas ya extraídas de un `ImplItemFn` o de un `TraitItemFn`: firma, atributos, +// visibilidad (ausente en métodos de trait), cuerpo (ausente en la declaración de un método trait +// sin implementación por defecto) y si el receptor es de trait (`self`) o de impl (`mut self`). +fn expand_builder( + sig: &Signature, + attrs: &[Attribute], + vis: Option<&Visibility>, + body_opt: Option<&Block>, + is_trait: bool, +) -> TokenStream { + let with_name = sig.ident.clone(); + let with_name_str = sig.ident.to_string(); + + // Valida el nombre del método. + if !with_name_str.starts_with("with_") { + return quote_spanned! { + sig.ident.span() => compile_error!("expected a named `with_...()` method"); + }; + } + + // Sólo se exige `pub` en `impl` (en `trait` no aplica). + let vis_pub = match (is_trait, vis) { + (false, Some(v)) => quote! { #v }, + _ => quote! {}, + }; + + // Validaciones comunes. + if sig.asyncness.is_some() { + return quote_spanned! { + sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`"); + }; + } + if sig.constness.is_some() { + return quote_spanned! { + sig.constness.span() => compile_error!("`with_...()` cannot be `const`"); + }; + } + if sig.abi.is_some() { + return quote_spanned! { + sig.abi.span() => compile_error!("`with_...()` cannot be `extern`"); + }; + } + if sig.unsafety.is_some() { + return quote_spanned! { + sig.unsafety.span() => compile_error!("`with_...()` cannot be `unsafe`"); + }; + } + + // En `impl` se exige exactamente `mut self`; y en `trait` se exige `self` (sin &). + let receiver_ok = match sig.inputs.first() { + Some(FnArg::Receiver(r)) => { + // Rechaza `self: SomeType`. + if r.colon_token.is_some() { + false + } else if is_trait { + // Exactamente `self` (sin &, sin mut). + r.reference.is_none() && r.mutability.is_none() + } else { + // Exactamente `mut self`. + r.reference.is_none() && r.mutability.is_some() + } + } + _ => false, + }; + if !receiver_ok { + let msg = if is_trait { + "expected `self` (not `mut self`, `&self` or `&mut self`) in trait method" + } else { + "expected first argument to be exactly `mut self`" + }; + let err = sig + .inputs + .first() + .map(|a| a.span()) + .unwrap_or(sig.ident.span()); + return quote_spanned! { + err => compile_error!(#msg); + }; + } + + // Valida que el método devuelve exactamente `Self`. + match &sig.output { + ReturnType::Type(_, ty) => match ty.as_ref() { + Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => {} + _ => { + return quote_spanned! { + ty.span() => compile_error!("expected return type to be exactly `Self`"); + }; + } + }, + _ => { + return quote_spanned! { + sig.output.span() => compile_error!("expected return type to be exactly `Self`"); + }; + } + } + + // Genera el nombre del método `alter_...()`. + let stem = with_name_str.strip_prefix("with_").expect("validated"); + let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span()); + + // Extrae genéricos y cláusulas `where`. + let generics = &sig.generics; + let where_clause = &sig.generics.where_clause; + + // Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos). + let args: Vec<_> = sig.inputs.iter().skip(1).collect(); + let call_idents: Vec = { + let mut v = Vec::new(); + for arg in sig.inputs.iter().skip(1) { + match arg { + FnArg::Typed(pat) => { + if let Pat::Ident(pat_ident) = pat.pat.as_ref() { + v.push(pat_ident.ident.clone()); + } else { + return quote_spanned! { + pat.pat.span() => compile_error!( + "each parameter must be a simple identifier, e.g. `value: T`" + ); + }; + } + } + _ => { + return quote_spanned! { + arg.span() => compile_error!("unexpected receiver in parameter list"); + }; + } + } + } + v + }; + + // Separa atributos de documentación y resto. + let mut doc_attrs = Vec::new(); + let mut other_attrs = Vec::new(); + let mut non_doc_or_inline_attrs = Vec::new(); + + for a in attrs.iter() { + let p = a.path(); + if p.is_ident("doc") { + doc_attrs.push(a.clone()); + } else { + other_attrs.push(a.clone()); + if !p.is_ident("inline") { + non_doc_or_inline_attrs.push(a.clone()); + } + } + } + + // Firma resumida de la función `alter_...()` para mostrarla en la doc de `with_...()`. + let alter_sig_tokens = if args.is_empty() { + // Sin argumentos sólo se muestra `&mut self` (puede que no tenga mucho sentido). + quote! { #vis_pub fn #alter_ident #generics (&mut self) -> &mut Self #where_clause } + } else { + // Con argumentos se muestra `&mut self, ...`. + quote! { #vis_pub fn #alter_ident #generics (&mut self, ...) -> &mut Self #where_clause } + }; + + // Normaliza espacios raros tipo `& mut`. + let alter_sig_str = alter_sig_tokens.to_string().replace("& mut", "&mut"); + + // Nombre de la función `alter_...()` como alias de búsqueda. + let alter_name_str = alter_ident.to_string(); + + // Texto introductorio para la documentación adicional de `with_...()`. + let with_alter_title = format!( + "# {} el método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)", + if doc_attrs.is_empty() { + "Añade" + } else { + "También añade" + }, + alter_name_str + ); + let with_alter_doc = concat!( + "Permite modificar la instancia (`&mut self`) con los mismos argumentos ", + "pero sin consumirla." + ); + + // Atributos completos que se aplican siempre a `with_...()`. + let with_prefix = quote! { + #(#other_attrs)* + #(#doc_attrs)* + #[doc(alias = #alter_name_str)] + #[doc = ""] + #[doc = #with_alter_title] + #[doc = #with_alter_doc] + #[doc = "```text"] + #[doc = #alter_sig_str] + #[doc = "```"] + }; + + // Genera el código final. + match body_opt { + None => { + quote! { + #with_prefix + fn #with_name #generics (self, #(#args),*) -> Self #where_clause; + + #(#non_doc_or_inline_attrs)* + #[doc(hidden)] + fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause; + } + } + Some(body) => { + // Si no se indicó ninguna forma de `inline`, fuerza `#[inline]` para `with_...()`. + let force_inline = if attrs.iter().any(|a| a.path().is_ident("inline")) { + quote! {} + } else { + quote! { #[inline] } + }; + + let with_fn = if is_trait { + // Un cuerpo por defecto se compila junto a la propia definición del trait, donde + // `Self` podría no ser `Sized`; a diferencia de una declaración sin cuerpo (rama + // `None`), aquí sí hace falta acotarlo explícitamente para poder devolver `Self` + // por valor. Se añade la cota sobre el `Punctuated` ya existente (en vez de + // concatenar tokens a mano) para que la coma se coloque bien incluso si el `where` + // original ya termina en una. + let with_where: WhereClause = match where_clause { + Some(wc) => { + let mut wc = wc.clone(); + wc.predicates.push(parse_quote!(Self: Sized)); + wc + } + None => parse_quote!(where Self: Sized), + }; + quote! { + #with_prefix + #force_inline + #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #with_where { + let mut s = self; + s.#alter_ident(#(#call_idents),*); + s + } + } + } else { + quote! { + #with_prefix + #force_inline + #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause { + self.#alter_ident(#(#call_idents),*); + self + } + } + }; + + quote! { + #with_fn + + #(#non_doc_or_inline_attrs)* + #[doc(hidden)] + #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause { + #body + } + } + } + } +} + +// Implementa `#[builder_fn]`: detecta si el ítem anotado es un método de `impl` o de `trait`, +// extrae sus piezas comunes y delega en `expand_builder`. +pub(crate) fn expand_fn(item: TokenStream) -> TokenStream { + enum Kind { + Impl(ImplItemFn), + Trait(TraitItemFn), + } + + // Detecta si estamos en `impl` o `trait`. + let kind = if let Ok(it) = parse2::(item.clone()) { + Kind::Impl(it) + } else if let Ok(tt) = parse2::(item.clone()) { + Kind::Trait(tt) + } else { + return quote! { + compile_error!("#[builder_fn] only supports methods in `impl` blocks or `trait` items"); + }; + }; + + // Extrae piezas comunes (sig, attrs, vis, bloque?, es_trait?). + let (sig, attrs, vis, body_opt, is_trait) = match &kind { + Kind::Impl(m) => (&m.sig, &m.attrs, Some(&m.vis), Some(&m.block), false), + Kind::Trait(t) => (&t.sig, &t.attrs, None, t.default.as_ref(), true), + }; + + expand_builder(sig, attrs, vis, body_opt, is_trait) +} + +// Comprueba si la lista de atributos contiene uno con el nombre dado. +fn has_attr(attrs: &[Attribute], name: &str) -> bool { + attrs.iter().any(|a| a.path().is_ident(name)) +} + +// Decide qué hacer con un único método durante el barrido de `#[builder_impl]`, sea de un `impl` +// o de un `trait`. Devuelve `None` si el método no es un `with_...()` a barrer (ni siquiera marcado +// con `#[builder_skip]`), en cuyo caso el llamador lo reemite intacto. +fn sweep_with_fn( + sig: &Signature, + attrs: &[Attribute], + vis: Option<&Visibility>, + body_opt: Option<&Block>, + is_trait: bool, +) -> Option { + if !sig.ident.to_string().starts_with("with_") { + return None; + } + let skip = has_attr(attrs, "builder_skip"); + // Se rechaza `with_...()` marcado a la vez con `#[builder_skip]` y `#[builder_fn]`. + if skip && has_attr(attrs, "builder_fn") { + return Some(quote_spanned! { + sig.ident.span() => compile_error!( + "`#[builder_skip]` and `#[builder_fn]` cannot be combined on the same method" + ); + }); + } + if skip { + return None; + } + // Descarta atributos auxiliares para no reprocesar ni dejar atributos desconocidos. + let clean: Vec = attrs + .iter() + .filter(|a| !a.path().is_ident("builder_fn") && !a.path().is_ident("builder_skip")) + .cloned() + .collect(); + Some(expand_builder(sig, &clean, vis, body_opt, is_trait)) +} + +// Implementa `#[builder_impl]`: aplica `expand_builder` a todos los métodos `with_...()` de un +// bloque `impl` o de una definición de `trait`, dejando el resto de ítems intactos. +pub(crate) fn expand_impl(item: TokenStream) -> TokenStream { + if let Ok(item_impl) = parse2::(item.clone()) { + return expand_item_impl(item_impl); + } + if let Ok(item_trait) = parse2::(item) { + return expand_item_trait(item_trait); + } + quote! { + compile_error!("#[builder_impl] only supports `impl` blocks or `trait` definitions"); + } +} + +fn expand_item_impl(item: ItemImpl) -> TokenStream { + let ItemImpl { + attrs, + defaultness, + unsafety, + impl_token, + generics, + trait_, + self_ty, + items, + .. + } = item; + + let mut out = Vec::new(); + + for it in items { + match it { + ImplItem::Fn(f) => { + match sweep_with_fn(&f.sig, &f.attrs, Some(&f.vis), Some(&f.block), false) { + Some(ts) => out.push(ts), + None => { + // Método no-builder (o `with_...()` marcado con `#[builder_skip]`): se + // reemite intacto, retirando siempre `#[builder_skip]` (atributo inerte). + let mut f = f; + f.attrs.retain(|a| !a.path().is_ident("builder_skip")); + out.push(quote! { #f }); + } + } + } + other => out.push(quote! { #other }), + } + } + + let (impl_generics, _type_generics, where_clause) = generics.split_for_impl(); + + // Reconstruye la parte `Trait for` si el impl es de trait. + let trait_ = trait_.map(|(bang, path, for_token)| quote! { #bang #path #for_token }); + + quote! { + #(#attrs)* + #defaultness #unsafety #impl_token #impl_generics #trait_ #self_ty #where_clause { + #(#out)* + } + } +} + +fn expand_item_trait(item: ItemTrait) -> TokenStream { + let ItemTrait { + attrs, + vis, + unsafety, + auto_token, + trait_token, + ident, + generics, + colon_token, + supertraits, + items, + .. + } = item; + + let mut out = Vec::new(); + + for it in items { + match it { + TraitItem::Fn(f) => { + match sweep_with_fn(&f.sig, &f.attrs, None, f.default.as_ref(), true) { + Some(ts) => out.push(ts), + None => { + // Método no-builder (o `with_...()` marcado con `#[builder_skip]`): se + // reemite intacto, retirando siempre `#[builder_skip]` (atributo inerte). + let mut f = f; + f.attrs.retain(|a| !a.path().is_ident("builder_skip")); + out.push(quote! { #f }); + } + } + } + other => out.push(quote! { #other }), + } + } + + // El propio nombre de la lista de genéricos (``) es el que lleva las cotas en una + // definición de trait, a diferencia de su uso como tipo; por eso se usa `impl_generics` y no + // `type_generics` para reconstruir `trait Nombre<...>`. + let (impl_generics, _type_generics, where_clause) = generics.split_for_impl(); + + quote! { + #(#attrs)* + #vis #unsafety #auto_token #trait_token #ident #impl_generics + #colon_token #supertraits + #where_clause + { + #(#out)* + } + } +} diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index bb9aaa89..70c97e1a 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -34,12 +34,13 @@ cada proyecto PageTop. html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] +mod builder; mod maud; mod smart_default; use proc_macro::TokenStream; -use quote::{quote, quote_spanned}; -use syn::{DeriveInput, ItemFn, parse_macro_input, spanned::Spanned}; +use quote::quote; +use syn::{DeriveInput, ItemFn, parse_macro_input}; /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)). #[proc_macro] @@ -162,280 +163,84 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// La documentación del método `with_...()` incluirá también la firma resumida del método /// `alter_...()` y un alias de búsqueda con su nombre, de tal manera que buscando `alter_...` en la /// documentación se mostrará la entrada del método `with_...()`. +/// +/// Para aplicar la misma transformación a todos los métodos `with_...()` de un `impl` de una sola +/// vez, usa [`#[builder_impl]`](builder_impl). #[proc_macro_attribute] pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { - use syn::{FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type, parse2}; + builder::expand_fn(item.into()).into() +} - let ts: proc_macro2::TokenStream = item.clone().into(); - - enum Kind { - Impl(ImplItemFn), - Trait(TraitItemFn), - } - - // Detecta si estamos en `impl` o `trait`. - let kind = if let Ok(it) = parse2::(ts.clone()) { - Kind::Impl(it) - } else if let Ok(tt) = parse2::(ts.clone()) { - Kind::Trait(tt) - } else { - return quote! { - compile_error!("#[builder_fn] only supports methods in `impl` blocks or `trait` items"); - } - .into(); - }; - - // Extrae piezas comunes (sig, attrs, vis, bloque?, es_trait?). - let (sig, attrs, vis, body_opt, is_trait) = match &kind { - Kind::Impl(m) => (&m.sig, &m.attrs, Some(&m.vis), Some(&m.block), false), - Kind::Trait(t) => (&t.sig, &t.attrs, None, t.default.as_ref(), true), - }; - - let with_name = sig.ident.clone(); - let with_name_str = sig.ident.to_string(); - - // Valida el nombre del método. - if !with_name_str.starts_with("with_") { - return quote_spanned! { - sig.ident.span() => compile_error!("expected a named `with_...()` method"); - } - .into(); - } - - // Sólo se exige `pub` en `impl` (en `trait` no aplica). - let vis_pub = match (is_trait, vis) { - (false, Some(v)) => quote! { #v }, - _ => quote! {}, - }; - - // Validaciones comunes. - if sig.asyncness.is_some() { - return quote_spanned! { - sig.asyncness.span() => compile_error!("`with_...()` cannot be `async`"); - } - .into(); - } - if sig.constness.is_some() { - return quote_spanned! { - sig.constness.span() => compile_error!("`with_...()` cannot be `const`"); - } - .into(); - } - if sig.abi.is_some() { - return quote_spanned! { - sig.abi.span() => compile_error!("`with_...()` cannot be `extern`"); - } - .into(); - } - if sig.unsafety.is_some() { - return quote_spanned! { - sig.unsafety.span() => compile_error!("`with_...()` cannot be `unsafe`"); - } - .into(); - } - - // En `impl` se exige exactamente `mut self`; y en `trait` se exige `self` (sin &). - let receiver_ok = match sig.inputs.first() { - Some(FnArg::Receiver(r)) => { - // Rechaza `self: SomeType`. - if r.colon_token.is_some() { - false - } else if is_trait { - // Exactamente `self` (sin &, sin mut). - r.reference.is_none() && r.mutability.is_none() - } else { - // Exactamente `mut self`. - r.reference.is_none() && r.mutability.is_some() - } - } - _ => false, - }; - if !receiver_ok { - let msg = if is_trait { - "expected `self` (not `mut self`, `&self` or `&mut self`) in trait method" - } else { - "expected first argument to be exactly `mut self`" - }; - let err = sig - .inputs - .first() - .map(|a| a.span()) - .unwrap_or(sig.ident.span()); - return quote_spanned! { - err => compile_error!(#msg); - } - .into(); - } - - // Valida que el método devuelve exactamente `Self`. - match &sig.output { - ReturnType::Type(_, ty) => match ty.as_ref() { - Type::Path(p) if p.qself.is_none() && p.path.is_ident("Self") => {} - _ => { - return quote_spanned! { - ty.span() => compile_error!("expected return type to be exactly `Self`"); - } - .into(); - } - }, - _ => { - return quote_spanned! { - sig.output.span() => compile_error!("expected return type to be exactly `Self`"); - } - .into(); - } - } - - // Genera el nombre del método `alter_...()`. - let stem = with_name_str.strip_prefix("with_").expect("validated"); - let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span()); - - // Extrae genéricos y cláusulas `where`. - let generics = &sig.generics; - let where_clause = &sig.generics.where_clause; - - // Extrae identificadores de los argumentos para la llamada (sin `mut` ni patrones complejos). - let args: Vec<_> = sig.inputs.iter().skip(1).collect(); - let call_idents: Vec = { - let mut v = Vec::new(); - for arg in sig.inputs.iter().skip(1) { - match arg { - FnArg::Typed(pat) => { - if let Pat::Ident(pat_ident) = pat.pat.as_ref() { - v.push(pat_ident.ident.clone()); - } else { - return quote_spanned! { - pat.pat.span() => compile_error!( - "each parameter must be a simple identifier, e.g. `value: T`" - ); - } - .into(); - } - } - _ => { - return quote_spanned! { - arg.span() => compile_error!("unexpected receiver in parameter list"); - } - .into(); - } - } - } - v - }; - - // Separa atributos de documentación y resto. - let mut doc_attrs = Vec::new(); - let mut other_attrs = Vec::new(); - let mut non_doc_or_inline_attrs = Vec::new(); - - for a in attrs.iter() { - let p = a.path(); - if p.is_ident("doc") { - doc_attrs.push(a.clone()); - } else { - other_attrs.push(a.clone()); - if !p.is_ident("inline") { - non_doc_or_inline_attrs.push(a.clone()); - } - } - } - - // Firma resumida de la función `alter_...()` para mostrarla en la doc de `with_...()`. - let alter_sig_tokens = if args.is_empty() { - // Sin argumentos sólo se muestra `&mut self` (puede que no tenga mucho sentido). - quote! { #vis_pub fn #alter_ident #generics (&mut self) -> &mut Self #where_clause } - } else { - // Con argumentos se muestra `&mut self, ...`. - quote! { #vis_pub fn #alter_ident #generics (&mut self, ...) -> &mut Self #where_clause } - }; - - // Normaliza espacios raros tipo `& mut`. - let alter_sig_str = alter_sig_tokens.to_string().replace("& mut", "&mut"); - - // Nombre de la función `alter_...()` como alias de búsqueda. - let alter_name_str = alter_ident.to_string(); - - // Texto introductorio para la documentación adicional de `with_...()`. - let with_alter_title = format!( - "# {} el método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)", - if doc_attrs.is_empty() { - "Añade" - } else { - "También añade" - }, - alter_name_str - ); - let with_alter_doc = concat!( - "Permite modificar la instancia (`&mut self`) con los mismos argumentos ", - "pero sin consumirla." - ); - - // Atributos completos que se aplican siempre a `with_...()`. - let with_prefix = quote! { - #(#other_attrs)* - #(#doc_attrs)* - #[doc(alias = #alter_name_str)] - #[doc = ""] - #[doc = #with_alter_title] - #[doc = #with_alter_doc] - #[doc = "```text"] - #[doc = #alter_sig_str] - #[doc = "```"] - }; - - // Genera el código final. - let expanded = match body_opt { - None => { - quote! { - #with_prefix - fn #with_name #generics (self, #(#args),*) -> Self #where_clause; - - #(#non_doc_or_inline_attrs)* - #[doc(hidden)] - fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause; - } - } - Some(body) => { - // Si no se indicó ninguna forma de `inline`, fuerza `#[inline]` para `with_...()`. - let force_inline = if attrs.iter().any(|a| a.path().is_ident("inline")) { - quote! {} - } else { - quote! { #[inline] } - }; - - let with_fn = if is_trait { - quote! { - #with_prefix - #force_inline - #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause { - let mut s = self; - s.#alter_ident(#(#call_idents),*); - s - } - } - } else { - quote! { - #with_prefix - #force_inline - #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause { - self.#alter_ident(#(#call_idents),*); - self - } - } - }; - - quote! { - #with_fn - - #(#non_doc_or_inline_attrs)* - #[doc(hidden)] - #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause { - #body - } - } - } - }; - expanded.into() +/// Macro (*attribute*) que aplica [`#[builder_fn]`](builder_fn) a los métodos `with_` de un +/// `impl`/`trait`. +/// +/// Cada método que empiece por `with_` se transforma igual que si llevara `#[builder_fn]` +/// individualmente: se genera su correspondiente método `alter_...()` y se añade la misma +/// documentación. El resto de ítems del bloque (métodos que no empiecen por `with_`, constantes +/// asociadas, tipos, etc.) no se modifican. +/// +/// La política es estricta; si un método `with_...()` no cumple la firma esperada por +/// [`#[builder_fn]`](builder_fn) para su contexto, la macro emite el mismo error de compilación que +/// emitiría `#[builder_fn]` sobre ese método. Para excluir deliberadamente un método `with_...()`, +/// márcalo con `#[builder_skip]`; se mantendrá intacto, como cualquier otro método que no sea +/// *builder*. +/// +/// Un `#[builder_fn]` explícito sobre un método dentro de un bloque `#[builder_impl]` es +/// redundante pero inofensivo, no se expande dos veces. Combinar `#[builder_skip]` y +/// `#[builder_fn]` sobre el mismo método sí es un error de compilación porque la intención de ambos +/// atributos sí es contradictoria. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// # use pagetop_macros::builder_impl; +/// # #[derive(Default)] +/// # struct Example { a: Option, b: Option } +/// #[builder_impl] +/// impl Example { +/// pub fn with_a(mut self, value: impl Into) -> Self { +/// self.a = Some(value.into()); +/// self +/// } +/// +/// pub fn with_b(mut self, value: u32) -> Self { +/// self.b = Some(value); +/// self +/// } +/// +/// pub fn a(&self) -> Option<&str> { +/// self.a.as_deref() +/// } +/// } +/// +/// let example = Example::default().with_a("hello").with_b(42); +/// ``` +/// +/// genera, para `with_a` y `with_b`, el mismo par `with_.../alter_...` que produciría anotar cada +/// uno individualmente con [`#[builder_fn]`](builder_fn); `a()` se reemite sin modificar. +/// +/// Sobre una definición de `trait`, con receptor `self` (sin `mut`) en cada `with_...()`: +/// +/// ```rust,no_run +/// # use pagetop_macros::builder_impl; +/// #[builder_impl] +/// pub trait Example { +/// /// Sin cuerpo por defecto: sólo genera la declaración. +/// fn with_a(self, value: impl Into) -> Self; +/// +/// /// Con cuerpo por defecto: genera también la implementación, heredable sin redefinirla. +/// fn with_b(self, value: u32) -> Self { +/// self +/// } +/// } +/// ``` +/// +/// Un `with_...()` de trait con cuerpo por defecto añade `where Self: Sized` automáticamente. A +/// diferencia de una declaración sin cuerpo, éste se compila junto a la propia definición del +/// trait, donde `Self` podría no ser `Sized`, y Rust lo exige para poder devolverlo por valor. +#[proc_macro_attribute] +pub fn builder_impl(_: TokenStream, item: TokenStream) -> TokenStream { + builder::expand_impl(item.into()).into() } /// Define una función `main` asíncrona como punto de entrada de PageTop. diff --git a/src/lib.rs b/src/lib.rs index 61df71e1..2d56ebcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,7 +140,7 @@ pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); /// [`impl Extension`](crate::core::extension::Extension). pub use async_trait::async_trait; -pub use pagetop_macros::{AutoDefault, builder_fn, html, main, test}; +pub use pagetop_macros::{AutoDefault, builder_fn, builder_impl, html, main, test}; pub use pagetop_statics::{StaticFile, resource}; diff --git a/src/prelude.rs b/src/prelude.rs index c00e1eb9..9d8e570e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,7 @@ pub use crate::PAGETOP_VERSION; -pub use crate::{async_trait, builder_fn, html, main, test}; +pub use crate::{async_trait, builder_fn, builder_impl, html, main, test}; pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight};