From e06d11d7ded92344ecc687d4a11bea3f891b25b5 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 27 Jul 2025 00:27:11 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reemplaza=20ContextOp?= =?UTF-8?q?=20por=20AssetsOp=20en=20el=20contexto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/html.rs | 2 +- src/html/context.rs | 151 +++++++++++++++++++++++--------------------- 2 files changed, 80 insertions(+), 73 deletions(-) diff --git a/src/html.rs b/src/html.rs index 7f18ee8..82fa906 100644 --- a/src/html.rs +++ b/src/html.rs @@ -10,7 +10,7 @@ pub use assets::stylesheet::{StyleSheet, TargetMedia}; pub(crate) use assets::Assets; mod context; -pub use context::{Context, ContextOp, ErrorParam}; +pub use context::{AssetsOp, Context, ErrorParam}; mod opt_id; pub use opt_id::OptionId; diff --git a/src/html/context.rs b/src/html/context.rs index 4374b14..a8d634a 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -1,11 +1,11 @@ use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; use crate::core::theme::ThemeRef; use crate::core::TypeInfo; -use crate::html::{html, Markup, Render}; +use crate::html::{html, Markup}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::join; use crate::locale::{LanguageIdentifier, DEFAULT_LANGID}; use crate::service::HttpRequest; +use crate::{builder_fn, join}; use std::collections::HashMap; use std::error::Error; @@ -14,17 +14,7 @@ use std::str::FromStr; use std::fmt; /// Operaciones para modificar el contexto ([`Context`]) del documento. -pub enum ContextOp { - /// Modifica el identificador de idioma del documento. - LangId(&'static LanguageIdentifier), - /// Establece el tema que se usará para renderizar el documento. - /// - /// Localiza el tema por su [`short_name`](crate::core::AnyInfo::short_name), y si no aplica - /// ninguno entonces usará el tema por defecto. - Theme(&'static str), - /// Define el tipo de composición usado para renderizar el documento. - Layout(&'static str), - +pub enum AssetsOp { // Favicon. /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. SetFavicon(Option), @@ -76,38 +66,35 @@ impl Error for ErrorParam {} /// ```rust /// use pagetop::prelude::*; /// -/// fn configure_context(mut cx: Context) { +/// fn configure_context(cx: &mut Context) { /// // Establece el idioma del documento a español. -/// cx.alter_assets(ContextOp::LangId( -/// LangMatch::langid_or_default("es-ES") -/// )) +/// cx.alter_langid(LangMatch::langid_or_default("es-ES")) /// // Selecciona un tema (por su nombre corto). -/// .alter_assets(ContextOp::Theme("aliner")) +/// .alter_theme("aliner") +/// // Añade un parámetro dinámico al contexto. +/// .alter_param("usuario_id", 42) /// // Asigna un favicon. -/// .alter_assets(ContextOp::SetFavicon(Some( +/// .alter_assets(AssetsOp::SetFavicon(Some( /// Favicon::new().with_icon("/icons/favicon.ico") /// ))) /// // Añade una hoja de estilo externa. -/// .alter_assets(ContextOp::AddStyleSheet( +/// .alter_assets(AssetsOp::AddStyleSheet( /// StyleSheet::from("/css/style.css") /// )) /// // Añade un script JavaScript. -/// .alter_assets(ContextOp::AddJavaScript( +/// .alter_assets(AssetsOp::AddJavaScript( /// JavaScript::defer("/js/main.js") /// )); /// -/// // Añade un parámetro dinámico al contexto. -/// cx.set_param("usuario_id", 42); -/// -/// // Recupera el parámetro y lo convierte a su tipo original. -/// let id: i32 = cx.get_param("usuario_id").unwrap(); -/// assert_eq!(id, 42); -/// /// // Recupera el tema seleccionado. /// let active_theme = cx.theme(); /// assert_eq!(active_theme.short_name(), "aliner"); /// -/// // Genera un identificador único para un componente de tipo `Menu`. +/// // Recupera el parámetro a su tipo original. +/// let id: i32 = cx.param("usuario_id").unwrap(); +/// assert_eq!(id, 42); +/// +/// // Genera un identificador para un componente de tipo `Menu`. /// struct Menu; /// let unique_id = cx.required_id::(None); /// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. @@ -119,10 +106,10 @@ pub struct Context { langid : &'static LanguageIdentifier, // Identificador del idioma. theme : ThemeRef, // Referencia al tema para renderizar. layout : &'static str, // Composición del documento para renderizar. + params : HashMap, // Parámetros definidos en tiempo de ejecución. favicon : Option, // Favicon, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // Scripts JavaScript. - params : HashMap, // Parámetros definidos en tiempo de ejecución. id_counter : usize, // Contador para generar identificadores únicos. } @@ -137,60 +124,84 @@ impl Context { langid : &DEFAULT_LANGID, theme : *DEFAULT_THEME, layout : "default", + params : HashMap::::new(), favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), - params : HashMap::::new(), id_counter : 0, } } - /// Modifica información o recursos del contexto usando [`ContextOp`]. - pub fn alter_assets(&mut self, op: ContextOp) -> &mut Self { + // Context BUILDER ***************************************************************************** + + /// Modifica el identificador de idioma del documento. + #[builder_fn] + pub fn with_langid(&mut self, langid: &'static LanguageIdentifier) -> &mut Self { + self.langid = langid; + self + } + + /// Establece el tema que se usará para renderizar el documento. + /// + /// Localiza el tema por su [`short_name`](crate::core::AnyInfo::short_name), y si no aplica + /// ninguno entonces usará el tema por defecto. + #[builder_fn] + pub fn with_theme(&mut self, theme_name: impl AsRef) -> &mut Self { + self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); + self + } + + /// Define el tipo de composición usado para renderizar el documento. + #[builder_fn] + pub fn with_layout(&mut self, layout_name: &'static str) -> &mut Self { + self.layout = layout_name; + self + } + + /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`]. + #[builder_fn] + pub fn with_param(&mut self, key: impl AsRef, value: T) -> &mut Self { + self.params + .insert(key.as_ref().to_string(), value.to_string()); + self + } + + /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó. + pub fn remove_param(&mut self, key: impl AsRef) -> bool { + self.params.remove(key.as_ref()).is_some() + } + + /// Modifica información o recursos del contexto usando [`AssetsOp`]. + #[builder_fn] + pub fn with_assets(&mut self, op: AssetsOp) -> &mut Self { match op { - ContextOp::LangId(langid) => { - self.langid = langid; - } - ContextOp::Theme(theme_name) => { - self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); - } - ContextOp::Layout(layout) => { - self.layout = layout; - } // Favicon. - ContextOp::SetFavicon(favicon) => { + AssetsOp::SetFavicon(favicon) => { self.favicon = favicon; } - ContextOp::SetFaviconIfNone(icon) => { + AssetsOp::SetFaviconIfNone(icon) => { if self.favicon.is_none() { self.favicon = Some(icon); } } // Stylesheets. - ContextOp::AddStyleSheet(css) => { + AssetsOp::AddStyleSheet(css) => { self.stylesheets.add(css); } - ContextOp::RemoveStyleSheet(path) => { + AssetsOp::RemoveStyleSheet(path) => { self.stylesheets.remove(path); } // JavaScripts. - ContextOp::AddJavaScript(js) => { + AssetsOp::AddJavaScript(js) => { self.javascripts.add(js); } - ContextOp::RemoveJavaScript(path) => { + AssetsOp::RemoveJavaScript(path) => { self.javascripts.remove(path); } } self } - /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`]. - pub fn set_param(&mut self, key: impl AsRef, value: T) -> &mut Self { - self.params - .insert(key.as_ref().to_string(), value.to_string()); - self - } - // Context GETTERS ***************************************************************************** /// Devuelve la solicitud HTTP asociada al documento. @@ -218,20 +229,28 @@ impl Context { /// /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla /// ([`ErrorParam::ParseError`]). - pub fn get_param(&self, key: impl AsRef) -> Result { + pub fn param(&self, key: impl AsRef) -> Result { self.params .get(key.as_ref()) .ok_or(ErrorParam::NotFound) .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone()))) } - // Context EXTRAS ****************************************************************************** + // Context RENDER ****************************************************************************** - /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó. - pub fn remove_param(&mut self, key: impl AsRef) -> bool { - self.params.remove(key.as_ref()).is_some() + /// Renderiza los recursos del contexto. + pub fn render_assets(&self) -> Markup { + html! { + @if let Some(favicon) = &self.favicon { + (favicon) + } + (self.stylesheets) + (self.javascripts) + } } + // Context EXTRAS ****************************************************************************** + /// Genera un identificador único si no se proporciona uno explícito. /// /// Si no se proporciona un `id`, se genera un identificador único en la forma `-` @@ -256,15 +275,3 @@ impl Context { } } } - -impl Render for Context { - fn render(&self) -> Markup { - html! { - @if let Some(favicon) = &self.favicon { - (favicon) - } - (self.stylesheets) - (self.javascripts) - } - } -} From b568db1a02c5f2ba813ab0cbdab7cdfb7eeb0da5 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 27 Jul 2025 00:33:32 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Corrige=20las=20refere?= =?UTF-8?q?ncias=20"builder"=20a=20self/Self?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/html/context.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/html/context.rs b/src/html/context.rs index a8d634a..0035b26 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -136,7 +136,7 @@ impl Context { /// Modifica el identificador de idioma del documento. #[builder_fn] - pub fn with_langid(&mut self, langid: &'static LanguageIdentifier) -> &mut Self { + pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self { self.langid = langid; self } @@ -146,21 +146,21 @@ impl Context { /// Localiza el tema por su [`short_name`](crate::core::AnyInfo::short_name), y si no aplica /// ninguno entonces usará el tema por defecto. #[builder_fn] - pub fn with_theme(&mut self, theme_name: impl AsRef) -> &mut Self { + pub fn with_theme(mut self, theme_name: impl AsRef) -> Self { self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); self } /// Define el tipo de composición usado para renderizar el documento. #[builder_fn] - pub fn with_layout(&mut self, layout_name: &'static str) -> &mut Self { + pub fn with_layout(mut self, layout_name: &'static str) -> Self { self.layout = layout_name; self } /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`]. #[builder_fn] - pub fn with_param(&mut self, key: impl AsRef, value: T) -> &mut Self { + pub fn with_param(mut self, key: impl AsRef, value: T) -> Self { self.params .insert(key.as_ref().to_string(), value.to_string()); self @@ -173,7 +173,7 @@ impl Context { /// Modifica información o recursos del contexto usando [`AssetsOp`]. #[builder_fn] - pub fn with_assets(&mut self, op: AssetsOp) -> &mut Self { + pub fn with_assets(mut self, op: AssetsOp) -> Self { match op { // Favicon. AssetsOp::SetFavicon(favicon) => { From 430ecada413cbb98b0e0f84e69d36f4fd5b569fb Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 27 Jul 2025 01:19:16 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Corrige=20validaci=C3=B3n=20?= =?UTF-8?q?"mut=20self"=20y=20retorno=20de=20Self?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/pagetop-macros/src/lib.rs | 43 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index ecca395..d9533fe 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -103,32 +103,16 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { }; return expanded.into(); } - // Valida que el método sea público. + // Valida que el método es público. if !matches!(fn_with.vis, syn::Visibility::Public(_)) { return quote_spanned! { fn_with.sig.ident.span() => compile_error!("expected method to be `pub`"); } .into(); } - // Valida que el método devuelva el tipo `Self`. - if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output { - if let syn::Type::Path(type_path) = &**ty { - let ident = &type_path.path.segments.last().unwrap().ident; - if ident != "Self" { - return quote_spanned! { - fn_with.sig.output.span() => compile_error!("expected return type to be `Self`"); - }.into(); - } - } - } else { - return quote_spanned! { - fn_with.sig.output.span() => compile_error!("expected method to return `Self`"); - } - .into(); - } - // Valida que el primer argumento sea `mut self`. + // Valida que el primer argumento es exactamente `mut self`. if let Some(syn::FnArg::Receiver(receiver)) = fn_with.sig.inputs.first() { - if receiver.mutability.is_none() { + if receiver.mutability.is_none() || receiver.reference.is_some() { return quote_spanned! { receiver.span() => compile_error!("expected `mut self` as the first argument"); } @@ -140,6 +124,27 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { } .into(); } + // Valida que el método devuelve exactamente `Self`. + if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output { + if let syn::Type::Path(type_path) = ty.as_ref() { + if type_path.qself.is_some() || !type_path.path.is_ident("Self") { + return quote_spanned! { ty.span() => + compile_error!("expected return type to be exactly `Self`"); + } + .into(); + } + } else { + return quote_spanned! { ty.span() => + compile_error!("expected return type to be exactly `Self`"); + } + .into(); + } + } else { + return quote_spanned! { + fn_with.sig.output.span() => compile_error!("expected method to return `Self`"); + } + .into(); + } // Genera el nombre del método alter_...(). let fn_alter_name_str = fn_with_name_str.replace("with_", "alter_");