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_"); 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..0035b26 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) -> 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 { 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) - } - } -}