♻️ (pagetop): Sustituye StyleSheet::inline

- `StyleSheet::inline` se resuelve ahora con `ResponsiveStyles`.
- `StyleSheet` se simplifica a sólo hojas de estilo externas.
- `ResponsiveStyles` añade `add_styles()` para declarar varias
  propiedades en una sola llamada.
- `AssetsOp` gana un constructor por variante y conversiones
  "From<Favicon/Preload/StyleSheet/JavaScript>" para pasarlos
  directamente a `with_assets()`.
This commit is contained in:
Manuel Cillero 2026-09-12 20:44:17 +02:00
parent 0b8f3f3000
commit 64113aff09
21 changed files with 764 additions and 497 deletions

View file

@ -2,246 +2,28 @@ use crate::auth::CurrentUser;
use crate::core::TypeInfo;
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
use crate::core::theme::all::DEFAULT_THEME;
use crate::core::theme::{Breakpoint, ChildrenInRegions, CoreRegions, CoreTemplates};
use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates};
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet};
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
use crate::locale::Lc;
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
use crate::web::HttpRequest;
use crate::{CowStr, builder_impl, util};
use crate::{builder_impl, util};
use parking_lot::Mutex;
use thiserror::Error;
use std::any::{Any, TypeId};
use std::collections::HashMap;
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
pub enum AssetsOp {
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
SetFavicon(Option<Favicon>),
/// Define el *favicon* solo si no se ha establecido previamente.
SetFaviconIfNone(Favicon),
mod assets_op;
pub use assets_op::AssetsOp;
/// Añade un recurso para precarga al documento.
AddPreload(Preload),
/// Elimina un recurso para precarga por su ruta.
RemovePreload(&'static str),
mod error;
pub use error::ContextError;
/// Añade una hoja de estilos CSS al documento.
AddStyleSheet(StyleSheet),
/// Elimina una hoja de estilos por su ruta o identificador.
RemoveStyleSheet(&'static str),
/// Añade un script JavaScript al documento.
AddJavaScript(JavaScript),
/// Elimina un script por su ruta o identificador.
RemoveJavaScript(&'static str),
/// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas,
/// dentro del punto de corte dado (`None` para una regla siempre activa). Ver
/// [`ResponsiveStyles::add_style()`].
AddResponsiveStyle(Option<Breakpoint>, CowStr, CowStr, CowStr),
}
/// Errores de acceso a parámetros dinámicos del contexto.
#[derive(Debug, Error)]
pub enum ContextError {
/// La clave no existe.
#[error("parameter not found")]
ParamNotFound,
/// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye
/// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`)
/// para facilitar el diagnóstico.
#[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")]
ParamTypeMismatch {
key: &'static str,
expected: &'static str,
saved: &'static str,
},
}
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
///
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
///
/// - Almacenar la **petición HTTP** de origen.
/// - Seleccionar la **plantilla** y el **tema** 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.
///
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page).
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_aliner::Aliner;
/// fn prepare_context<C: Contextual>(cx: C) -> C {
/// cx.with_langid(&Locale::resolve("es-ES"))
/// .with_template(&CoreTemplates::Standard)
/// .with_theme(&Aliner)
/// .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("user_id", 42_i32)
/// }
/// ```
#[builder_impl]
pub trait Contextual: LangId {
// **< Contextual BUILDER >*********************************************************************
/// Establece el idioma del documento.
fn with_langid(self, language: &impl LangId) -> Self;
/// Almacena la petición HTTP de origen en el contexto.
///
/// También recalcula el idioma ([`RequestLocale::from_request()`]) y
/// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando
/// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya
/// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en
/// la cadena de construcción, nunca después.
fn with_request(self, request: Option<HttpRequest>) -> Self;
/// Especifica la plantilla para renderizar el documento.
fn with_template(self, template: TemplateRef) -> Self;
/// Especifica el tema para renderizar el documento.
fn with_theme(self, theme: ThemeRef) -> Self;
/// Añade o modifica un parámetro dinámico del contexto.
///
/// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
/// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no
/// coincide.
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let cx = Context::default()
/// .with_param("user_id", 42_i32)
/// .with_param("title", "Hello".to_string())
/// .with_param("flags", vec!["a", "b"]);
/// ```
fn with_param<T: Send + Sync + 'static>(self, key: &'static str, value: T) -> Self;
/// Define los recursos del contexto usando [`AssetsOp`].
fn with_assets(self, op: AssetsOp) -> Self;
/// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento `<body>`.
fn with_body_props(self, op: PropsOp) -> Self;
/// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del
/// documento.
fn with_child(self, op: impl Into<ChildOp>) -> Self;
/// Añade un componente o aplica una operación [`ChildOp`] en una región específica del
/// documento.
fn with_child_in(self, region: RegionRef, op: impl Into<ChildOp>) -> Self;
// **< Contextual GETTERS >*********************************************************************
/// Devuelve una referencia a la petición HTTP asociada, si existe.
fn request(&self) -> Option<&HttpRequest>;
/// Devuelve la identidad del usuario actual.
///
/// Si ninguna extensión de autenticación ha inyectado un
/// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve
/// `&CurrentUser::Anonymous`.
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// async fn greet(request: HttpRequest) -> Result<Markup, ErrorPage> {
/// let mut page = Page::new(request);
/// if page.current_user().is_authenticated() {
/// // Personalizar la página para el usuario autenticado.
/// }
/// page.render().await
/// }
/// ```
fn current_user(&self) -> &CurrentUser;
/// Devuelve la plantilla configurada para renderizar el documento.
fn template(&self) -> TemplateRef;
/// Devuelve el tema que se usará para renderizar el documento.
fn theme(&self) -> ThemeRef;
/// Recupera una *referencia tipada* al parámetro solicitado.
///
/// Devuelve:
///
/// - `Ok(&T)` si la clave existe y el tipo coincide.
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let cx = Context::default()
/// .with_param("user_id", 42_i32)
/// .with_param("title", "Hello".to_string());
///
/// let id: i32 = *cx.param("user_id").unwrap();
/// let title: &String = cx.param("title").unwrap();
///
/// // Error de tipo:
/// assert!(cx.param::<String>("user_id").is_err());
/// ```
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError>;
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
fn param_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
self.param::<T>(key).ok().cloned().unwrap_or_default()
}
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
self.param::<T>(key).ok().cloned().unwrap_or(default)
}
/// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe.
fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
self.param::<T>(key).ok().cloned().unwrap_or_else(f)
}
/// 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<StyleSheet>;
/// Devuelve los scripts JavaScript de los recursos del contexto.
fn javascripts(&self) -> &Assets<JavaScript>;
/// Devuelve los estilos *responsive* acumulados en el contexto.
fn responsive_styles(&self) -> &ResponsiveStyles;
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
fn body_props(&self) -> &Props;
// **< Contextual HELPERS >*********************************************************************
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let mut cx = Context::default().with_param("temp", 1u8);
/// assert!(cx.remove_param("temp"));
/// assert!(!cx.remove_param("temp")); // ya no existe
/// ```
fn remove_param(&mut self, key: &'static str) -> bool;
}
mod contextual;
pub use contextual::Contextual;
/// Implementa un **contexto de renderizado** para un documento HTML.
///
@ -280,11 +62,11 @@ pub trait Contextual: LangId {
/// // Establece el tema para renderizar.
/// .with_theme(&Aliner)
/// // Asigna un favicon.
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
/// .with_assets(Favicon::new().with_icon("/favicon.ico"))
/// // Añade una hoja de estilo externa.
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/style.css")))
/// .with_assets(StyleSheet::from("/css/style.css"))
/// // Añade un script JavaScript.
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
/// .with_assets(JavaScript::defer("/js/main.js"))
/// // Añade un parámetro dinámico al contexto.
/// .with_param("user_id", 42);
/// # cx }
@ -586,8 +368,8 @@ impl Contextual for Context {
self
}
fn with_assets(mut self, op: AssetsOp) -> Self {
match op {
fn with_assets(mut self, op: impl Into<AssetsOp>) -> Self {
match op.into() {
// Favicon.
AssetsOp::SetFavicon(favicon) => {
self.favicon = favicon;
@ -623,6 +405,9 @@ impl Contextual for Context {
self.responsives
.add_style(breakpoint, classes, property, value);
}
AssetsOp::AddResponsiveStyles(breakpoint, classes, styles) => {
self.responsives.add_styles(breakpoint, classes, styles);
}
}
self
}

View file

@ -0,0 +1,169 @@
use crate::CowStr;
use crate::core::theme::Breakpoint;
use crate::html::{Favicon, JavaScript, Preload, StyleSheet};
/// Operaciones para modificar recursos asociados al [`Context`](super::Context) de un documento.
///
/// [`Favicon`], [`Preload`], [`StyleSheet`] y [`JavaScript`] se convierten implícitamente en la
/// operación de añadir correspondiente (ver sus `impl From<...>` más abajo), por lo que no
/// necesitarían ningún constructor. Para el resto de operaciones, el método recomendado es recurrir
/// a los constructores asociados como [`remove_stylesheet()`], [`add_responsive_style()`], etc.
///
/// [`remove_stylesheet()`]: Self::remove_stylesheet
/// [`add_responsive_style()`]: Self::add_responsive_style
pub enum AssetsOp {
/// Define el *favicon* del documento. Sobrescribe cualquier valor anterior.
SetFavicon(Option<Favicon>),
/// Define el *favicon* sólo si no se ha establecido previamente.
SetFaviconIfNone(Favicon),
/// Añade un recurso para precarga al documento.
AddPreload(Preload),
/// Elimina un recurso para precarga por su ruta.
RemovePreload(&'static str),
/// Añade una hoja de estilos CSS al documento.
AddStyleSheet(StyleSheet),
/// Elimina una hoja de estilos por su ruta.
RemoveStyleSheet(&'static str),
/// Añade un script JavaScript al documento.
AddJavaScript(JavaScript),
/// Elimina un script por su ruta o identificador.
RemoveJavaScript(&'static str),
/// Añade una declaración de estilo *responsive* (`property: value`) para las clases indicadas,
/// para un punto de corte dado (`None` para una regla siempre activa). Ver
/// [`ResponsiveStyles::add_style()`](crate::html::ResponsiveStyles::add_style).
AddResponsiveStyle(Option<Breakpoint>, CowStr, CowStr, CowStr),
/// Añade varias declaraciones de estilo *responsive* (`property: value`) para las clases
/// indicadas, para un punto de corte dado, en una única llamada. Ver
/// [`ResponsiveStyles::add_styles()`](crate::html::ResponsiveStyles::add_styles).
AddResponsiveStyles(Option<Breakpoint>, CowStr, Vec<(CowStr, CowStr)>),
}
impl AssetsOp {
/// Crea la variante [`SetFavicon`](Self::SetFavicon) con el favicon indicado, o `None` para
/// eliminar cualquier favicon ya establecido.
pub fn set_favicon(favicon: impl Into<Option<Favicon>>) -> Self {
Self::SetFavicon(favicon.into())
}
/// Crea la variante [`SetFaviconIfNone`](Self::SetFaviconIfNone) con el favicon indicado.
pub fn set_favicon_if_none(favicon: Favicon) -> Self {
Self::SetFaviconIfNone(favicon)
}
/// Crea la variante [`AddPreload`](Self::AddPreload) con el recurso indicado.
pub fn add_preload(preload: Preload) -> Self {
Self::AddPreload(preload)
}
/// Crea la variante [`RemovePreload`](Self::RemovePreload) para la ruta indicada.
pub fn remove_preload(path: &'static str) -> Self {
Self::RemovePreload(path)
}
/// Crea la variante [`AddStyleSheet`](Self::AddStyleSheet) con la hoja de estilos indicada.
pub fn add_stylesheet(stylesheet: StyleSheet) -> Self {
Self::AddStyleSheet(stylesheet)
}
/// Crea la variante [`RemoveStyleSheet`](Self::RemoveStyleSheet) para la ruta indicada.
pub fn remove_stylesheet(path: &'static str) -> Self {
Self::RemoveStyleSheet(path)
}
/// Crea la variante [`AddJavaScript`](Self::AddJavaScript) con el script indicado.
pub fn add_javascript(js: JavaScript) -> Self {
Self::AddJavaScript(js)
}
/// Crea la variante [`RemoveJavaScript`](Self::RemoveJavaScript) para la ruta o identificador
/// indicado.
pub fn remove_javascript(path: &'static str) -> Self {
Self::RemoveJavaScript(path)
}
/// Crea la variante [`AddResponsiveStyle`](Self::AddResponsiveStyle) con la declaración de
/// estilo (`property: value`) indicada, para las clases y el punto de corte dados.
pub fn add_responsive_style(
breakpoint: impl Into<Option<Breakpoint>>,
classes: impl Into<CowStr>,
property: impl Into<CowStr>,
value: impl Into<CowStr>,
) -> Self {
Self::AddResponsiveStyle(
breakpoint.into(),
classes.into(),
property.into(),
value.into(),
)
}
/// Crea la variante [`AddResponsiveStyles`](Self::AddResponsiveStyles) con las declaraciones
/// de estilo (`property: value`) indicadas, para las clases y el punto de corte dados.
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let op = AssetsOp::add_responsive_styles(
/// None,
/// "flex-demo-box",
/// [("background-color", "#0d6efd"), ("color", "#fff")],
/// );
/// ```
pub fn add_responsive_styles(
breakpoint: impl Into<Option<Breakpoint>>,
classes: impl Into<CowStr>,
styles: impl IntoIterator<Item = (impl Into<CowStr>, impl Into<CowStr>)>,
) -> Self {
Self::AddResponsiveStyles(
breakpoint.into(),
classes.into(),
styles
.into_iter()
.map(|(p, v)| (p.into(), v.into()))
.collect(),
)
}
}
impl From<Favicon> for AssetsOp {
/// Convierte un favicon en [`AssetsOp::SetFavicon`] (lo sobrescribe siempre), permitiendo
/// pasarlo directamente a métodos como [`Contextual::with_assets`] sin envolverlo
/// explícitamente. Para establecerlo sólo si no hay uno ya definido, usar
/// [`AssetsOp::set_favicon_if_none()`] explícitamente.
///
/// [`Contextual::with_assets`]: crate::core::component::Contextual::with_assets
#[inline]
fn from(favicon: Favicon) -> Self {
Self::SetFavicon(Some(favicon))
}
}
impl From<Preload> for AssetsOp {
/// Convierte un recurso de precarga en [`AssetsOp::AddPreload`]. Ver la conversión
/// equivalente para [`Favicon`].
#[inline]
fn from(preload: Preload) -> Self {
Self::AddPreload(preload)
}
}
impl From<StyleSheet> for AssetsOp {
/// Convierte una hoja de estilos en [`AssetsOp::AddStyleSheet`]. Ver la conversión
/// equivalente para [`Favicon`].
#[inline]
fn from(stylesheet: StyleSheet) -> Self {
Self::AddStyleSheet(stylesheet)
}
}
impl From<JavaScript> for AssetsOp {
/// Convierte un script en [`AssetsOp::AddJavaScript`]. Ver la conversión equivalente para
/// [`Favicon`].
#[inline]
fn from(js: JavaScript) -> Self {
Self::AddJavaScript(js)
}
}

View file

@ -0,0 +1,195 @@
use super::{AssetsOp, ContextError};
use crate::auth::CurrentUser;
use crate::builder_impl;
use crate::core::component::ChildOp;
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
use crate::html::{Assets, Favicon, JavaScript, Props, PropsOp, ResponsiveStyles, StyleSheet};
use crate::locale::LangId;
use crate::web::HttpRequest;
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
///
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
///
/// - Almacenar la **petición HTTP** de origen.
/// - Seleccionar la **plantilla** y el **tema** de renderizado.
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
/// [`StyleSheet`] o los scripts [`JavaScript`], directamente o mediante una operación
/// [`AssetsOp`].
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
///
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page).
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_aliner::Aliner;
/// fn prepare_context<C: Contextual>(cx: C) -> C {
/// cx.with_langid(&Locale::resolve("es-ES"))
/// .with_template(&CoreTemplates::Standard)
/// .with_theme(&Aliner)
/// .with_assets(Favicon::new().with_icon("/favicon.ico"))
/// .with_assets(StyleSheet::from("/css/app.css"))
/// .with_assets(JavaScript::defer("/js/app.js"))
/// .with_param("user_id", 42_i32)
/// }
/// ```
#[builder_impl]
pub trait Contextual: LangId {
// **< Contextual BUILDER >*********************************************************************
/// Establece el idioma del documento.
fn with_langid(self, language: &impl LangId) -> Self;
/// Almacena la petición HTTP de origen en el contexto.
///
/// También recalcula el idioma ([`RequestLocale::from_request()`]) y
/// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando
/// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya
/// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en
/// la cadena de construcción, nunca después.
///
/// [`RequestLocale::from_request()`]: crate::locale::RequestLocale::from_request
fn with_request(self, request: Option<HttpRequest>) -> Self;
/// Especifica la plantilla para renderizar el documento.
fn with_template(self, template: TemplateRef) -> Self;
/// Especifica el tema para renderizar el documento.
fn with_theme(self, theme: ThemeRef) -> Self;
/// Añade o modifica un parámetro dinámico del contexto.
///
/// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
/// error precisos al recuperarlo con [`param`](Contextual::param) si el tipo solicitado no
/// coincide.
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let cx = Context::default()
/// .with_param("user_id", 42_i32)
/// .with_param("title", "Hello".to_string())
/// .with_param("flags", vec!["a", "b"]);
/// ```
fn with_param<T: Send + Sync + 'static>(self, key: &'static str, value: T) -> Self;
/// Añade un recurso ([`Favicon`], [`StyleSheet`], [`JavaScript`] o
/// [`Preload`](crate::html::Preload)) directamente, o aplica una operación [`AssetsOp`] sobre
/// los recursos del contexto.
fn with_assets(self, op: impl Into<AssetsOp>) -> Self;
/// Modifica identificador, clases CSS, atributos HTML o valores extra del elemento `<body>`.
fn with_body_props(self, op: PropsOp) -> Self;
/// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del
/// documento.
fn with_child(self, op: impl Into<ChildOp>) -> Self;
/// Añade un componente o aplica una operación [`ChildOp`] en una región específica del
/// documento.
fn with_child_in(self, region: RegionRef, op: impl Into<ChildOp>) -> Self;
// **< Contextual GETTERS >*********************************************************************
/// Devuelve una referencia a la petición HTTP asociada, si existe.
fn request(&self) -> Option<&HttpRequest>;
/// Devuelve la identidad del usuario actual.
///
/// Si ninguna extensión de autenticación ha inyectado un
/// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve
/// `&CurrentUser::Anonymous`.
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// async fn greet(request: HttpRequest) -> Result<Markup, ErrorPage> {
/// let mut page = Page::new(request);
/// if page.current_user().is_authenticated() {
/// // Personalizar la página para el usuario autenticado.
/// }
/// page.render().await
/// }
/// ```
fn current_user(&self) -> &CurrentUser;
/// Devuelve la plantilla configurada para renderizar el documento.
fn template(&self) -> TemplateRef;
/// Devuelve el tema que se usará para renderizar el documento.
fn theme(&self) -> ThemeRef;
/// Recupera una *referencia tipada* al parámetro solicitado.
///
/// Devuelve:
///
/// - `Ok(&T)` si la clave existe y el tipo coincide.
/// - `Err(ContextError::ParamNotFound)` si la clave no existe.
/// - `Err(ContextError::ParamTypeMismatch)` si la clave existe pero el tipo no coincide.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let cx = Context::default()
/// .with_param("user_id", 42_i32)
/// .with_param("title", "Hello".to_string());
///
/// let id: i32 = *cx.param("user_id").unwrap();
/// let title: &String = cx.param("title").unwrap();
///
/// // Error de tipo:
/// assert!(cx.param::<String>("user_id").is_err());
/// ```
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError>;
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
fn param_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
self.param::<T>(key).ok().cloned().unwrap_or_default()
}
/// Devuelve el parámetro clonado o un **valor por defecto** si no existe.
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
self.param::<T>(key).ok().cloned().unwrap_or(default)
}
/// Devuelve el parámetro clonado o el **valor evaluado** por la función `f` si no existe.
fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
self.param::<T>(key).ok().cloned().unwrap_or_else(f)
}
/// 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<StyleSheet>;
/// Devuelve los scripts JavaScript de los recursos del contexto.
fn javascripts(&self) -> &Assets<JavaScript>;
/// Devuelve los estilos *responsive* acumulados en el contexto.
fn responsive_styles(&self) -> &ResponsiveStyles;
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
fn body_props(&self) -> &Props;
// **< Contextual HELPERS >*********************************************************************
/// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let mut cx = Context::default().with_param("temp", 1u8);
/// assert!(cx.remove_param("temp"));
/// assert!(!cx.remove_param("temp")); // ya no existe
/// ```
fn remove_param(&mut self, key: &'static str) -> bool;
}

View file

@ -0,0 +1,18 @@
use thiserror::Error;
/// Errores de acceso a parámetros dinámicos del contexto.
#[derive(Debug, Error)]
pub enum ContextError {
/// La clave no existe.
#[error("parameter not found")]
ParamNotFound,
/// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye
/// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`)
/// para facilitar el diagnóstico.
#[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")]
ParamTypeMismatch {
key: &'static str,
expected: &'static str,
saved: &'static str,
},
}