From 04dbbc8858e9e202709763636c02b6016610a4d8 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 21 Mar 2026 13:24:42 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20A=C3=B1ade=20`StatusMessage`/`Messa?= =?UTF-8?q?geLevel`=20al=20contexto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/component.rs | 3 + src/core/component/context.rs | 131 ++++++++++++++++++++-------------- src/core/component/message.rs | 49 +++++++++++++ src/response/page.rs | 4 ++ 4 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 src/core/component/message.rs diff --git a/src/core/component.rs b/src/core/component.rs index ecd6eda6..50c43c21 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -13,6 +13,9 @@ pub use children::Children; pub use children::{Child, ChildOp}; pub use children::{Typed, TypedOp}; +mod message; +pub use message::{MessageLevel, StatusMessage}; + mod context; pub use context::{AssetsOp, Context, ContextError, Contextual}; diff --git a/src/core/component/context.rs b/src/core/component/context.rs index e2a220b7..c1bea79f 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,9 +1,10 @@ -use crate::core::component::ChildOp; +use crate::core::component::{ChildOp, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef}; use crate::core::TypeInfo; use crate::html::{html, Markup, RoutePath}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::locale::L10n; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::service::HttpRequest; use crate::{builder_fn, util, CowStr}; @@ -115,6 +116,19 @@ pub trait Contextual: LangId { fn with_template(self, template: TemplateRef) -> Self; /// Añade o modifica un parámetro dinámico del contexto. + /// + /// El valor se guardará 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", "Hola".to_string()) + /// .with_param("flags", vec!["a", "b"]); + /// ``` #[builder_fn] fn with_param(self, key: &'static str, value: T) -> Self; @@ -137,7 +151,34 @@ pub trait Contextual: LangId { /// Devuelve la plantilla configurada para renderizar el documento. fn template(&self) -> TemplateRef; - /// Recupera un parámetro como [`Option`]. + /// Recupera un parámetro como [`Option`], simplificando el acceso. + /// + /// A diferencia de [`get_param`](Context::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", "Alice".to_string()); + /// + /// // 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>; /// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`). @@ -166,11 +207,27 @@ pub trait Contextual: LangId { // **< Contextual HELPERS >********************************************************************* - /// Genera un identificador único por tipo (`-`) cuando no se aporta uno explícito. + /// Devuelve el `id` proporcionado tal cual, o genera uno único para el tipo `T` si no se + /// proporciona ninguno. /// - /// Es útil para componentes u otros elementos HTML que necesitan un identificador predecible si - /// no se proporciona ninguno. + /// Si `id` es `None`, construye un identificador en la forma `-`, donde `` es el + /// nombre corto del tipo en minúsculas y `` un contador incremental interno del contexto. Es + /// útil para asignar identificadores HTML predecibles cuando el componente no recibe uno + /// explícito. fn required_id(&mut self, id: Option) -> String; + + /// Acumula un [`StatusMessage`] en el contexto para notificar al visitante. + /// + /// Pueden generarse en cualquier punto del ciclo de una petición web (manejadores, renderizado, + /// lógica de negocio, etc.) que tengan acceso al contexto, y mostrarlos luego, por ejemplo, en + /// la página final devuelta al usuario. + /// + /// # Ejemplo + /// + /// ```rust,ignore + /// cx.push_message(MessageLevel::Warning, L10n::l("session-not-valid")); + /// ``` + fn push_message(&mut self, level: MessageLevel, text: L10n); } /// Implementa un **contexto de renderizado** para un documento HTML. @@ -235,6 +292,7 @@ pub struct Context { regions : ChildrenInRegions, // Regiones de componentes para renderizar. params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. id_counter : usize, // Contador para generar identificadores únicos. + messages : Vec, // Mensajes de usuario acumulados. } impl Default for Context { @@ -262,6 +320,7 @@ impl Context { regions : ChildrenInRegions::default(), params : HashMap::default(), id_counter : 0, + messages : Vec::new(), } } @@ -403,6 +462,16 @@ impl Context { } route } + + /// Devuelve todos los mensajes de usuario acumulados. + pub fn messages(&self) -> &[StatusMessage] { + &self.messages + } + + /// Indica si hay mensajes de usuario acumulados. + pub fn has_messages(&self) -> bool { + !self.messages.is_empty() + } } /// Permite a [`Context`](crate::core::component::Context) actuar como proveedor de idioma. @@ -449,20 +518,6 @@ impl Contextual for Context { 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", "Hola".to_string()) - /// .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::(); @@ -520,34 +575,6 @@ impl Contextual for Context { self.template } - /// 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", "Alice".to_string()); - /// - /// // 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() } @@ -566,12 +593,6 @@ impl Contextual for Context { // **< 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 @@ -590,4 +611,8 @@ impl Contextual for Context { util::join!(prefix, "-", self.id_counter.to_string()) } } + + fn push_message(&mut self, level: MessageLevel, text: L10n) { + self.messages.push(StatusMessage::new(level, text)); + } } diff --git a/src/core/component/message.rs b/src/core/component/message.rs new file mode 100644 index 00000000..d0f9d77b --- /dev/null +++ b/src/core/component/message.rs @@ -0,0 +1,49 @@ +use crate::locale::L10n; +use crate::{AutoDefault, Getters}; + +/// Nivel de severidad de un [`StatusMessage`]. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum MessageLevel { + /// Mensaje informativo para el usuario. + #[default] + Info, + /// Aviso o advertencia para el usuario. + Warning, + /// Error comunicado al usuario. + Error, +} + +/// Notificación amigable para el usuario generada al procesar una petición web. +/// +/// Representa un mensaje con carácter informativo, una advertencia o un error. A diferencia de +/// [`ComponentError`](super::ComponentError), no está ligado a un fallo interno de renderizado, +/// puede generarse en cualquier punto del procesamiento de una petición web (manejadores, +/// renderizado, lógica de negocio, etc.). +/// +/// El texto se almacena como [`L10n`] para resolverse con el idioma del contexto en el momento de +/// la visualización. +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// // Mensaje informativo con clave traducible. +/// let info = StatusMessage::new(MessageLevel::Info, L10n::l("saved-successfully")); +/// +/// // Aviso con texto literal sin traducción. +/// let warn = StatusMessage::new(MessageLevel::Warning, L10n::n("Formulario incompleto.")); +/// ``` +#[derive(Debug, Getters)] +pub struct StatusMessage { + /// Nivel de severidad del mensaje. + level: MessageLevel, + /// Texto del mensaje. + text: L10n, +} + +impl StatusMessage { + /// Crea un nuevo mensaje de usuario con el nivel y texto indicados. + pub fn new(level: MessageLevel, text: L10n) -> Self { + StatusMessage { level, text } + } +} diff --git a/src/response/page.rs b/src/response/page.rs index 5af063e4..a30c2324 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -380,4 +380,8 @@ impl Contextual for Page { fn required_id(&mut self, id: Option) -> String { self.context.required_id::(id) } + + fn push_message(&mut self, level: crate::prelude::MessageLevel, text: L10n) { + self.context.push_message(level, text); + } }