✨ Añade StatusMessage/MessageLevel al contexto
This commit is contained in:
parent
34aeeab2d7
commit
04dbbc8858
4 changed files with 134 additions and 53 deletions
|
|
@ -13,6 +13,9 @@ pub use children::Children;
|
||||||
pub use children::{Child, ChildOp};
|
pub use children::{Child, ChildOp};
|
||||||
pub use children::{Typed, TypedOp};
|
pub use children::{Typed, TypedOp};
|
||||||
|
|
||||||
|
mod message;
|
||||||
|
pub use message::{MessageLevel, StatusMessage};
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
pub use context::{AssetsOp, Context, ContextError, Contextual};
|
pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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::all::DEFAULT_THEME;
|
||||||
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
|
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::html::{html, Markup, RoutePath};
|
use crate::html::{html, Markup, RoutePath};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
|
use crate::locale::L10n;
|
||||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||||
use crate::service::HttpRequest;
|
use crate::service::HttpRequest;
|
||||||
use crate::{builder_fn, util, CowStr};
|
use crate::{builder_fn, util, CowStr};
|
||||||
|
|
@ -115,6 +116,19 @@ pub trait Contextual: LangId {
|
||||||
fn with_template(self, template: TemplateRef) -> Self;
|
fn with_template(self, template: TemplateRef) -> Self;
|
||||||
|
|
||||||
/// Añade o modifica un parámetro dinámico del contexto.
|
/// 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]
|
#[builder_fn]
|
||||||
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
|
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
|
||||||
|
|
||||||
|
|
@ -137,7 +151,34 @@ pub trait Contextual: LangId {
|
||||||
/// Devuelve la plantilla configurada para renderizar el documento.
|
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||||
fn template(&self) -> TemplateRef;
|
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::<String>("username").map(|s| s.as_str()), Some("Alice"));
|
||||||
|
///
|
||||||
|
/// // Devuelve None si no existe o si el tipo no coincide.
|
||||||
|
/// assert!(cx.param::<i32>("username").is_none());
|
||||||
|
/// assert!(cx.param::<String>("missing").is_none());
|
||||||
|
///
|
||||||
|
/// // Acceso con valor por defecto.
|
||||||
|
/// let user = cx.param::<String>("missing")
|
||||||
|
/// .cloned()
|
||||||
|
/// .unwrap_or_else(|| "visitor".to_string());
|
||||||
|
/// assert_eq!(user, "visitor");
|
||||||
|
/// ```
|
||||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
||||||
|
|
||||||
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
|
/// Devuelve el parámetro clonado o el **valor por defecto del tipo** (`T::default()`).
|
||||||
|
|
@ -166,11 +207,27 @@ pub trait Contextual: LangId {
|
||||||
|
|
||||||
// **< Contextual HELPERS >*********************************************************************
|
// **< Contextual HELPERS >*********************************************************************
|
||||||
|
|
||||||
/// Genera un identificador único por tipo (`<tipo>-<n>`) 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
|
/// Si `id` es `None`, construye un identificador en la forma `<tipo>-<n>`, donde `<tipo>` es el
|
||||||
/// no se proporciona ninguno.
|
/// nombre corto del tipo en minúsculas y `<n>` un contador incremental interno del contexto. Es
|
||||||
|
/// útil para asignar identificadores HTML predecibles cuando el componente no recibe uno
|
||||||
|
/// explícito.
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String;
|
fn required_id<T>(&mut self, id: Option<String>) -> 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.
|
/// Implementa un **contexto de renderizado** para un documento HTML.
|
||||||
|
|
@ -235,6 +292,7 @@ pub struct Context {
|
||||||
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
||||||
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
||||||
id_counter : usize, // Contador para generar identificadores únicos.
|
id_counter : usize, // Contador para generar identificadores únicos.
|
||||||
|
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Context {
|
impl Default for Context {
|
||||||
|
|
@ -262,6 +320,7 @@ impl Context {
|
||||||
regions : ChildrenInRegions::default(),
|
regions : ChildrenInRegions::default(),
|
||||||
params : HashMap::default(),
|
params : HashMap::default(),
|
||||||
id_counter : 0,
|
id_counter : 0,
|
||||||
|
messages : Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,6 +462,16 @@ impl Context {
|
||||||
}
|
}
|
||||||
route
|
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.
|
/// Permite a [`Context`](crate::core::component::Context) actuar como proveedor de idioma.
|
||||||
|
|
@ -449,20 +518,6 @@ impl Contextual for Context {
|
||||||
self
|
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]
|
#[builder_fn]
|
||||||
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
|
||||||
let type_name = TypeInfo::FullName.of::<T>();
|
let type_name = TypeInfo::FullName.of::<T>();
|
||||||
|
|
@ -520,34 +575,6 @@ impl Contextual for Context {
|
||||||
self.template
|
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::<String>("username").map(|s| s.as_str()), Some("Alice"));
|
|
||||||
///
|
|
||||||
/// // Devuelve None si no existe o si el tipo no coincide.
|
|
||||||
/// assert!(cx.param::<i32>("username").is_none());
|
|
||||||
/// assert!(cx.param::<String>("missing").is_none());
|
|
||||||
///
|
|
||||||
/// // Acceso con valor por defecto.
|
|
||||||
/// let user = cx.param::<String>("missing")
|
|
||||||
/// .cloned()
|
|
||||||
/// .unwrap_or_else(|| "visitor".to_string());
|
|
||||||
/// assert_eq!(user, "visitor");
|
|
||||||
/// ```
|
|
||||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
|
fn param<T: 'static>(&self, key: &'static str) -> Option<&T> {
|
||||||
self.get_param::<T>(key).ok()
|
self.get_param::<T>(key).ok()
|
||||||
}
|
}
|
||||||
|
|
@ -566,12 +593,6 @@ impl Contextual for Context {
|
||||||
|
|
||||||
// **< Contextual HELPERS >*********************************************************************
|
// **< 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 `<tipo>-<número>`
|
|
||||||
/// donde `<tipo>` es el nombre corto del tipo en minúsculas (sin espacios) y `<número>` es un
|
|
||||||
/// contador interno incremental.
|
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
id
|
id
|
||||||
|
|
@ -590,4 +611,8 @@ impl Contextual for Context {
|
||||||
util::join!(prefix, "-", self.id_counter.to_string())
|
util::join!(prefix, "-", self.id_counter.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_message(&mut self, level: MessageLevel, text: L10n) {
|
||||||
|
self.messages.push(StatusMessage::new(level, text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
src/core/component/message.rs
Normal file
49
src/core/component/message.rs
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -380,4 +380,8 @@ impl Contextual for Page {
|
||||||
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
fn required_id<T>(&mut self, id: Option<String>) -> String {
|
||||||
self.context.required_id::<T>(id)
|
self.context.required_id::<T>(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_message(&mut self, level: crate::prelude::MessageLevel, text: L10n) {
|
||||||
|
self.context.push_message(level, text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue