diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 28c6b1b..0a60d53 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}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, ItemFn}; /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)). #[proc_macro] @@ -107,216 +107,114 @@ 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 { - 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(); + 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(); // 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`" + 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"); }; - let err = sig - .inputs - .first() - .map(|a| a.span()) - .unwrap_or(sig.ident.span()); + return expanded.into(); + } + // Valida que el método es público. + if !matches!(fn_with.vis, syn::Visibility::Public(_)) { return quote_spanned! { - err => compile_error!(#msg); + fn_with.sig.ident.span() => compile_error!("expected method to be `pub`"); } .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(); - } - }, - _ => { + // 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! { - sig.output.span() => compile_error!("expected return type to be exactly `Self`"); + receiver.span() => compile_error!("expected `mut self` as the first argument"); } .into(); } + } else { + return quote_spanned! { + fn_with.sig.ident.span() => compile_error!("expected `mut self` as the first argument"); + } + .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 stem = with_name_str.strip_prefix("with_").expect("validated"); - let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span()); + 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()); // Extrae genéricos y cláusulas where. - let generics = &sig.generics; - let where_clause = &sig.generics.where_clause; + let fn_generics = &fn_with.sig.generics; + let where_clause = &fn_with.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 - }; - - // Extrae atributos descartando la documentación para incluir en `alter_...()`. - let non_doc_attrs: Vec<_> = attrs + // 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 .iter() - .cloned() - .filter(|a| !a.path().is_ident("doc")) + .skip(1) + .map(|arg| match arg { + syn::FnArg::Typed(pat) => &pat.pat, + _ => panic!("unexpected argument type"), + }) .collect(); - // Documentación del método alter_...(). - let alter_doc = - format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*."); + // 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 + } + }; // Genera el código final. - 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 - } - } - } + let expanded = quote! { + #fn_with + #[inline] + #fn_alter }; expanded.into() } diff --git a/src/global.rs b/src/global.rs index ccc6d9d..6be0774 100644 --- a/src/global.rs +++ b/src/global.rs @@ -50,11 +50,12 @@ pub struct App { pub theme: String, /// Idioma por defecto para la aplicación. /// - /// 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"). + /// 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"). 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 9f3d70c..784457e 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 use assets::{Asset, Assets}; +pub(crate) use assets::Assets; // HTML DOCUMENT CONTEXT *************************************************************************** mod context; -pub use context::{AssetsOp, Context, Contextual, ErrorParam}; +pub use context::{AssetsOp, Context, ErrorParam}; // HTML ATTRIBUTES ********************************************************************************* diff --git a/src/html/assets.rs b/src/html/assets.rs index ee5431f..894b7e8 100644 --- a/src/html/assets.rs +++ b/src/html/assets.rs @@ -5,49 +5,22 @@ pub mod stylesheet; use crate::html::{html, Markup, Render}; use crate::{AutoDefault, Weight}; -/// 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. +pub trait AssetsTrait: Render { + // Devuelve el nombre del recurso, utilizado como clave única. fn name(&self) -> &str; - /// Devuelve el peso del recurso, usado para ordenar el renderizado de menor a mayor peso. + // Devuelve el peso del recurso, durante el renderizado se procesan 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 struct Assets(Vec); +pub(crate) struct Assets(Vec); -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. +impl Assets { pub fn new() -> Self { - Self(Vec::new()) + Assets::(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) => { @@ -66,9 +39,6 @@ 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); @@ -79,14 +49,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) + (a.render()) } } } diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index be6f906..db5754e 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -1,4 +1,4 @@ -use crate::html::assets::Asset; +use crate::html::assets::AssetsTrait; use crate::html::{html, Markup, Render}; use crate::{join, join_pair, AutoDefault, Weight}; @@ -137,10 +137,8 @@ impl JavaScript { } } -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. +impl AssetsTrait for JavaScript { + // Para *scripts* externos es la ruta; 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 38a97d7..bb60b01 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -1,4 +1,4 @@ -use crate::html::assets::Asset; +use crate::html::assets::AssetsTrait; use crate::html::{html, Markup, PreEscaped, Render}; use crate::{join_pair, AutoDefault, Weight}; @@ -142,10 +142,8 @@ impl StyleSheet { } } -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. +impl AssetsTrait for StyleSheet { + // Para hojas de estilos externas es la ruta; 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 79148b0..4ebd510 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`]) de un documento. +/// Operaciones para modificar el contexto ([`Context`]) del documento. pub enum AssetsOp { // Favicon. /// Define el *favicon* del documento. Sobrescribe cualquier valor anterior. @@ -47,101 +47,12 @@ pub enum ErrorParam { }, } -/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML. +/// Representa el contexto de un documento HTML. /// -/// `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. +/// 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. /// /// # Ejemplos /// @@ -196,7 +107,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 en ejecución. + params : HashMap<&'static str, (Box, &'static str)>, // Parámetros definidos en tiempo de ejecución. id_counter : usize, // Contador para generar identificadores únicos. } @@ -240,6 +151,80 @@ 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. @@ -255,6 +240,61 @@ 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: @@ -288,6 +328,23 @@ 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: @@ -323,21 +380,30 @@ impl Context { }) } - /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. + // Context EXTRAS ****************************************************************************** + + /// Genera un identificador único si no se proporciona uno explícito. /// - /// 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() + /// 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()) + } } } @@ -357,173 +423,3 @@ 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 77bc9c4..0942f8c 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -8,8 +8,7 @@ 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::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::html::{AssetsOp, Context, Contextual}; +use crate::html::{AssetsOp, Context}; use crate::html::{AttrClasses, ClassesOp}; use crate::html::{AttrId, AttrL10n}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; @@ -26,9 +25,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, } @@ -44,9 +43,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(), } } @@ -81,6 +80,40 @@ 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 { @@ -172,6 +205,25 @@ 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 @@ -181,19 +233,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 (`region_name`) de la página. + /// Renderiza los componentes de una región (`regiona_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) @@ -250,79 +302,3 @@ 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 be48e3e..ab56338 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,5 +1,4 @@ use crate::base::component::Html; -use crate::html::Contextual; use crate::locale::L10n; use crate::response::ResponseError; use crate::service::http::{header::ContentType, StatusCode};