diff --git a/src/base/component/html.rs b/src/base/component/html.rs
index cac39ea..7bde94a 100644
--- a/src/base/component/html.rs
+++ b/src/base/component/html.rs
@@ -25,7 +25,7 @@ use crate::prelude::*;
/// use pagetop::prelude::*;
///
/// let component = Html::with(|cx| {
-/// let user = cx.get_param::("username").unwrap_or(String::from("visitor"));
+/// let user = cx.param::("username").cloned().unwrap_or(String::from("visitor"));
/// html! {
/// h1 { "Hello, " (user) }
/// }
diff --git a/src/html/context.rs b/src/html/context.rs
index 8b7afba..4ebd510 100644
--- a/src/html/context.rs
+++ b/src/html/context.rs
@@ -7,10 +7,8 @@ use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLB
use crate::service::HttpRequest;
use crate::{builder_fn, join};
+use std::any::Any;
use std::collections::HashMap;
-use std::error::Error;
-use std::fmt::{self, Display};
-use std::str::FromStr;
/// Operaciones para modificar el contexto ([`Context`]) del documento.
pub enum AssetsOp {
@@ -33,32 +31,28 @@ pub enum AssetsOp {
RemoveJavaScript(&'static str),
}
-/// Errores de lectura o conversión de parámetros almacenados en el contexto.
+/// Errores de acceso a parámetros dinámicos del contexto.
+///
+/// - [`ErrorParam::NotFound`]: la clave no existe.
+/// - [`ErrorParam::TypeMismatch`]: 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.
#[derive(Debug)]
pub enum ErrorParam {
- /// El parámetro solicitado no existe.
NotFound,
- /// El valor del parámetro no pudo convertirse al tipo requerido.
- ParseError(String),
+ TypeMismatch {
+ key: &'static str,
+ expected: &'static str,
+ saved: &'static str,
+ },
}
-impl Display for ErrorParam {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- ErrorParam::NotFound => write!(f, "Parameter not found"),
- ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"),
- }
- }
-}
-
-impl Error for ErrorParam {}
-
/// Representa el contexto 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 de contexto
-/// definidos en tiempo de ejecución.
+/// hojas de estilo ([`StyleSheet`]) y *scripts* ([`JavaScript`]), así como *parámetros dinámicos
+/// heterogéneos* de contexto definidos en tiempo de ejecución.
///
/// # Ejemplos
///
@@ -95,7 +89,7 @@ impl Error for ErrorParam {}
/// assert_eq!(active_theme.short_name(), "aliner");
///
/// // Recupera el parámetro a su tipo original.
-/// let id: i32 = cx.get_param("usuario_id").unwrap();
+/// let id: i32 = *cx.get_param::("usuario_id").unwrap();
/// assert_eq!(id, 42);
///
/// // Genera un identificador para un componente de tipo `Menu`.
@@ -113,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, String>, // Parámetros definidos en tiempo de 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.
}
@@ -152,7 +146,7 @@ impl Context {
favicon : None,
stylesheets: Assets::::new(),
javascripts: Assets::::new(),
- params : HashMap::<&str, String>::new(),
+ params : HashMap::default(),
id_counter : 0,
}
}
@@ -246,29 +240,146 @@ impl Context {
// Context PARAMS ******************************************************************************
- /// Añade o modifica un parámetro del contexto almacenando el valor como [`String`].
+ /// 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 {
- self.params.insert(key, value.to_string());
+ 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 del contexto convertido al tipo especificado.
+ /// Recupera un parámetro como [`Option`], simplificando el acceso.
///
- /// Devuelve un error si el parámetro no existe ([`ErrorParam::NotFound`]) o la conversión falla
- /// ([`ErrorParam::ParseError`]).
- pub fn get_param(&self, key: &'static str) -> Result {
- self.params
- .get(key)
- .ok_or(ErrorParam::NotFound)
- .and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
+ /// 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()
}
- /// Elimina un parámetro del contexto. Devuelve `true` si existía y se eliminó.
+ /// Recupera una *referencia tipada* al parámetro solicitado.
+ ///
+ /// Devuelve:
+ ///
+ /// - `Ok(&T)` si la clave existe y el tipo coincide.
+ /// - `Err(ErrorParam::NotFound)` si la clave no existe.
+ /// - `Err(ErrorParam::TypeMismatch)` si la clave existe pero el tipo no coincide.
+ ///
+ /// # Ejemplos
+ ///
+ /// ```rust
+ /// use pagetop::prelude::*;
+ ///
+ /// let cx = Context::new(None)
+ /// .with_param("usuario_id", 42_i32)
+ /// .with_param("titulo", String::from("Hola"));
+ ///
+ /// let id: &i32 = cx.get_param("usuario_id").unwrap();
+ /// let titulo: &String = cx.get_param("titulo").unwrap();
+ ///
+ /// // Error de tipo:
+ /// assert!(cx.get_param::("usuario_id").is_err());
+ /// ```
+ pub fn get_param(&self, key: &'static str) -> Result<&T, ErrorParam> {
+ let (any, type_name) = self.params.get(key).ok_or(ErrorParam::NotFound)?;
+ any.downcast_ref::()
+ .ok_or_else(|| ErrorParam::TypeMismatch {
+ key,
+ expected: TypeInfo::FullName.of::(),
+ saved: *type_name,
+ })
+ }
+
+ /// 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:
+ ///
+ /// - `Ok(T)` si la clave existía y el tipo coincide.
+ /// - `Err(ErrorParam::NotFound)` si la clave no existe.
+ /// - `Err(ErrorParam::TypeMismatch)` si el tipo no coincide.
+ ///
+ /// # Ejemplos
+ ///
+ /// ```rust
+ /// use pagetop::prelude::*;
+ ///
+ /// let mut cx = Context::new(None)
+ /// .with_param("contador", 7_i32)
+ /// .with_param("titulo", String::from("Hola"));
+ ///
+ /// let n: i32 = cx.take_param("contador").unwrap();
+ /// assert!(cx.get_param::("contador").is_err()); // ya no está
+ ///
+ /// // Error de tipo:
+ /// assert!(cx.take_param::("titulo").is_err());
+ /// ```
+ pub fn take_param(&mut self, key: &'static str) -> Result {
+ let (boxed, saved) = self.params.remove(key).ok_or(ErrorParam::NotFound)?;
+ boxed
+ .downcast::()
+ .map(|b| *b)
+ .map_err(|_| ErrorParam::TypeMismatch {
+ key,
+ expected: TypeInfo::FullName.of::(),
+ saved,
+ })
+ }
+
// Context EXTRAS ******************************************************************************
/// Genera un identificador único si no se proporciona uno explícito.
diff --git a/src/response/page.rs b/src/response/page.rs
index 5ac3720..0942f8c 100644
--- a/src/response/page.rs
+++ b/src/response/page.rs
@@ -108,6 +108,12 @@ impl Page {
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 {
@@ -214,6 +220,10 @@ impl Page {
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
@@ -223,7 +233,16 @@ 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
+ }
+ */
// Page RENDER *********************************************************************************
/// Renderiza los componentes de una región (`regiona_name`) de la página.
diff --git a/tests/component_html.rs b/tests/component_html.rs
index b9b8e5e..bd7f3c0 100644
--- a/tests/component_html.rs
+++ b/tests/component_html.rs
@@ -20,7 +20,7 @@ async fn component_html_renders_using_context_param() {
let mut cx = Context::new(None).with_param("username", String::from("Alice"));
let component = Html::with(|cx| {
- let name = cx.get_param::("username").unwrap_or_default();
+ let name = cx.param::("username").cloned().unwrap_or_default();
html! {
span { (name) }
}