diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 0a60d53..28c6b1b 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -39,7 +39,7 @@ mod smart_default; use proc_macro::TokenStream; use quote::{quote, quote_spanned}; -use syn::{parse_macro_input, spanned::Spanned, DeriveInput, ItemFn}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput}; /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)). #[proc_macro] @@ -107,114 +107,216 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// `alter_...()`, que permitirá más adelante modificar instancias existentes. #[proc_macro_attribute] pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { - let fn_with = parse_macro_input!(item as ItemFn); - let fn_with_name = fn_with.sig.ident.clone(); - let fn_with_name_str = fn_with.sig.ident.to_string(); + use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type}; + + 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 !fn_with_name_str.starts_with("with_") { - let expanded = quote_spanned! { - fn_with.sig.ident.span() => - compile_error!("expected a \"pub fn with_...(mut self, ...) -> Self\" method"); - }; - return expanded.into(); - } - // Valida que el método es público. - if !matches!(fn_with.vis, syn::Visibility::Public(_)) { + if !with_name_str.starts_with("with_") { return quote_spanned! { - fn_with.sig.ident.span() => compile_error!("expected method to be `pub`"); + sig.ident.span() => compile_error!("expected a named `with_...()` method"); } .into(); } - // 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() || receiver.reference.is_some() { - return quote_spanned! { - receiver.span() => compile_error!("expected `mut self` as the first argument"); + + // 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() } - .into(); } - } else { + _ => 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! { - fn_with.sig.ident.span() => compile_error!("expected `mut self` as the first argument"); + err => compile_error!(#msg); } .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`"); + 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(); } - } else { - 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`"); } .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_"); - let fn_alter_name = syn::Ident::new(&fn_alter_name_str, fn_with.sig.ident.span()); + 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 fn_generics = &fn_with.sig.generics; - let where_clause = &fn_with.sig.generics.where_clause; + let generics = &sig.generics; + let where_clause = &sig.generics.where_clause; - // Extrae argumentos y parámetros de llamada. - let args: Vec<_> = fn_with.sig.inputs.iter().skip(1).collect(); - let params: Vec<_> = fn_with - .sig - .inputs + // 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 + }; + + // Extrae atributos descartando la documentación para incluir en `alter_...()`. + let non_doc_attrs: Vec<_> = attrs .iter() - .skip(1) - .map(|arg| match arg { - syn::FnArg::Typed(pat) => &pat.pat, - _ => panic!("unexpected argument type"), - }) + .cloned() + .filter(|a| !a.path().is_ident("doc")) .collect(); - // Extrae bloque del método. - let fn_with_block = &fn_with.block; - - // Extrae documentación y otros atributos del método. - let fn_with_attrs = &fn_with.attrs; - - // Genera el método alter_...() con el código del método with_...(). - let fn_alter_doc = - format!("Equivalente a [`Self::{fn_with_name_str}()`], pero sin usar el patrón *builder*."); - - let fn_alter = quote! { - #[doc = #fn_alter_doc] - pub fn #fn_alter_name #fn_generics(&mut self, #(#args),*) -> &mut Self #where_clause { - #fn_with_block - } - }; - - // Redefine el método with_...() para que llame a alter_...(). - let fn_with = quote! { - #(#fn_with_attrs)* - #[inline] - pub fn #fn_with_name #fn_generics(mut self, #(#args),*) -> Self #where_clause { - self.#fn_alter_name(#(#params),*); - self - } - }; + // Documentación del método alter_...(). + let alter_doc = + format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*."); // Genera el código final. - let expanded = quote! { - #fn_with - #[inline] - #fn_alter + let expanded = match body_opt { + None => { + quote! { + #(#attrs)* + fn #with_name #generics (self, #(#args),*) -> Self #where_clause; + + #(#non_doc_attrs)* + #[doc = #alter_doc] + fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause; + } + } + Some(body) => { + let with_fn = if is_trait { + quote! { + #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause { + let mut s = self; + s.#alter_ident(#(#call_idents),*); + s + } + } + } else { + quote! { + #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause { + self.#alter_ident(#(#call_idents),*); + self + } + } + }; + quote! { + #(#attrs)* + #with_fn + + #(#non_doc_attrs)* + #[doc = #alter_doc] + #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause { + #body + } + } + } }; expanded.into() } diff --git a/src/global.rs b/src/global.rs index 6be0774..ccc6d9d 100644 --- a/src/global.rs +++ b/src/global.rs @@ -50,12 +50,11 @@ pub struct App { pub theme: String, /// Idioma por defecto para la aplicación. /// - /// Si no se especifica un valor válido, normalmente se usará el idioma devuelto por la - /// implementación de [`LangId`](crate::locale::LangId) para [`Context`](crate::html::Context), - /// en el siguiente orden: primero, el idioma establecido explícitamente con - /// [`Context::with_langid()`](crate::html::Context::with_langid); si no se ha definido, se - /// usará el indicado en la cabecera `Accept-Language` del navegador; y, si ninguno aplica, se - /// empleará el idioma de respaldo ("en-US"). + /// Si no está definido o no es válido, el idioma efectivo para el renderizado se resolverá + /// según la implementación de [`LangId`](crate::locale::LangId) en este orden: primero intenta + /// con el establecido en [`Contextual::with_langid()`](crate::html::Contextual::with_langid); + /// pero si no se ha definido explícitamente, usará el indicado en la cabecera `Accept-Language` + /// del navegador; y, si ninguno aplica, se empleará el idioma de respaldo ("en-US"). pub language: String, /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// *"Starwars"*. diff --git a/src/html.rs b/src/html.rs index 784457e..9f3d70c 100644 --- a/src/html.rs +++ b/src/html.rs @@ -9,12 +9,12 @@ mod assets; pub use assets::favicon::Favicon; pub use assets::javascript::JavaScript; pub use assets::stylesheet::{StyleSheet, TargetMedia}; -pub(crate) use assets::Assets; +pub use assets::{Asset, Assets}; // HTML DOCUMENT CONTEXT *************************************************************************** mod context; -pub use context::{AssetsOp, Context, ErrorParam}; +pub use context::{AssetsOp, Context, Contextual, ErrorParam}; // HTML ATTRIBUTES ********************************************************************************* diff --git a/src/html/assets.rs b/src/html/assets.rs index 894b7e8..ee5431f 100644 --- a/src/html/assets.rs +++ b/src/html/assets.rs @@ -5,22 +5,49 @@ pub mod stylesheet; use crate::html::{html, Markup, Render}; use crate::{AutoDefault, Weight}; -pub trait AssetsTrait: Render { - // Devuelve el nombre del recurso, utilizado como clave única. +/// Representación genérica de un *script* [`JavaScript`](crate::html::JavaScript) o una hoja de +/// estilos [`StyleSheet`](crate::html::StyleSheet). +/// +/// Estos recursos se incluyen en los conjuntos de recursos ([`Assets`]) que suelen renderizarse en +/// un documento HTML. +/// +/// Cada recurso se identifica por un **nombre único** ([`Asset::name()`]), usado como clave; y un +/// **peso** ([`Asset::weight()`]), que determina su orden relativo de renderizado. +pub trait Asset: Render { + /// Devuelve el nombre del recurso, utilizado como clave única. fn name(&self) -> &str; - // Devuelve el peso del recurso, durante el renderizado se procesan de menor a mayor peso. + /// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso. fn weight(&self) -> Weight; } +/// Gestión común para conjuntos de recursos como [`JavaScript`](crate::html::JavaScript) y +/// [`StyleSheet`](crate::html::StyleSheet). +/// +/// Se emplea normalmente para agrupar, administrar y renderizar los recursos de un documento HTML. +/// Cada recurso se identifica por un nombre único ([`Asset::name()`]) y tiene asociado un peso +/// ([`Asset::weight()`]) que determina su orden de renderizado. +/// +/// Durante el renderizado, los recursos se procesan en orden ascendente de peso. En caso de +/// igualdad, se respeta el orden de inserción. #[derive(AutoDefault)] -pub(crate) struct Assets(Vec); +pub struct Assets(Vec); -impl Assets { +impl Assets { + /// Crea un nuevo conjunto vacío de recursos. + /// + /// Normalmente no se instancia directamente, sino como parte de la gestión de recursos que + /// hacen páginas o temas. pub fn new() -> Self { - Assets::(Vec::::new()) + Self(Vec::new()) } + /// Inserta un recurso. + /// + /// Si no existe otro con el mismo nombre, lo añade. Si ya existe y su peso era mayor, lo + /// reemplaza. Y si su peso era menor o igual, entonces no realiza ningún cambio. + /// + /// Devuelve `true` si el recurso fue insertado o reemplazado. pub fn add(&mut self, asset: T) -> bool { match self.0.iter().position(|x| x.name() == asset.name()) { Some(index) => { @@ -39,6 +66,9 @@ impl Assets { } } + /// Elimina un recurso por nombre. + /// + /// Devuelve `true` si el recurso existía y fue eliminado. pub fn remove(&mut self, name: impl AsRef) -> bool { if let Some(index) = self.0.iter().position(|x| x.name() == name.as_ref()) { self.0.remove(index); @@ -49,14 +79,14 @@ impl Assets { } } -impl Render for Assets { +impl Render for Assets { fn render(&self) -> Markup { let mut assets = self.0.iter().collect::>(); assets.sort_by_key(|a| a.weight()); html! { @for a in assets { - (a.render()) + (a) } } } diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index db5754e..be6f906 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -1,4 +1,4 @@ -use crate::html::assets::AssetsTrait; +use crate::html::assets::Asset; use crate::html::{html, Markup, Render}; use crate::{join, join_pair, AutoDefault, Weight}; @@ -137,8 +137,10 @@ impl JavaScript { } } -impl AssetsTrait for JavaScript { - // Para *scripts* externos es la ruta; para *scripts* embebidos, un identificador. +impl Asset for JavaScript { + /// Devuelve el nombre del recurso, utilizado como clave única. + /// + /// Para *scripts* externos es la ruta del recurso; para *scripts* embebidos, un identificador. fn name(&self) -> &str { match &self.source { Source::From(path) => path, diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index bb60b01..38a97d7 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -1,4 +1,4 @@ -use crate::html::assets::AssetsTrait; +use crate::html::assets::Asset; use crate::html::{html, Markup, PreEscaped, Render}; use crate::{join_pair, AutoDefault, Weight}; @@ -142,8 +142,10 @@ impl StyleSheet { } } -impl AssetsTrait for StyleSheet { - // Para hojas de estilos externas es la ruta; para las embebidas, un identificador. +impl Asset for StyleSheet { + /// Devuelve el nombre del recurso, utilizado como clave única. + /// + /// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador. fn name(&self) -> &str { match &self.source { Source::From(path) => path, diff --git a/src/html/context.rs b/src/html/context.rs index 4ebd510..79148b0 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -10,7 +10,7 @@ use crate::{builder_fn, join}; use std::any::Any; use std::collections::HashMap; -/// Operaciones para modificar el contexto ([`Context`]) del documento. +/// Operaciones para modificar el contexto ([`Context`]) de un documento. pub enum AssetsOp { // Favicon. /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. @@ -47,12 +47,101 @@ pub enum ErrorParam { }, } -/// Representa el contexto de un documento HTML. +/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML. /// -/// Se crea internamente para manejar información relevante del documento, como la solicitud HTTP de -/// origen, el idioma, tema y composición para el renderizado, los recursos *favicon* ([`Favicon`]), -/// hojas de estilo ([`StyleSheet`]) y *scripts* ([`JavaScript`]), así como *parámetros dinámicos -/// heterogéneos* de contexto definidos en tiempo de ejecución. +/// `Contextual` extiende [`LangId`] y define los métodos para: +/// +/// - Establecer el **idioma** del documento. +/// - Almacenar la **solicitud HTTP** de origen. +/// - Seleccionar **tema** y **composición** (*layout*) de renderizado. +/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo +/// [`StyleSheet`] o los *scripts* [`JavaScript`] mediante [`AssetsOp`]. +/// - Leer y mantener **parámetros dinámicos tipados** de contexto. +/// - Generar **identificadores únicos** por tipo de componente. +/// +/// Lo implementan, típicamente, estructuras que representan el contexto de renderizado, como +/// [`Context`](crate::html::Context) o [`Page`](crate::response::page::Page). +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// fn prepare_context(cx: C) -> C { +/// cx.with_langid(&LangMatch::resolve("es-ES")) +/// .with_theme("aliner") +/// .with_layout("default") +/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico")))) +/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css"))) +/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js"))) +/// .with_param("usuario_id", 42_i32) +/// } +/// ``` +pub trait Contextual: LangId { + // Contextual BUILDER ************************************************************************** + + /// Establece el idioma del documento. + #[builder_fn] + fn with_langid(self, language: &impl LangId) -> Self; + + /// Almacena la solicitud HTTP de origen en el contexto. + #[builder_fn] + fn with_request(self, request: Option) -> Self; + + /// Especifica el tema para renderizar el documento. + #[builder_fn] + fn with_theme(self, theme_name: &'static str) -> Self; + + /// Especifica la composición para renderizar el documento. + #[builder_fn] + fn with_layout(self, layout_name: &'static str) -> Self; + + /// Añade o modifica un parámetro dinámico del contexto. + #[builder_fn] + fn with_param(self, key: &'static str, value: T) -> Self; + + /// Define los recursos del contexto usando [`AssetsOp`]. + #[builder_fn] + fn with_assets(self, op: AssetsOp) -> Self; + + // Contextual GETTERS ************************************************************************** + + /// Devuelve una referencia a la solicitud HTTP asociada, si existe. + fn request(&self) -> Option<&HttpRequest>; + + /// Devuelve el tema que se usará para renderizar el documento. + fn theme(&self) -> ThemeRef; + + /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`. + fn layout(&self) -> &str; + + /// Recupera un parámetro como [`Option`]. + fn param(&self, key: &'static str) -> Option<&T>; + + /// Devuelve el Favicon de los recursos del contexto. + fn favicon(&self) -> Option<&Favicon>; + + /// Devuelve las hojas de estilo de los recursos del contexto. + fn stylesheets(&self) -> &Assets; + + /// Devuelve los *scripts* JavaScript de los recursos del contexto. + fn javascripts(&self) -> &Assets; + + // Contextual HELPERS ************************************************************************** + + /// Genera un identificador único por tipo (`-`) cuando no se aporta uno explícito. + /// + /// Es útil para componentes u otros elementos HTML que necesitan un identificador predecible si + /// no se proporciona ninguno. + fn required_id(&mut self, id: Option) -> String; +} + +/// Implementa un **contexto de renderizado** para un documento HTML. +/// +/// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto, +/// **renderizar los recursos** del documento (incluyendo el [`Favicon`], las hojas de estilo +/// [`StyleSheet`] y los *scripts* [`JavaScript`]), o extender el uso de **parámetros dinámicos +/// tipados** con nuevos métodos. /// /// # Ejemplos /// @@ -107,7 +196,7 @@ pub struct Context { favicon : Option, // Favicon, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // Scripts JavaScript. - params : HashMap<&'static str, (Box, &'static str)>, // Parámetros definidos en tiempo de ejecución. + params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. id_counter : usize, // Contador para generar identificadores únicos. } @@ -151,80 +240,6 @@ impl Context { } } - // Context BUILDER ***************************************************************************** - - /// Modifica la fuente de idioma del documento. - #[builder_fn] - pub fn with_langid(mut self, language: &impl LangId) -> Self { - self.langid = language.langid(); - self - } - - /// Modifica 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: &'static str) -> Self { - self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); - self - } - - /// Modifica la composición para renderizar el documento. - #[builder_fn] - pub fn with_layout(mut self, layout_name: &'static str) -> Self { - self.layout = layout_name; - self - } - - /// Define los recursos del contexto usando [`AssetsOp`]. - #[builder_fn] - pub fn with_assets(mut self, op: AssetsOp) -> Self { - match op { - // Favicon. - AssetsOp::SetFavicon(favicon) => { - self.favicon = favicon; - } - AssetsOp::SetFaviconIfNone(icon) => { - if self.favicon.is_none() { - self.favicon = Some(icon); - } - } - // Stylesheets. - AssetsOp::AddStyleSheet(css) => { - self.stylesheets.add(css); - } - AssetsOp::RemoveStyleSheet(path) => { - self.stylesheets.remove(path); - } - // JavaScripts. - AssetsOp::AddJavaScript(js) => { - self.javascripts.add(js); - } - AssetsOp::RemoveJavaScript(path) => { - self.javascripts.remove(path); - } - } - self - } - - // Context GETTERS ***************************************************************************** - - /// Devuelve una referencia a la solicitud HTTP asociada, si existe. - pub fn request(&self) -> Option<&HttpRequest> { - self.request.as_ref() - } - - /// Devuelve el tema que se usará para renderizar el documento. - pub fn theme(&self) -> ThemeRef { - self.theme - } - - /// Devuelve la composición para renderizar el documento. Por defecto es `"default"`. - pub fn layout(&self) -> &str { - self.layout - } - // Context RENDER ****************************************************************************** /// Renderiza los recursos del contexto. @@ -240,61 +255,6 @@ impl Context { // Context PARAMS ****************************************************************************** - /// Añade o modifica un parámetro dinámico del contexto. - /// - /// El valor se guarda conservando el *nombre del tipo* real para mejorar los mensajes de error - /// posteriores. - /// - /// # Ejemplos - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// let cx = Context::new(None) - /// .with_param("usuario_id", 42_i32) - /// .with_param("titulo", String::from("Hola")) - /// .with_param("flags", vec!["a", "b"]); - /// ``` - #[builder_fn] - pub fn with_param(mut self, key: &'static str, value: T) -> Self { - let type_name = TypeInfo::FullName.of::(); - self.params.insert(key, (Box::new(value), type_name)); - self - } - - /// Recupera un parámetro como [`Option`], simplificando el acceso. - /// - /// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información - /// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor - /// guardado no coincide con el tipo solicitado. - /// - /// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo - /// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo. - /// - /// # Ejemplo - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// let cx = Context::new(None).with_param("username", String::from("Alice")); - /// - /// // Devuelve Some(&String) si existe y coincide el tipo. - /// assert_eq!(cx.param::("username").map(|s| s.as_str()), Some("Alice")); - /// - /// // Devuelve None si no existe o si el tipo no coincide. - /// assert!(cx.param::("username").is_none()); - /// assert!(cx.param::("missing").is_none()); - /// - /// // Acceso con valor por defecto. - /// let user = cx.param::("missing") - /// .cloned() - /// .unwrap_or_else(|| "visitor".to_string()); - /// assert_eq!(user, "visitor"); - /// ``` - pub fn param(&self, key: &'static str) -> Option<&T> { - self.get_param::(key).ok() - } - /// Recupera una *referencia tipada* al parámetro solicitado. /// /// Devuelve: @@ -328,23 +288,6 @@ impl Context { }) } - /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. - /// - /// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada. - /// - /// # Ejemplos - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// let mut cx = Context::new(None).with_param("temp", 1u8); - /// assert!(cx.remove_param("temp")); - /// assert!(!cx.remove_param("temp")); // ya no existe - /// ``` - pub fn remove_param(&mut self, key: &'static str) -> bool { - self.params.remove(key).is_some() - } - /// Recupera el parámetro solicitado y lo elimina del contexto. /// /// Devuelve: @@ -380,30 +323,21 @@ impl Context { }) } - // Context EXTRAS ****************************************************************************** - - /// Genera un identificador único si no se proporciona uno explícito. + /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. /// - /// Si no se proporciona un `id`, se genera un identificador único en la forma `-` - /// donde `` es el nombre corto del tipo en minúsculas (sin espacios) y `` es un - /// contador interno incremental. - pub fn required_id(&mut self, id: Option) -> String { - if let Some(id) = id { - id - } else { - let prefix = TypeInfo::ShortName - .of::() - .trim() - .replace(' ', "_") - .to_lowercase(); - let prefix = if prefix.is_empty() { - "prefix".to_owned() - } else { - prefix - }; - self.id_counter += 1; - join!(prefix, "-", self.id_counter.to_string()) - } + /// Devuelve `false` en caso contrario. Usar cuando solo interesa borrar la entrada. + /// + /// # Ejemplos + /// + /// ```rust + /// use pagetop::prelude::*; + /// + /// let mut cx = Context::new(None).with_param("temp", 1u8); + /// assert!(cx.remove_param("temp")); + /// assert!(!cx.remove_param("temp")); // ya no existe + /// ``` + pub fn remove_param(&mut self, key: &'static str) -> bool { + self.params.remove(key).is_some() } } @@ -423,3 +357,173 @@ impl LangId for Context { self.langid } } + +impl Contextual for Context { + // Contextual BUILDER ************************************************************************** + + #[builder_fn] + fn with_request(mut self, request: Option) -> Self { + self.request = request; + self + } + + #[builder_fn] + fn with_langid(mut self, language: &impl LangId) -> Self { + self.langid = language.langid(); + self + } + + /// Asigna el tema 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] + fn with_theme(mut self, theme_name: &'static str) -> Self { + self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME); + self + } + + #[builder_fn] + fn with_layout(mut self, layout_name: &'static str) -> Self { + self.layout = layout_name; + self + } + + /// Añade o modifica un parámetro dinámico del contexto. + /// + /// El valor se guarda conservando el *nombre del tipo* real para mejorar los mensajes de error + /// posteriores. + /// + /// # Ejemplos + /// + /// ```rust + /// use pagetop::prelude::*; + /// + /// let cx = Context::new(None) + /// .with_param("usuario_id", 42_i32) + /// .with_param("titulo", String::from("Hola")) + /// .with_param("flags", vec!["a", "b"]); + /// ``` + #[builder_fn] + fn with_param(mut self, key: &'static str, value: T) -> Self { + let type_name = TypeInfo::FullName.of::(); + self.params.insert(key, (Box::new(value), type_name)); + self + } + + #[builder_fn] + fn with_assets(mut self, op: AssetsOp) -> Self { + match op { + // Favicon. + AssetsOp::SetFavicon(favicon) => { + self.favicon = favicon; + } + AssetsOp::SetFaviconIfNone(icon) => { + if self.favicon.is_none() { + self.favicon = Some(icon); + } + } + // Stylesheets. + AssetsOp::AddStyleSheet(css) => { + self.stylesheets.add(css); + } + AssetsOp::RemoveStyleSheet(path) => { + self.stylesheets.remove(path); + } + // JavaScripts. + AssetsOp::AddJavaScript(js) => { + self.javascripts.add(js); + } + AssetsOp::RemoveJavaScript(path) => { + self.javascripts.remove(path); + } + } + self + } + + // Contextual GETTERS ************************************************************************** + + fn request(&self) -> Option<&HttpRequest> { + self.request.as_ref() + } + + fn theme(&self) -> ThemeRef { + self.theme + } + + fn layout(&self) -> &str { + self.layout + } + + /// Recupera un parámetro como [`Option`], simplificando el acceso. + /// + /// A diferencia de [`get_param`](Self::get_param), que devuelve un [`Result`] con información + /// detallada de error, este método devuelve `None` tanto si la clave no existe como si el valor + /// guardado no coincide con el tipo solicitado. + /// + /// Resulta útil en escenarios donde sólo interesa saber si el valor existe y es del tipo + /// correcto, sin necesidad de diferenciar entre error de ausencia o de tipo. + /// + /// # Ejemplo + /// + /// ```rust + /// use pagetop::prelude::*; + /// + /// let cx = Context::new(None).with_param("username", String::from("Alice")); + /// + /// // Devuelve Some(&String) si existe y coincide el tipo. + /// assert_eq!(cx.param::("username").map(|s| s.as_str()), Some("Alice")); + /// + /// // Devuelve None si no existe o si el tipo no coincide. + /// assert!(cx.param::("username").is_none()); + /// assert!(cx.param::("missing").is_none()); + /// + /// // Acceso con valor por defecto. + /// let user = cx.param::("missing") + /// .cloned() + /// .unwrap_or_else(|| "visitor".to_string()); + /// assert_eq!(user, "visitor"); + /// ``` + fn param(&self, key: &'static str) -> Option<&T> { + self.get_param::(key).ok() + } + + fn favicon(&self) -> Option<&Favicon> { + self.favicon.as_ref() + } + + fn stylesheets(&self) -> &Assets { + &self.stylesheets + } + + fn javascripts(&self) -> &Assets { + &self.javascripts + } + + // Contextual HELPERS ************************************************************************** + + /// Devuelve un identificador único dentro del contexto para el tipo `T`, si no se proporciona + /// un `id` explícito. + /// + /// Si no se proporciona un `id`, se genera un identificador único en la forma `-` + /// donde `` es el nombre corto del tipo en minúsculas (sin espacios) y `` es un + /// contador interno incremental. + fn required_id(&mut self, id: Option) -> String { + if let Some(id) = id { + id + } else { + let prefix = TypeInfo::ShortName + .of::() + .trim() + .replace(' ', "_") + .to_lowercase(); + let prefix = if prefix.is_empty() { + "prefix".to_owned() + } else { + prefix + }; + self.id_counter += 1; + join!(prefix, "-", self.id_counter.to_string()) + } + } +} diff --git a/src/response/page.rs b/src/response/page.rs index 0942f8c..77bc9c4 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -8,7 +8,8 @@ use crate::builder_fn; use crate::core::component::{Child, ChildOp, Component}; use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; use crate::html::{html, Markup, DOCTYPE}; -use crate::html::{AssetsOp, Context}; +use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::html::{AssetsOp, Context, Contextual}; use crate::html::{AttrClasses, ClassesOp}; use crate::html::{AttrId, AttrL10n}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; @@ -25,9 +26,9 @@ pub struct Page { description : AttrL10n, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, - context : Context, body_id : AttrId, body_classes: AttrClasses, + context : Context, regions : ChildrenInRegions, } @@ -43,9 +44,9 @@ impl Page { description : AttrL10n::default(), metadata : Vec::default(), properties : Vec::default(), - context : Context::new(request), body_id : AttrId::default(), body_classes: AttrClasses::default(), + context : Context::new(request), regions : ChildrenInRegions::default(), } } @@ -80,40 +81,6 @@ impl Page { self } - /// Modifica la fuente de idioma de la página ([`Context::with_langid()`]). - #[builder_fn] - pub fn with_langid(mut self, language: &impl LangId) -> Self { - self.context.alter_langid(language); - self - } - - /// Modifica el tema que se usará para renderizar la página ([`Context::with_theme()`]). - #[builder_fn] - pub fn with_theme(mut self, theme_name: &'static str) -> Self { - self.context.alter_theme(theme_name); - self - } - - /// Modifica la composición para renderizar la página ([`Context::with_layout()`]). - #[builder_fn] - pub fn with_layout(mut self, layout_name: &'static str) -> Self { - self.context.alter_layout(layout_name); - self - } - - /// Define los recursos de la página usando [`AssetsOp`]. - #[builder_fn] - pub fn with_assets(mut self, op: AssetsOp) -> Self { - self.context.alter_assets(op); - self - } - - #[builder_fn] - pub fn with_param(mut self, key: &'static str, value: T) -> Self { - self.context.alter_param(key, value); - self - } - /// Establece el atributo `id` del elemento ``. #[builder_fn] pub fn with_body_id(mut self, id: impl AsRef) -> Self { @@ -205,25 +172,6 @@ impl Page { &self.properties } - /// Devuelve la solicitud HTTP asociada. - pub fn request(&self) -> Option<&HttpRequest> { - self.context.request() - } - - /// Devuelve el tema que se usará para renderizar la página. - pub fn theme(&self) -> ThemeRef { - self.context.theme() - } - - /// Devuelve la composición para renderizar la página. Por defecto es `"default"`. - pub fn layout(&self) -> &str { - self.context.layout() - } - - pub fn param(&self, key: &'static str) -> Option<&T> { - self.context.param(key) - } - /// Devuelve el identificador del elemento ``. pub fn body_id(&self) -> &AttrId { &self.body_id @@ -233,19 +181,19 @@ impl Page { pub fn body_classes(&self) -> &AttrClasses { &self.body_classes } - /* - /// Devuelve una referencia mutable al [`Context`] de la página. - /// - /// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema, - /// *layout*, recursos, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente - /// o un tema necesita recibir el contexto como parámetro. - pub fn context(&mut self) -> &mut Context { - &mut self.context - } - */ + + /// Devuelve una referencia mutable al [`Context`] de la página. + /// + /// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema, + /// *layout*, recursos, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente + /// o un tema necesita recibir el contexto como parámetro. + pub fn context(&mut self) -> &mut Context { + &mut self.context + } + // Page RENDER ********************************************************************************* - /// Renderiza los componentes de una región (`regiona_name`) de la página. + /// Renderiza los componentes de una región (`region_name`) de la página. pub fn render_region(&mut self, region_name: &'static str) -> Markup { self.regions .merge_all_components(self.context.theme(), region_name) @@ -302,3 +250,79 @@ impl LangId for Page { self.context.langid() } } + +impl Contextual for Page { + // Contextual BUILDER ************************************************************************** + + #[builder_fn] + fn with_request(mut self, request: Option) -> Self { + self.context.alter_request(request); + self + } + + #[builder_fn] + fn with_langid(mut self, language: &impl LangId) -> Self { + self.context.alter_langid(language); + self + } + + #[builder_fn] + fn with_theme(mut self, theme_name: &'static str) -> Self { + self.context.alter_theme(theme_name); + self + } + + #[builder_fn] + fn with_layout(mut self, layout_name: &'static str) -> Self { + self.context.alter_layout(layout_name); + self + } + + #[builder_fn] + fn with_param(mut self, key: &'static str, value: T) -> Self { + self.context.alter_param(key, value); + self + } + + #[builder_fn] + fn with_assets(mut self, op: AssetsOp) -> Self { + self.context.alter_assets(op); + self + } + + // Contextual GETTERS ************************************************************************** + + fn request(&self) -> Option<&HttpRequest> { + self.context.request() + } + + fn theme(&self) -> ThemeRef { + self.context.theme() + } + + fn layout(&self) -> &str { + self.context.layout() + } + + fn param(&self, key: &'static str) -> Option<&T> { + self.context.param(key) + } + + fn favicon(&self) -> Option<&Favicon> { + self.context.favicon() + } + + fn stylesheets(&self) -> &Assets { + self.context.stylesheets() + } + + fn javascripts(&self) -> &Assets { + self.context.javascripts() + } + + // Contextual HELPERS ************************************************************************** + + fn required_id(&mut self, id: Option) -> String { + self.context.required_id::(id) + } +} diff --git a/src/response/page/error.rs b/src/response/page/error.rs index ab56338..be48e3e 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,4 +1,5 @@ use crate::base::component::Html; +use crate::html::Contextual; use crate::locale::L10n; use crate::response::ResponseError; use crate::service::http::{header::ContentType, StatusCode};