diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index d9533fe..ecca395 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -103,16 +103,32 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { }; return expanded.into(); } - // Valida que el método es público. + // Valida que el método sea público. if !matches!(fn_with.vis, syn::Visibility::Public(_)) { return quote_spanned! { fn_with.sig.ident.span() => compile_error!("expected method to be `pub`"); } .into(); } - // Valida que el primer argumento es exactamente `mut self`. + // Valida que el método devuelva el tipo `Self`. + if let syn::ReturnType::Type(_, ty) = &fn_with.sig.output { + if let syn::Type::Path(type_path) = &**ty { + let ident = &type_path.path.segments.last().unwrap().ident; + if ident != "Self" { + return quote_spanned! { + fn_with.sig.output.span() => compile_error!("expected return type to be `Self`"); + }.into(); + } + } + } else { + return quote_spanned! { + fn_with.sig.output.span() => compile_error!("expected method to return `Self`"); + } + .into(); + } + // Valida que el primer argumento sea `mut self`. if let Some(syn::FnArg::Receiver(receiver)) = fn_with.sig.inputs.first() { - if receiver.mutability.is_none() || receiver.reference.is_some() { + if receiver.mutability.is_none() { return quote_spanned! { receiver.span() => compile_error!("expected `mut self` as the first argument"); } @@ -124,27 +140,6 @@ 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_"); diff --git a/src/html.rs b/src/html.rs index 82fa906..7f18ee8 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::{AssetsOp, Context, ErrorParam}; +pub use context::{Context, ContextOp, ErrorParam}; mod opt_id; pub use opt_id::OptionId; diff --git a/src/html/context.rs b/src/html/context.rs index 0035b26..4374b14 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}; +use crate::html::{html, Markup, Render}; 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,7 +14,17 @@ use std::str::FromStr; use std::fmt; /// Operaciones para modificar el contexto ([`Context`]) del documento. -pub enum AssetsOp { +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), + // Favicon. /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. SetFavicon(Option), @@ -66,35 +76,38 @@ impl Error for ErrorParam {} /// ```rust /// use pagetop::prelude::*; /// -/// fn configure_context(cx: &mut Context) { +/// fn configure_context(mut cx: Context) { /// // Establece el idioma del documento a español. -/// cx.alter_langid(LangMatch::langid_or_default("es-ES")) +/// cx.alter_assets(ContextOp::LangId( +/// LangMatch::langid_or_default("es-ES") +/// )) /// // Selecciona un tema (por su nombre corto). -/// .alter_theme("aliner") -/// // Añade un parámetro dinámico al contexto. -/// .alter_param("usuario_id", 42) +/// .alter_assets(ContextOp::Theme("aliner")) /// // Asigna un favicon. -/// .alter_assets(AssetsOp::SetFavicon(Some( +/// .alter_assets(ContextOp::SetFavicon(Some( /// Favicon::new().with_icon("/icons/favicon.ico") /// ))) /// // Añade una hoja de estilo externa. -/// .alter_assets(AssetsOp::AddStyleSheet( +/// .alter_assets(ContextOp::AddStyleSheet( /// StyleSheet::from("/css/style.css") /// )) /// // Añade un script JavaScript. -/// .alter_assets(AssetsOp::AddJavaScript( +/// .alter_assets(ContextOp::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"); /// -/// // 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`. +/// // Genera un identificador único 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. @@ -106,10 +119,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. } @@ -124,84 +137,60 @@ 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, } } - // Context BUILDER ***************************************************************************** - - /// Modifica el identificador de idioma del documento. - #[builder_fn] - pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> 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) -> 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) -> 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) -> 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) -> Self { + /// Modifica información o recursos del contexto usando [`ContextOp`]. + pub fn alter_assets(&mut self, op: ContextOp) -> &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. - AssetsOp::SetFavicon(favicon) => { + ContextOp::SetFavicon(favicon) => { self.favicon = favicon; } - AssetsOp::SetFaviconIfNone(icon) => { + ContextOp::SetFaviconIfNone(icon) => { if self.favicon.is_none() { self.favicon = Some(icon); } } // Stylesheets. - AssetsOp::AddStyleSheet(css) => { + ContextOp::AddStyleSheet(css) => { self.stylesheets.add(css); } - AssetsOp::RemoveStyleSheet(path) => { + ContextOp::RemoveStyleSheet(path) => { self.stylesheets.remove(path); } // JavaScripts. - AssetsOp::AddJavaScript(js) => { + ContextOp::AddJavaScript(js) => { self.javascripts.add(js); } - AssetsOp::RemoveJavaScript(path) => { + ContextOp::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. @@ -229,28 +218,20 @@ impl Context { /// /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla /// ([`ErrorParam::ParseError`]). - pub fn param(&self, key: impl AsRef) -> Result { + pub fn get_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 RENDER ****************************************************************************** - - /// 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 ****************************************************************************** + /// 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() + } + /// 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 `-` @@ -275,3 +256,15 @@ impl Context { } } } + +impl Render for Context { + fn render(&self) -> Markup { + html! { + @if let Some(favicon) = &self.favicon { + (favicon) + } + (self.stylesheets) + (self.javascripts) + } + } +}