diff --git a/extensions/pagetop-admin/src/component/admin_frame.rs b/extensions/pagetop-admin/src/component/admin_frame.rs index cbdfd371..8a63ed23 100644 --- a/extensions/pagetop-admin/src/component/admin_frame.rs +++ b/extensions/pagetop-admin/src/component/admin_frame.rs @@ -68,15 +68,16 @@ impl Component for AdminFrame { } } -#[builder_impl] impl AdminFrame { /// Establece el título de la página. + #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { self.title = title; self } /// Añade un componente hijo al contenido de la página. + #[builder_fn] pub fn with_child(mut self, op: impl Into) -> Self { self.children.alter_child(op.into()); self diff --git a/extensions/pagetop-admin/src/component/config_form.rs b/extensions/pagetop-admin/src/component/config_form.rs index d8ea0da0..304fb621 100644 --- a/extensions/pagetop-admin/src/component/config_form.rs +++ b/extensions/pagetop-admin/src/component/config_form.rs @@ -201,10 +201,8 @@ impl Component for ConfigForm { } } -#[builder_impl] impl ConfigForm { /// Crea el componente con el [`SettingsSchema`] dado. - #[builder_skip] pub fn with_schema(schema: SettingsSchema) -> Self { ConfigForm { schema: Some(schema), @@ -212,6 +210,7 @@ impl ConfigForm { } } + #[builder_fn] pub fn with_action_path(mut self, v: impl Into>) -> Self { if let Some(v) = v.into() { self.action_path = Some(v); @@ -219,7 +218,6 @@ impl ConfigForm { self } - #[builder_skip] pub(crate) fn with_saved(mut self, saved: bool, error: bool) -> Self { self.saved = saved; self.error = error; diff --git a/extensions/pagetop-admin/src/settings.rs b/extensions/pagetop-admin/src/settings.rs index e2e38ee7..0eb3028a 100644 --- a/extensions/pagetop-admin/src/settings.rs +++ b/extensions/pagetop-admin/src/settings.rs @@ -3,7 +3,7 @@ //! Proporciona una API async para leer y escribir valores JSON en la tabla `settings`. use pagetop::datetime::Utc; -use pagetop::{Getters, builder_impl}; +use pagetop::{Getters, builder_fn}; use pagetop_seaorm::db::{ ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, dbconn, }; @@ -128,7 +128,6 @@ pub struct SettingField { default_value: Option, } -#[builder_impl] impl SettingField { /// Crea un campo de texto con nombre y etiqueta. pub fn text(name: impl Into, label: impl Into) -> Self { @@ -186,18 +185,21 @@ impl SettingField { } /// Establece si el campo es obligatorio. + #[builder_fn] pub fn with_required(mut self, required: bool) -> Self { self.required = required; self } /// Añade texto de ayuda bajo el campo. + #[builder_fn] pub fn with_help(mut self, text: impl Into) -> Self { self.help_text = Some(text.into()); self } /// Establece el valor por defecto (como valor JSON serializado). + #[builder_fn] pub fn with_default(mut self, value: &T) -> Self { self.default_value = serde_json::to_string(value).ok(); self @@ -213,7 +215,6 @@ pub struct SettingsSchema { fields: Vec, } -#[builder_impl] impl SettingsSchema { /// Crea un nuevo esquema vacío para el `scope` dado. pub fn new(scope: impl Into) -> Self { @@ -224,6 +225,7 @@ impl SettingsSchema { } /// Añade un campo al esquema. + #[builder_fn] pub fn with_field(mut self, field: SettingField) -> Self { self.fields.push(field); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/badge.rs b/extensions/pagetop-bootsier/src/theme/bs/badge.rs index ab058de7..bd3a40dc 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/badge.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/badge.rs @@ -18,15 +18,15 @@ const EXTRA_COLOR: &str = "bootsier.badge.color"; /// /// let badge = bs::Badge::labeled(Lc::n("Beta")).with_color(BootsierColors::Dark); /// ``` -#[builder_impl] pub trait BadgeBootsier { /// Fuerza un color de la paleta de Bootsier, ignorando el que le correspondería a la `Intent` /// del badge. `None` restablece el comportamiento por defecto (color derivado de la `Intent`). + #[builder_fn] fn with_color(self, color: impl Into>) -> Self; } -#[builder_impl] impl BadgeBootsier for Badge { + #[builder_fn] fn with_color(mut self, color: impl Into>) -> Self { match color.into() { Some(color) => self.alter_prop(PropsOp::set_extra(EXTRA_COLOR, color)), diff --git a/extensions/pagetop-bootsier/src/theme/bs/button.rs b/extensions/pagetop-bootsier/src/theme/bs/button.rs index 56ac3adb..6581f95a 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/button.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/button.rs @@ -33,32 +33,36 @@ const EXTRA_COLOR: &str = "bootsier.button.color"; /// .with_style(button::Style::Solid(Intent::Neutral)) /// .with_color(BootsierColors::Light); /// ``` -#[builder_impl] pub trait ButtonBootsier { /// Marca el botón como activo (`.active`, `aria-pressed="true"`). + #[builder_fn] fn with_active(self, active: bool) -> Self; /// Expande el botón al ancho completo de su contenedor (`w-100`). + #[builder_fn] fn with_full_width(self, full_width: bool) -> Self; /// Fuerza un color de la paleta de Bootsier, ignorando el que le correspondería a la `Intent` /// del botón. `None` restablece el comportamiento por defecto (color derivado de la `Intent`). /// Sin efecto si el estilo del botón es [`Style::Link`] o [`Style::None`]. + #[builder_fn] fn with_color(self, color: impl Into>) -> Self; } -#[builder_impl] impl ButtonBootsier for Button { + #[builder_fn] fn with_active(mut self, active: bool) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_ACTIVE, active)); self } + #[builder_fn] fn with_full_width(mut self, full_width: bool) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_FULL_WIDTH, full_width)); self } + #[builder_fn] fn with_color(mut self, color: impl Into>) -> Self { match color.into() { Some(color) => self.alter_prop(PropsOp::set_extra(EXTRA_COLOR, color)), diff --git a/extensions/pagetop-bootsier/src/theme/bs/container.rs b/extensions/pagetop-bootsier/src/theme/bs/container.rs index 67526ab8..e86b5679 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/container.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/container.rs @@ -32,18 +32,18 @@ const EXTRA_WIDTH: &str = "bootsier.container.width"; /// .with_prop(PropsOp::add_classes(class::Border::with(ScaleSize::One))) /// .with_prop(PropsOp::add_classes(class::Rounded::new())); /// ``` -#[builder_impl] pub trait ContainerBootsier { /// Establece el comportamiento del ancho para el contenedor. /// /// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de /// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo /// explícito. Ver [`Width`] para las variantes disponibles. + #[builder_fn] fn with_width(self, width: Width) -> Self; } -#[builder_impl] impl ContainerBootsier for Container { + #[builder_fn] fn with_width(mut self, width: Width) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_WIDTH, width)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs b/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs index 338a45fd..bbb08461 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs @@ -44,46 +44,54 @@ const EXTRA_MENU_POSITION: &str = "bootsier.dropdown.menu_position"; /// .with_item(bs::dropdown::Item::header(Lc::n("User session"))) /// .with_item(bs::dropdown::Item::button(Lc::n("Sign out"))); /// ``` -#[builder_impl] pub trait DropdownBootsier { /// Indica si el botón del menú está integrado en un grupo de botones. + #[builder_fn] fn with_button_grouped(self, grouped: bool) -> Self; /// Establece la política de cierre automático del menú desplegable. + #[builder_fn] fn with_auto_close(self, auto_close: AutoClose) -> Self; /// Establece la dirección de despliegue del menú. + #[builder_fn] fn with_direction(self, direction: Direction) -> Self; /// Configura la alineación horizontal (con posible comportamiento *responsive* adicional). + #[builder_fn] fn with_menu_align(self, align: MenuAlign) -> Self; /// Configura la posición del menú. + #[builder_fn] fn with_menu_position(self, position: MenuPosition) -> Self; } -#[builder_impl] impl DropdownBootsier for Dropdown { + #[builder_fn] fn with_button_grouped(mut self, grouped: bool) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_BUTTON_GROUPED, grouped)); self } + #[builder_fn] fn with_auto_close(mut self, auto_close: AutoClose) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_AUTO_CLOSE, auto_close)); self } + #[builder_fn] fn with_direction(mut self, direction: Direction) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_DIRECTION, direction)); self } + #[builder_fn] fn with_menu_align(mut self, align: MenuAlign) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_MENU_ALIGN, align)); self } + #[builder_fn] fn with_menu_position(mut self, position: MenuPosition) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_MENU_POSITION, position)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs index 524b2fa4..6de0698e 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs @@ -22,18 +22,18 @@ const EXTRA_FLOATING_LABEL: &str = "bootsier.form.input.floating_label"; /// .with_placeholder(Lc::n("Enter your name")) /// .with_floating_label(true); /// ``` -#[builder_impl] pub trait InputBootsier { /// Establece si la etiqueta se muestra flotante sobre el campo. /// /// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene /// contenido. Requiere que el campo tenga un atributo `placeholder` definido; si no se /// especifica, se fuerza `placeholder=""` antes del renderizado. + #[builder_fn] fn with_floating_label(self, floating: bool) -> Self; } -#[builder_impl] impl InputBootsier for Field { + #[builder_fn] fn with_floating_label(mut self, floating: bool) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/select.rs b/extensions/pagetop-bootsier/src/theme/bs/form/select.rs index 7132ca26..4759c4f8 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/select.rs @@ -24,7 +24,6 @@ const EXTRA_FLOATING_LABEL: &str = "bootsier.form.select.floating_label"; /// .with_item(bs::form::select::Item::new("es", Lc::n("Spanish"))) /// .with_item(bs::form::select::Item::new("en", Lc::n("English"))); /// ``` -#[builder_impl] pub trait SelectBootsier { /// Establece si la etiqueta se muestra flotante sobre el campo. /// @@ -34,11 +33,12 @@ pub trait SelectBootsier { /// Si se usa la etiqueta flotante, se anulan los valores establecidos con /// [`with_multiple()`](form::select::Field::with_multiple) y /// [`with_rows()`](form::select::Field::with_rows) antes del renderizado. + #[builder_fn] fn with_floating_label(self, floating: bool) -> Self; } -#[builder_impl] impl SelectBootsier for Field { + #[builder_fn] fn with_floating_label(mut self, floating: bool) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs index 8d5573a8..264a6d59 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs @@ -22,7 +22,6 @@ const EXTRA_FLOATING_LABEL: &str = "bootsier.form.textarea.floating_label"; /// .with_placeholder(Lc::n("Write here...")) /// .with_floating_label(true); /// ``` -#[builder_impl] pub trait TextareaBootsier { /// Establece si la etiqueta se muestra flotante sobre el campo. /// @@ -32,11 +31,12 @@ pub trait TextareaBootsier { /// /// Si se usa la etiqueta flotante, se anula el valor establecido con /// [`with_rows()`](form::Textarea::with_rows) antes del renderizado. + #[builder_fn] fn with_floating_label(self, floating: bool) -> Self; } -#[builder_impl] impl TextareaBootsier for Textarea { + #[builder_fn] fn with_floating_label(mut self, floating: bool) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/icon.rs b/extensions/pagetop-bootsier/src/theme/bs/icon.rs index ea4bb430..4fe0f8ec 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/icon.rs @@ -78,7 +78,6 @@ impl Component for Icon { } } -#[builder_impl] impl Icon { pub fn font() -> Self { Self::default().with_icon_kind(IconKind::Font(FontSize::default())) @@ -105,22 +104,26 @@ impl Icon { // **< Icon BUILDER >*************************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } + #[builder_fn] pub fn with_icon_kind(mut self, icon_kind: IconKind) -> Self { self.icon_kind = icon_kind; self } + #[builder_fn] pub fn with_aria_label(mut self, label: Lc) -> Self { self.aria_label.alter_value(label); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/nav.rs b/extensions/pagetop-bootsier/src/theme/bs/nav.rs index ba2079ef..13a9f3d3 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/nav.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/nav.rs @@ -46,14 +46,14 @@ pub(crate) const EXTRA_IN_NAVBAR: &str = "bootsier.nav.in_navbar"; /// )) /// .with_item(bs::nav::Item::link_disabled(Lc::n("Disabled"), "#")); /// ``` -#[builder_impl] pub trait NavBootsier { /// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*). + #[builder_fn] fn with_kind(self, kind: Kind) -> Self; } -#[builder_impl] impl NavBootsier for Nav { + #[builder_fn] fn with_kind(mut self, kind: Kind) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_KIND, kind)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs index 82a0bff6..26a651fe 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs @@ -140,7 +140,6 @@ const EXTRA_EXPAND: &str = "bootsier.navbar.expand"; /// .with_item(bs::nav::Item::link(Lc::n("Stock"), "/stock")) /// )); /// ``` -#[builder_impl] pub trait NavbarBootsier { /// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**. fn offcanvas(oc: bs::Offcanvas) -> Self; @@ -152,13 +151,14 @@ pub trait NavbarBootsier { fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self; /// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar. + #[builder_fn] fn with_expand(self, bp: BreakPoint) -> Self; /// Define dónde se mostrará la barra de navegación dentro del documento. + #[builder_fn] fn with_position(self, position: bs::navbar::Position) -> Self; } -#[builder_impl] impl NavbarBootsier for Navbar { fn offcanvas(oc: bs::Offcanvas) -> Self { let mut navbar = Self::new(); @@ -187,11 +187,13 @@ impl NavbarBootsier for Navbar { navbar } + #[builder_fn] fn with_expand(mut self, bp: BreakPoint) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_EXPAND, bp)); self } + #[builder_fn] fn with_position(mut self, position: bs::navbar::Position) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position)); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs index 1dcabe91..207f756f 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs @@ -90,23 +90,25 @@ impl Component for Offcanvas { } } -#[builder_impl] impl Offcanvas { // **< Offcanvas BUILDER >********************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } /// Establece el título del encabezado. + #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { self.title = title; self @@ -121,6 +123,7 @@ impl Offcanvas { /// Por ejemplo, con `BreakPoint::Lg`, será *offcanvas* en móviles y tabletas, y visible /// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea /// *offcanvas* siempre. + #[builder_fn] pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self { self.breakpoint = bp; self @@ -128,24 +131,28 @@ impl Offcanvas { /// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del /// panel. + #[builder_fn] pub fn with_backdrop(mut self, backdrop: bs::offcanvas::Backdrop) -> Self { self.backdrop = backdrop; self } /// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto. + #[builder_fn] pub fn with_body_scroll(mut self, scrolling: bs::offcanvas::BodyScroll) -> Self { self.body_scroll = scrolling; self } /// Indica desde qué borde de la ventana entra y se ancla el panel. + #[builder_fn] pub fn with_placement(mut self, placement: bs::offcanvas::Placement) -> Self { self.placement = placement; self } /// Fija el estado inicial del panel (oculto o visible al cargar). + #[builder_fn] pub fn with_visibility(mut self, visibility: bs::offcanvas::Visibility) -> Self { self.visibility = visibility; self @@ -153,6 +160,7 @@ impl Offcanvas { /// Añade un nuevo componente al panel o modifica la lista de componentes (`children`) con una /// operación [`ChildOp`]. + #[builder_fn] pub fn with_child(mut self, op: impl Into) -> Self { self.children.alter_child(op.into()); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/sidebar/item.rs b/extensions/pagetop-bootsier/src/theme/bs/sidebar/item.rs index 49b34932..2305fdd0 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/sidebar/item.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/sidebar/item.rs @@ -65,7 +65,6 @@ impl Component for Item { } } -#[builder_impl] impl Item { /// Crea un ítem de navegación con etiqueta, ruta e icono. /// @@ -83,18 +82,21 @@ impl Item { // **< Item BUILDER >*************************************************************************** /// Establece el texto localizable del ítem. + #[builder_fn] pub fn with_label(mut self, label: Lc) -> Self { self.label = label; self } /// Establece la ruta de destino del ítem. + #[builder_fn] pub fn with_route(mut self, route: impl Into>) -> Self { self.route = route.into(); self } /// Establece el nombre del icono de Bootstrap Icons (sin el prefijo `bi-`). + #[builder_fn] pub fn with_icon(mut self, icon: impl Into) -> Self { self.icon = icon.into(); self diff --git a/extensions/pagetop-bootsier/src/theme/bs/sidebar/section.rs b/extensions/pagetop-bootsier/src/theme/bs/sidebar/section.rs index d4d7805f..16fbb793 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/sidebar/section.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/sidebar/section.rs @@ -40,7 +40,6 @@ impl Component for Section { } } -#[builder_impl] impl Section { /// Crea un encabezado de sección con el título indicado. pub fn titled(title: Lc) -> Self { @@ -50,6 +49,7 @@ impl Section { // **< Section BUILDER >************************************************************************ /// Establece el título localizable de la sección. + #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { self.title = title; self diff --git a/extensions/pagetop-menu/src/component/menu_block.rs b/extensions/pagetop-menu/src/component/menu_block.rs index 2bd24030..afcd79b4 100644 --- a/extensions/pagetop-menu/src/component/menu_block.rs +++ b/extensions/pagetop-menu/src/component/menu_block.rs @@ -107,7 +107,6 @@ impl Component for MenuBlock { } } -#[builder_impl] impl MenuBlock { /// Crea un `MenuBlock` para el menú con el `machine_name` dado. pub fn with(menu_name: impl Into) -> Self { @@ -116,6 +115,7 @@ impl MenuBlock { block } + #[builder_fn] pub fn with_show_title(mut self, v: impl Into>) -> Self { if let Some(v) = v.into() { self.show_title = v; @@ -125,11 +125,13 @@ impl MenuBlock { /// Establece la profundidad máxima de nodos a incluir. El efectivo nunca supera `2`, por muy /// alto que sea el valor indicado (ver "Limitaciones conocidas" en [`MenuBlock`]). + #[builder_fn] pub fn with_max_depth(mut self, v: impl Into>) -> Self { self.max_depth = v.into(); self } + #[builder_fn] pub fn with_include_disabled(mut self, v: impl Into>) -> Self { if let Some(v) = v.into() { self.include_disabled = v; @@ -137,6 +139,7 @@ impl MenuBlock { self } + #[builder_fn] pub fn with_hide_when_empty(mut self, v: impl Into>) -> Self { if let Some(v) = v.into() { self.hide_when_empty = v; diff --git a/extensions/pagetop-menu/src/component/menu_breadcrumb.rs b/extensions/pagetop-menu/src/component/menu_breadcrumb.rs index c049883c..9d9d95e2 100644 --- a/extensions/pagetop-menu/src/component/menu_breadcrumb.rs +++ b/extensions/pagetop-menu/src/component/menu_breadcrumb.rs @@ -73,7 +73,6 @@ impl Component for MenuBreadcrumb { } } -#[builder_impl] impl MenuBreadcrumb { /// Crea un `MenuBreadcrumb` para el menú con el `machine_name` dado. pub fn with(menu_name: impl Into) -> Self { @@ -82,6 +81,7 @@ impl MenuBreadcrumb { bc } + #[builder_fn] pub fn with_include_current(mut self, v: impl Into>) -> Self { if let Some(v) = v.into() { self.include_current = v; diff --git a/extensions/pagetop-user/src/component/admin/admin_password_form.rs b/extensions/pagetop-user/src/component/admin/admin_password_form.rs index 3c418660..85befebf 100644 --- a/extensions/pagetop-user/src/component/admin/admin_password_form.rs +++ b/extensions/pagetop-user/src/component/admin/admin_password_form.rs @@ -43,20 +43,22 @@ impl Component for AdminPasswordForm { } } -#[builder_impl] impl AdminPasswordForm { // **< AdminPasswordForm BUILDER >************************************************************** + #[builder_fn] pub(crate) fn with_user_id(mut self, user_id: i32) -> Self { self.user_id = user_id; self } + #[builder_fn] pub(crate) fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self } + #[builder_fn] pub(crate) fn with_waypoint(mut self, waypoint: impl Into) -> Self { self.waypoint = waypoint.into(); self diff --git a/extensions/pagetop-user/src/component/admin/role_form.rs b/extensions/pagetop-user/src/component/admin/role_form.rs index 5bbfea89..d8efba69 100644 --- a/extensions/pagetop-user/src/component/admin/role_form.rs +++ b/extensions/pagetop-user/src/component/admin/role_form.rs @@ -98,45 +98,52 @@ impl Component for RoleForm { } } -#[builder_impl] impl RoleForm { // **< RoleForm BUILDER >*********************************************************************** + #[builder_fn] pub(crate) fn with_mode(mut self, mode: RoleFormMode) -> Self { self.mode = mode; self } + #[builder_fn] pub(crate) fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self } + #[builder_fn] pub(crate) fn with_role_id(mut self, role_id: impl Into>) -> Self { self.role_id = role_id.into(); self } + #[builder_fn] pub(crate) fn with_waypoint(mut self, waypoint: impl Into) -> Self { self.waypoint = waypoint.into(); self } + #[builder_fn] pub(crate) fn with_machine_name(mut self, machine_name: impl Into) -> Self { self.machine_name = machine_name.into(); self } + #[builder_fn] pub(crate) fn with_label(mut self, label: impl Into) -> Self { self.label = label.into(); self } + #[builder_fn] pub(crate) fn with_description(mut self, description: impl Into) -> Self { self.description = description.into(); self } + #[builder_fn] pub(crate) fn with_weight(mut self, weight: i32) -> Self { self.weight = weight; self diff --git a/extensions/pagetop-user/src/component/admin/role_permissions_form.rs b/extensions/pagetop-user/src/component/admin/role_permissions_form.rs index ad1b237c..9263588e 100644 --- a/extensions/pagetop-user/src/component/admin/role_permissions_form.rs +++ b/extensions/pagetop-user/src/component/admin/role_permissions_form.rs @@ -61,25 +61,28 @@ impl Component for RolePermissionsForm { } } -#[builder_impl] impl RolePermissionsForm { // **< RolePermissionsForm BUILDER >************************************************************ + #[builder_fn] pub(crate) fn with_role_id(mut self, role_id: i32) -> Self { self.role_id = role_id; self } + #[builder_fn] pub(crate) fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self } + #[builder_fn] pub(crate) fn with_groups(mut self, groups: PermissionGroups) -> Self { self.groups = groups; self } + #[builder_fn] pub(crate) fn with_waypoint(mut self, waypoint: impl Into) -> Self { self.waypoint = waypoint.into(); self diff --git a/extensions/pagetop-user/src/component/admin/role_table.rs b/extensions/pagetop-user/src/component/admin/role_table.rs index 8bd97db0..9c28ead8 100644 --- a/extensions/pagetop-user/src/component/admin/role_table.rs +++ b/extensions/pagetop-user/src/component/admin/role_table.rs @@ -128,45 +128,52 @@ impl Component for RoleTable { } } -#[builder_impl] impl RoleTable { // **< RoleTable BUILDER >********************************************************************** + #[builder_fn] pub(crate) fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } + #[builder_fn] pub(crate) fn with_items(mut self, items: Vec) -> Self { self.items = items; self } + #[builder_fn] pub(crate) fn with_message(mut self, message: impl Into>) -> Self { self.message = message.into(); self } + #[builder_fn] pub(crate) fn with_sort(mut self, sort: RoleSortField) -> Self { self.sort = sort; self } + #[builder_fn] pub(crate) fn with_dir(mut self, dir: SortDir) -> Self { self.dir = dir; self } + #[builder_fn] pub(crate) fn with_page(mut self, page: u64) -> Self { self.page = page; self } + #[builder_fn] pub(crate) fn with_per_page(mut self, per_page: u64) -> Self { self.per_page = per_page; self } + #[builder_fn] pub(crate) fn with_total(mut self, total: u64) -> Self { self.total = total; self diff --git a/extensions/pagetop-user/src/component/admin/user_form.rs b/extensions/pagetop-user/src/component/admin/user_form.rs index ecaf8217..4ee68fcd 100644 --- a/extensions/pagetop-user/src/component/admin/user_form.rs +++ b/extensions/pagetop-user/src/component/admin/user_form.rs @@ -121,65 +121,76 @@ impl Component for UserForm { } } -#[builder_impl] impl UserForm { // **< UserForm BUILDER >*********************************************************************** + #[builder_fn] pub(crate) fn with_mode(mut self, mode: UserFormMode) -> Self { self.mode = mode; self } + #[builder_fn] pub(crate) fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self } + #[builder_fn] pub(crate) fn with_user_id(mut self, user_id: impl Into>) -> Self { self.user_id = user_id.into(); self } + #[builder_fn] pub(crate) fn with_waypoint(mut self, waypoint: impl Into) -> Self { self.waypoint = waypoint.into(); self } + #[builder_fn] pub(crate) fn with_username(mut self, username: impl Into) -> Self { self.username = username.into(); self } + #[builder_fn] pub(crate) fn with_email(mut self, email: impl Into) -> Self { self.email = email.into(); self } + #[builder_fn] pub(crate) fn with_display_name(mut self, display_name: impl Into) -> Self { self.display_name = display_name.into(); self } + #[builder_fn] pub(crate) fn with_language(mut self, language: impl Into) -> Self { self.language = language.into(); self } + #[builder_fn] pub(crate) fn with_timezone(mut self, timezone: impl Into) -> Self { self.timezone = timezone.into(); self } + #[builder_fn] pub(crate) fn with_roles(mut self, roles: Vec<(i32, String, bool)>) -> Self { self.roles = roles; self } + #[builder_fn] pub(crate) fn with_allow_admin_field(mut self, allow_admin_field: bool) -> Self { self.allow_admin_field = allow_admin_field; self } + #[builder_fn] pub(crate) fn with_is_admin(mut self, is_admin: bool) -> Self { self.is_admin = is_admin; self diff --git a/extensions/pagetop-user/src/component/admin/user_roles_form.rs b/extensions/pagetop-user/src/component/admin/user_roles_form.rs index 927c07b0..cbf9e779 100644 --- a/extensions/pagetop-user/src/component/admin/user_roles_form.rs +++ b/extensions/pagetop-user/src/component/admin/user_roles_form.rs @@ -43,25 +43,28 @@ impl Component for UserRolesForm { } } -#[builder_impl] impl UserRolesForm { // **< UserRolesForm BUILDER >****************************************************************** + #[builder_fn] pub(crate) fn with_user_id(mut self, user_id: i32) -> Self { self.user_id = user_id; self } + #[builder_fn] pub(crate) fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self } + #[builder_fn] pub(crate) fn with_roles(mut self, roles: Vec<(i32, String, bool)>) -> Self { self.roles = roles; self } + #[builder_fn] pub(crate) fn with_waypoint(mut self, waypoint: impl Into) -> Self { self.waypoint = waypoint.into(); self diff --git a/extensions/pagetop-user/src/component/admin/user_table.rs b/extensions/pagetop-user/src/component/admin/user_table.rs index b32315eb..d8b67b17 100644 --- a/extensions/pagetop-user/src/component/admin/user_table.rs +++ b/extensions/pagetop-user/src/component/admin/user_table.rs @@ -100,45 +100,52 @@ impl Component for UserTable { } } -#[builder_impl] impl UserTable { // **< UserTable BUILDER >********************************************************************** + #[builder_fn] pub(crate) fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } + #[builder_fn] pub(crate) fn with_items(mut self, items: Vec) -> Self { self.items = items; self } + #[builder_fn] pub(crate) fn with_sort(mut self, sort: UserSortField) -> Self { self.sort = sort; self } + #[builder_fn] pub(crate) fn with_dir(mut self, dir: SortDir) -> Self { self.dir = dir; self } + #[builder_fn] pub(crate) fn with_query(mut self, query: impl Into>) -> Self { self.query = query.into(); self } + #[builder_fn] pub(crate) fn with_page(mut self, page: u64) -> Self { self.page = page; self } + #[builder_fn] pub(crate) fn with_per_page(mut self, per_page: u64) -> Self { self.per_page = per_page; self } + #[builder_fn] pub(crate) fn with_total(mut self, total: u64) -> Self { self.total = total; self diff --git a/extensions/pagetop-user/src/component/login_form.rs b/extensions/pagetop-user/src/component/login_form.rs index ec582d0b..7012331a 100644 --- a/extensions/pagetop-user/src/component/login_form.rs +++ b/extensions/pagetop-user/src/component/login_form.rs @@ -97,13 +97,14 @@ fn links(allow_registration: bool) -> Html { }) } -#[builder_impl] impl LoginForm { + #[builder_fn] pub fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self } + #[builder_fn] pub fn with_waypoint(mut self, waypoint: impl Into) -> Self { self.waypoint = waypoint.into(); self diff --git a/extensions/pagetop-user/src/component/password_confirm.rs b/extensions/pagetop-user/src/component/password_confirm.rs index 480b2ca1..93c445b1 100644 --- a/extensions/pagetop-user/src/component/password_confirm.rs +++ b/extensions/pagetop-user/src/component/password_confirm.rs @@ -48,17 +48,18 @@ impl Component for PasswordConfirm { } } -#[builder_impl] impl PasswordConfirm { // **< PasswordConfirm BUILDER >******************************************************************** /// Establece la etiqueta del campo de contraseña (por defecto, "field-password"). + #[builder_fn] pub(crate) fn with_password_label(mut self, label: Lc) -> Self { self.password_label = label; self } /// Establece la etiqueta del campo de confirmación (por defecto, "field-confirm-password"). + #[builder_fn] pub(crate) fn with_confirm_label(mut self, label: Lc) -> Self { self.confirm_label = label; self diff --git a/extensions/pagetop-user/src/component/password_reset_confirm_form.rs b/extensions/pagetop-user/src/component/password_reset_confirm_form.rs index 172af055..dc185c07 100644 --- a/extensions/pagetop-user/src/component/password_reset_confirm_form.rs +++ b/extensions/pagetop-user/src/component/password_reset_confirm_form.rs @@ -44,8 +44,8 @@ impl Component for PasswordResetConfirmForm { } } -#[builder_impl] impl PasswordResetConfirmForm { + #[builder_fn] pub fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self diff --git a/extensions/pagetop-user/src/component/password_reset_form.rs b/extensions/pagetop-user/src/component/password_reset_form.rs index 89bb3361..5e0bdb29 100644 --- a/extensions/pagetop-user/src/component/password_reset_form.rs +++ b/extensions/pagetop-user/src/component/password_reset_form.rs @@ -49,8 +49,8 @@ fn back_to_login() -> Html { }) } -#[builder_impl] impl PasswordResetForm { + #[builder_fn] pub fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self diff --git a/extensions/pagetop-user/src/component/register_form.rs b/extensions/pagetop-user/src/component/register_form.rs index 5eec2871..66ac153b 100644 --- a/extensions/pagetop-user/src/component/register_form.rs +++ b/extensions/pagetop-user/src/component/register_form.rs @@ -45,8 +45,8 @@ impl Component for RegisterForm { } } -#[builder_impl] impl RegisterForm { + #[builder_fn] pub fn with_error(mut self, error: impl Into>) -> Self { self.error = error.into(); self diff --git a/helpers/pagetop-macros/src/builder.rs b/helpers/pagetop-macros/src/builder.rs deleted file mode 100644 index 8c6bf696..00000000 --- a/helpers/pagetop-macros/src/builder.rs +++ /dev/null @@ -1,449 +0,0 @@ -//! 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 70c97e1a..bb9aaa89 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -34,13 +34,12 @@ 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; -use syn::{DeriveInput, ItemFn, parse_macro_input}; +use quote::{quote, quote_spanned}; +use syn::{DeriveInput, ItemFn, parse_macro_input, spanned::Spanned}; /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)). #[proc_macro] @@ -163,84 +162,280 @@ 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 { - builder::expand_fn(item.into()).into() -} + use syn::{FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type, parse2}; -/// 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() + 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() } /// Define una función `main` asíncrona como punto de entrada de PageTop. diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs index 3aee8b06..1a7c45a0 100644 --- a/src/base/component/badge.rs +++ b/src/base/component/badge.rs @@ -49,7 +49,6 @@ impl Component for Badge { } } -#[builder_impl] impl Badge { /// Crea un badge predeterminado (`Intent::default()`) con la etiqueta indicada. pub fn labeled(label: Lc) -> Self { @@ -116,24 +115,28 @@ impl Badge { // **< Badge BUILDER >************************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } /// Establece la etiqueta del badge. + #[builder_fn] pub fn with_label(mut self, label: Lc) -> Self { self.label = label; self } /// Establece la intención semántica del badge. + #[builder_fn] pub fn with_intent(mut self, intent: Intent) -> Self { self.intent = intent; self diff --git a/src/base/component/block.rs b/src/base/component/block.rs index 2f01f01e..2bf350df 100644 --- a/src/base/component/block.rs +++ b/src/base/component/block.rs @@ -50,23 +50,25 @@ impl Component for Block { } } -#[builder_impl] impl Block { // **< Block BUILDER >************************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } /// Establece el título del bloque. + #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { self.title = title; self @@ -74,6 +76,7 @@ impl Block { /// Añade un nuevo componente al bloque o modifica la lista de componentes (`children`) con una /// operación [`ChildOp`]. + #[builder_fn] pub fn with_child(mut self, op: impl Into) -> Self { self.children.alter_child(op.into()); self diff --git a/src/base/component/brand.rs b/src/base/component/brand.rs index 913a24e1..a1113ef3 100644 --- a/src/base/component/brand.rs +++ b/src/base/component/brand.rs @@ -77,35 +77,39 @@ impl Component for Brand { } } -#[builder_impl] impl Brand { // **< Brand BUILDER >************************************************************************** /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. + #[builder_fn] pub fn with_image(mut self, image: impl Into>) -> Self { self.image.alter_component(image); self } /// Establece el título de la identidad de marca. + #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { self.title = title; self } /// Define la ruta de destino. Si es `None`, la marca no será un enlace. + #[builder_fn] pub fn with_route(mut self, route: impl Into>) -> Self { self.route = route.into(); self diff --git a/src/base/component/breadcrumb/component.rs b/src/base/component/breadcrumb/component.rs index ce3bfa9e..dca96e18 100644 --- a/src/base/component/breadcrumb/component.rs +++ b/src/base/component/breadcrumb/component.rs @@ -67,23 +67,25 @@ impl Component for Breadcrumb { } } -#[builder_impl] impl Breadcrumb { // **< Breadcrumb BUILDER >********************************************************************* /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } /// Añade un nuevo elemento al final del breadcrumb. + #[builder_fn] pub fn with_crumb(mut self, crumb: breadcrumb::Crumb) -> Self { self.crumbs.push(crumb); self diff --git a/src/base/component/breadcrumb/crumb.rs b/src/base/component/breadcrumb/crumb.rs index 0352a76f..b29ce92b 100644 --- a/src/base/component/breadcrumb/crumb.rs +++ b/src/base/component/breadcrumb/crumb.rs @@ -31,7 +31,6 @@ pub struct Crumb { is_current: bool, } -#[builder_impl] impl Crumb { /// Crea un elemento enlazado a la ruta indicada. pub fn new(label: Lc, route: impl Into) -> Self { @@ -67,12 +66,14 @@ impl Crumb { // **< Crumb BUILDER >************************************************************************** /// Establece el identificador único del elemento. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS o atributos HTML del elemento. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self diff --git a/src/base/component/button/component.rs b/src/base/component/button/component.rs index 805881d5..34a1bb1f 100644 --- a/src/base/component/button/component.rs +++ b/src/base/component/button/component.rs @@ -131,7 +131,6 @@ impl Component for Button { } } -#[builder_impl] impl Button { /// Crea un botón de **envío** (`type="submit"`). /// @@ -187,30 +186,35 @@ impl Button { // **< Button BUILDER >************************************************************************* /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] pub fn with_id(mut self, id: impl Into) -> Self { self.props.alter_id(id); self } /// Modifica identificador, clases CSS, atributos HTML o valores extra del componente. + #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); self } /// Establece el comportamiento del botón al activarse. + #[builder_fn] pub fn with_kind(mut self, kind: button::Kind) -> Self { self.kind = kind; self } /// Establece el tamaño visual del botón (usa [`button::Size::None`] para quitarlo). + #[builder_fn] pub fn with_size(mut self, size: button::Size) -> Self { self.size = size; self } /// Establece el estilo visual del botón (usa [`button::Style::None`] para quitarlo). + #[builder_fn] pub fn with_style(mut self, style: button::Style) -> Self { self.style = style; self @@ -220,6 +224,7 @@ impl Button { /// /// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par /// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado. + #[builder_fn] pub fn with_name(mut self, name: impl AsRef) -> Self { self.name.alter_name(name); self @@ -229,18 +234,21 @@ impl Button { /// /// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón /// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario. + #[builder_fn] pub fn with_value(mut self, value: impl AsRef) -> Self { self.value.alter_str(value); self } /// Establece la etiqueta visible del botón (usa [`Lc::none()`] para quitarla). + #[builder_fn] pub fn with_label(mut self, label: Lc) -> Self { self.label = label; self } /// Establece el texto emergente del botón (usa [`Lc::none()`] para quitarlo). + #[builder_fn] pub fn with_title(mut self, title: Lc) -> Self { self.title = title; self @@ -249,18 +257,21 @@ impl Button { /// Establece la ruta de destino y convierte el botón en enlace de navegación (``). /// Puedes usar un [`Route`] vacío (por defecto) para que vuelva a renderizarse como `