Compare commits
No commits in common. "24bd13414cd09d97f171be51e4f8e44a09e45419" and "771f61bf292b62a8fb41286ba217298228643daf" have entirely different histories.
24bd13414c
...
771f61bf29
16 changed files with 170 additions and 489 deletions
|
|
@ -105,7 +105,7 @@ impl Extension for SuperMenu {
|
|||
)),
|
||||
));
|
||||
|
||||
InRegion::Global(&DefaultRegions::Header).add(
|
||||
InRegion::Global(&DefaultRegion::Header).add(
|
||||
bs::Container::new()
|
||||
.with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||
.with_child(navbar_menu),
|
||||
|
|
|
|||
33
src/app.rs
33
src/app.rs
|
|
@ -3,31 +3,20 @@
|
|||
mod figfont;
|
||||
|
||||
use crate::core::{extension, extension::ExtensionRef};
|
||||
use crate::html::Markup;
|
||||
use crate::locale::Locale;
|
||||
use crate::response::page::{render_error_pages, response_for_panic, route_not_found};
|
||||
use crate::web::Router;
|
||||
use crate::response::page::{ErrorPage, render_error_pages};
|
||||
use crate::web::{HttpRequest, Router};
|
||||
use crate::{PAGETOP_VERSION, global, trace};
|
||||
|
||||
use tower_http::catch_panic::CatchPanicLayer;
|
||||
|
||||
use std::io::Error;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Punto de entrada de una aplicación PageTop.
|
||||
///
|
||||
/// Orquesta el arranque de la aplicación. Primero se instancia con [`Application::new()`] o
|
||||
/// [`Application::prepare()`], y después se ejecuta usando [`run()`](Application::run). Si se está
|
||||
/// preparando un entorno de pruebas, se usa [`test()`](Application::test).
|
||||
///
|
||||
/// Los **errores controlados** (403, 404, o un fallo que un handler devuelva explícitamente como
|
||||
/// [`ErrorPage`](crate::response::page::ErrorPage)) se renderizan usando el tema activo (ver
|
||||
/// [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
||||
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) y
|
||||
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
||||
///
|
||||
/// La última capa del router captura cualquier **fallo catastrófico** (`panic!`) de la aplicación
|
||||
/// en lugar de abortar la conexión. Devuelve una respuesta mínima HTTP 500 que es independiente del
|
||||
/// tema y del ciclo de renderizado de componentes.
|
||||
/// Orquesta el arranque de la aplicación. Primero se instancia con [`new()`](Application::new) o
|
||||
/// [`prepare()`](Application::prepare), y después se ejecuta usando [`run()`](Application::run) (o
|
||||
/// usando [`test()`](Application::test) si se está preparando un entorno de pruebas).
|
||||
pub struct Application;
|
||||
|
||||
impl Application {
|
||||
|
|
@ -45,7 +34,7 @@ impl Application {
|
|||
/// que no dependen de ninguna otra, luego las que dependen de extensiones ya habilitadas, y así
|
||||
/// hasta habilitar la extensión raíz.
|
||||
///
|
||||
/// Es `async` porque cada extensión puede realizar operaciones asíncronas en su
|
||||
/// Es *async* porque cada extensión puede realizar operaciones asíncronas en su
|
||||
/// [`initialize()`](crate::core::extension::Extension::initialize) (conexión a base de datos,
|
||||
/// migraciones, semillas de datos...).
|
||||
pub async fn prepare(root_extension: ExtensionRef) -> Self {
|
||||
|
|
@ -117,16 +106,12 @@ impl Application {
|
|||
}
|
||||
|
||||
// Construye el router con las rutas y el middleware de todas las extensiones habilitadas.
|
||||
//
|
||||
// Con `CatchPanicLayer` en la última capa se capturan incluso los `panic!` que se produzcan
|
||||
// dentro del propio renderizado de una página de error.
|
||||
fn build_router() -> Router {
|
||||
let router = extension::all::configure_routes(Router::new());
|
||||
let router = extension::all::configure_middleware(router);
|
||||
router
|
||||
.fallback(route_not_found)
|
||||
.layer(axum::middleware::from_fn(render_error_pages))
|
||||
.layer(CatchPanicLayer::custom(response_for_panic))
|
||||
}
|
||||
|
||||
/// Arranca el servidor web de la aplicación.
|
||||
|
|
@ -170,3 +155,7 @@ impl Application {
|
|||
Self::build_router()
|
||||
}
|
||||
}
|
||||
|
||||
async fn route_not_found(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
Err(ErrorPage::NotFound(request))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::{UniqueId, Weight};
|
|||
///
|
||||
/// Se almacena automáticamente en el [`Context`] a partir de la petición HTTP (ver
|
||||
/// [`Context::new()`](crate::core::component::Context::new)). La identidad se extrae de las
|
||||
/// extensiones de la petición, que una extensión de autenticación inyecta mediante su middleware.
|
||||
/// extensiones de la petición, que una extensión de autenticación inyecta mediante su *middleware*.
|
||||
///
|
||||
/// Se accede con [`Contextual::current_user()`](crate::core::component::Contextual::current_user).
|
||||
///
|
||||
|
|
@ -72,9 +72,9 @@ impl CurrentUser {
|
|||
/// Se invoca con:
|
||||
///
|
||||
/// - `cx`: el contexto de renderizado desde el que se puede acceder a la petición HTTP y a
|
||||
/// cualquier dato inyectado por el middleware de autenticación.
|
||||
/// cualquier dato inyectado por el *middleware* de autenticación.
|
||||
/// - `key`: clave del permiso a comprobar (p. ej. `"myapp.edit_posts"`).
|
||||
/// - `granted`: referencia mutable; el handler debe asignarla a `true` si concede el permiso.
|
||||
/// - `granted`: referencia mutable; el *handler* debe asignarla a `true` si concede el permiso.
|
||||
pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool);
|
||||
|
||||
/// Acción para comprobar si el usuario actual tiene un permiso concreto.
|
||||
|
|
@ -148,7 +148,7 @@ impl CheckPermission {
|
|||
/// Comprueba si el usuario actual tiene el permiso indicado.
|
||||
///
|
||||
/// Despacha la acción [`CheckPermission`]: cualquier extensión registrada puede conceder el permiso
|
||||
/// asignando `granted = true` en su handler. Si no hay extensiones de autenticación activas,
|
||||
/// asignando `granted = true` en su *handler*. Si no hay extensiones de autenticación activas,
|
||||
/// devuelve `false` para cualquier usuario, incluido el anónimo.
|
||||
///
|
||||
/// La decisión de conceder o denegar permisos al usuario anónimo también es responsabilidad de cada
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ impl Theme for Basic {
|
|||
.with_weight(-99),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegions::Footer,
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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::{ChildrenInRegions, DefaultRegions, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet};
|
||||
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||
use crate::locale::L10n;
|
||||
|
|
@ -96,7 +96,7 @@ impl std::error::Error for ContextError {}
|
|||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||
/// .with_theme(&Aliner)
|
||||
/// .with_template(&DefaultTemplates::Standard)
|
||||
/// .with_template(&DefaultTemplate::Standard)
|
||||
/// .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")))
|
||||
|
|
@ -306,36 +306,13 @@ pub trait Contextual: LangId {
|
|||
/// let _unique_id = cx.build_id::<Menu>(1); // => "menu-1" si es el primero
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
// Cómo obtener la plantilla activa del contexto: la del tema (por defecto o de administración), o
|
||||
// una fijada explícitamente. Se resuelve contra el tema activo al leer `Context::template()`, no al
|
||||
// asignarla, para que un cambio de tema posterior con `with_theme()` se refleje automáticamente.
|
||||
enum TemplateSource {
|
||||
// Plantilla por defecto.
|
||||
Default,
|
||||
// Plantilla de administración del tema activo.
|
||||
Admin,
|
||||
// Plantilla fijada explícitamente con `with_template()`.
|
||||
Explicit(TemplateRef),
|
||||
}
|
||||
|
||||
impl TemplateSource {
|
||||
fn resolve(&self, theme: ThemeRef) -> TemplateRef {
|
||||
match self {
|
||||
Self::Default => theme.default_template(),
|
||||
Self::Admin => theme.admin_template(),
|
||||
Self::Explicit(template) => *template,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub struct Context {
|
||||
request : Option<HttpRequest>, // Petición HTTP de origen.
|
||||
locale : RequestLocale, // Idioma asociado a la petición.
|
||||
current_user: CurrentUser, // Identidad del usuario actual.
|
||||
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
||||
template : TemplateSource, // Plantilla usada para renderizar.
|
||||
template : TemplateRef, // Plantilla usada para renderizar.
|
||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||
preloads : Assets<Preload>, // Recursos para precarga.
|
||||
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
|
|
@ -367,7 +344,7 @@ impl Context {
|
|||
locale,
|
||||
current_user,
|
||||
theme : *DEFAULT_THEME,
|
||||
template : TemplateSource::Default,
|
||||
template : DEFAULT_THEME.default_template(),
|
||||
favicon : None,
|
||||
preloads : Assets::<Preload>::new(),
|
||||
stylesheets: Assets::<StyleSheet>::new(),
|
||||
|
|
@ -380,7 +357,7 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
// Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o
|
||||
// Extrae el `CurrentUser` inyectado por *middleware* en las extensiones de la petición, o
|
||||
// `CurrentUser::Anonymous` si no hay petición o ninguna extensión de autenticación está activa.
|
||||
fn resolve_current_user(request: Option<&HttpRequest>) -> CurrentUser {
|
||||
request
|
||||
|
|
@ -389,13 +366,6 @@ impl Context {
|
|||
.unwrap_or(CurrentUser::Anonymous)
|
||||
}
|
||||
|
||||
// Fuerza la plantilla de administración del tema activo (usada por `Page::admin()`). Se
|
||||
// resuelve dinámicamente contra `self.theme`, igual que `TemplateSource::Default`, así que
|
||||
// sigue reflejando cualquier cambio de tema posterior con `with_theme()`.
|
||||
pub(crate) fn use_admin_template(&mut self) {
|
||||
self.template = TemplateSource::Admin;
|
||||
}
|
||||
|
||||
// **< Context RENDER >*************************************************************************
|
||||
|
||||
/// Renderiza los recursos del contexto.
|
||||
|
|
@ -564,7 +534,7 @@ impl Contextual for Context {
|
|||
|
||||
#[builder_fn]
|
||||
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||
self.template = TemplateSource::Explicit(template);
|
||||
self.template = template;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -621,7 +591,7 @@ impl Contextual for Context {
|
|||
#[builder_fn]
|
||||
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.regions
|
||||
.alter_child_in(&DefaultRegions::Content, op.into());
|
||||
.alter_child_in(&DefaultRegion::Content, op.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -646,7 +616,7 @@ impl Contextual for Context {
|
|||
}
|
||||
|
||||
fn template(&self) -> TemplateRef {
|
||||
self.template.resolve(self.theme)
|
||||
self.template
|
||||
}
|
||||
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ pub fn configure_routes(router: Router) -> Router {
|
|||
|
||||
// **< CONFIGURA EL MIDDLEWARE GLOBAL >*************************************************************
|
||||
|
||||
/// Aplica las capas de middleware globales de todas las extensiones sobre el router ya enrutado.
|
||||
/// Aplica las capas de *middleware* globales de todas las extensiones sobre el router ya enrutado.
|
||||
///
|
||||
/// Se llama después de [`configure_routes`] para garantizar que las capas envuelven todas las
|
||||
/// rutas de la aplicación, independientemente del orden de las extensiones.
|
||||
|
|
|
|||
|
|
@ -192,12 +192,12 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Rutas con middleware acotado a esta extensión
|
||||
/// ## Rutas con *middleware* acotado a esta extensión
|
||||
///
|
||||
/// Cada `Router` mantiene su propia pila de middleware independiente. Cuando se crea un
|
||||
/// Cada `Router` mantiene su propia pila de *middleware* independiente. Cuando se crea un
|
||||
/// `Router::new()` separado y se llama a `.layer()` sobre él, esa capa sólo se aplica a las
|
||||
/// rutas de ese router concreto. Al fusionarlo con `.merge()` en el `router` principal, cada
|
||||
/// ruta se añade con su middleware asociado, sin tocar las demás.
|
||||
/// rutas de ese *router* concreto. Al fusionarlo con `.merge()` en el `router` principal, cada
|
||||
/// ruta se añade con su *middleware* asociado, sin tocar las demás.
|
||||
///
|
||||
/// Otra cosa es llamar a `.layer()` directamente sobre el `router` principal que se recibe como
|
||||
/// parámetro, porque ese objeto ya contiene todas las rutas acumuladas por extensiones
|
||||
|
|
@ -218,7 +218,7 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Para middleware que deba cubrir **todas** las rutas, usar
|
||||
/// Para *middleware* que deba cubrir **todas** las rutas, usar
|
||||
/// [`configure_middleware`](Self::configure_middleware).
|
||||
///
|
||||
/// ## Archivos estáticos
|
||||
|
|
@ -242,13 +242,13 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
router
|
||||
}
|
||||
|
||||
/// Añade capas de middleware globales al router ya completamente preparado.
|
||||
/// Añade capas de *middleware* globales al *router* ya completamente preparado.
|
||||
///
|
||||
/// Se invoca **después** de que todas las extensiones hayan registrado sus rutas con
|
||||
/// [`configure_router`](Self::configure_router), de modo que las capas añadidas aquí se aplican
|
||||
/// a **todas** las rutas de la aplicación, independientemente del orden de las extensiones.
|
||||
///
|
||||
/// Usar este método cuando el middleware deba interceptar cualquier petición entrante (p. ej.
|
||||
/// Usar este método cuando el *middleware* deba interceptar cualquier petición entrante (p. ej.
|
||||
/// resolución de sesión, autenticación, cabeceras de seguridad, ...).
|
||||
///
|
||||
/// # Ejemplo
|
||||
|
|
|
|||
|
|
@ -1,86 +1,31 @@
|
|||
//! API para añadir y gestionar nuevos temas.
|
||||
//!
|
||||
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension) y
|
||||
//! también [`Theme`], de modo que [`Extension::theme()`](crate::core::extension::Extension::theme)
|
||||
//! permita identificar y registrar los temas disponibles.
|
||||
//!
|
||||
//! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos
|
||||
//! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo
|
||||
//! del documento a partir de regiones ([`Region`]). Cada región es un contenedor lógico
|
||||
//! identificado por un nombre para agrupar y renderizar componentes.
|
||||
//! del documento a partir de varias regiones ([`Region`]). Cada región es un contenedor lógico
|
||||
//! identificado por un nombre, cuyo contenido se obtiene del [`Context`] de la página.
|
||||
//!
|
||||
//! Una página ([`Page`](crate::response::page::Page)) es un documento HTML completo. Implementa
|
||||
//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio [`Context`], donde
|
||||
//! mantiene el tema activo, la plantilla seleccionada y los componentes asociados a cada región a
|
||||
//! renderizar.
|
||||
//! Una página ([`Page`](crate::response::page::Page)) representa un documento HTML completo.
|
||||
//! Implementa [`Contextual`](crate::core::component::Contextual) para gestionar su propio
|
||||
//! [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
|
||||
//! asociados a cada región.
|
||||
//!
|
||||
//! Además, PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un
|
||||
//! tema hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos
|
||||
//! selectivamente. Por ejemplo, puede redefinir el renderizado de un componente a través de
|
||||
//! [`Theme::handle_component()`] sin cambiar el resto del comportamiento heredado. Un tema hijo
|
||||
//! puede ser a su vez padre de otro, basta declararlo cada vez con [`Theme::parent()`].
|
||||
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
|
||||
//! registran componentes en el [`Context`], las plantillas organizan las regiones y las páginas
|
||||
//! generan el documento HTML resultante.
|
||||
//!
|
||||
//! # Cómo crear un tema nuevo
|
||||
//! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre. Un tema
|
||||
//! hijo hereda automáticamente todos los métodos del padre y puede sobrescribirlos selectivamente:
|
||||
//! por ejemplo, puede redefinir el renderizado de un componente con [`Theme::handle_component()`]
|
||||
//! sin modificar el resto del comportamiento heredado. Un tema hijo puede ser a su vez padre de
|
||||
//! otro, basta declararlo cada vez con [`Theme::parent()`].
|
||||
//!
|
||||
//! Un tema mínimo es una extensión que implementa [`Extension`](crate::core::extension::Extension)
|
||||
//! y también [`Theme`] para que [`Extension::theme()`](crate::core::extension::Extension::theme)
|
||||
//! devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, ya que todos los
|
||||
//! métodos de [`Theme`] tienen implementación por defecto.
|
||||
//!
|
||||
//! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por
|
||||
//! defecto no basta:
|
||||
//!
|
||||
//! 1. **Definir regiones propias**. Por defecto PageTop define [`DefaultRegions`], con tres
|
||||
//! regiones (`Header`, `Content` y `Footer`) que usan la implementación por defecto de
|
||||
//! [`Region::render()`]. Un tema puede definir un *enum* propio que implemente [`Region`] para
|
||||
//! exponer sus propias regiones (por ejemplo, una barra lateral) o para cambiar cómo se muestra
|
||||
//! el contenido de una región ya existente (identificada por su nombre).
|
||||
//! 2. **Definir plantillas propias**. Por defecto existe [`DefaultTemplates`], con dos plantillas
|
||||
//! (`Standard` y `Admin`) que usan la implementación por defecto de [`Template::render()`] para
|
||||
//! renderizar [`DefaultRegions::Header`], [`DefaultRegions::Content`] y
|
||||
//! [`DefaultRegions::Footer`], en este orden. Un tema puede definir un *enum* propio que
|
||||
//! implemente [`Template`] para crear nuevas plantillas, maquetar las regiones de otra forma,
|
||||
//! cambiar su orden o envolverlas en contenedores adicionales.
|
||||
//! 3. **Elegir las plantillas predeterminadas**. Por un lado, la plantilla por defecto vía
|
||||
//! [`Theme::default_template()`] y, por otro, la plantilla para las páginas de administración,
|
||||
//! [`Theme::admin_template()`]. De esta forma, las páginas creadas con `Page::new()` usarán
|
||||
//! automáticamente la plantilla `default_template()` del tema activo, y las páginas creadas con
|
||||
//! `Page::admin()` usarán la de `admin_template()`, sin tener que llamar manualmente a
|
||||
//! [`with_template()`](crate::core::component::Contextual::with_template). Un tema que no
|
||||
//! sobrescriba estos métodos sigue usando las plantillas por defecto de PageTop.
|
||||
//!
|
||||
//! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se
|
||||
//! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de
|
||||
//! navegación del sitio. Los temas pueden personalizar su contenido sobrescribiendo
|
||||
//! [`Theme::error_403()`], [`Theme::error_404()`] o [`Theme::error_fatal()`], sin necesidad de una
|
||||
//! plantilla distinta.
|
||||
//!
|
||||
//! El resto del comportamiento de un tema (renderizado del `<head>`, o intervención en el
|
||||
//! renderizado de componentes concretos con [`Theme::handle_component()`]) se sobrescribe de forma
|
||||
//! independiente de estos tres pasos y no es necesario para tener un tema funcional.
|
||||
//!
|
||||
//! # Componentes que se procesan en todas las páginas
|
||||
//!
|
||||
//! Los componentes añadidos a una página con
|
||||
//! [`with_child_in()`](crate::core::component::Contextual::with_child_in) sólo existen para esa
|
||||
//! petición concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`]
|
||||
//! resuelve el caso contrario: un componente que se debe procesar en todas las páginas, o en todas
|
||||
//! las de un tema concreto, sin tener que registrarlo en el código de cada página.
|
||||
//!
|
||||
//! `InRegion` registra el componente una sola vez, normalmente al arrancar la aplicación o al
|
||||
//! inicializar una extensión, y a partir de ahí se procesa automáticamente en todas las páginas que
|
||||
//! correspondan:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use pagetop::prelude::*;
|
||||
//! InRegion::Global(&DefaultRegions::Footer).add(PoweredBy::new());
|
||||
//! ```
|
||||
//!
|
||||
//! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del
|
||||
//! renderizado, de modo que su `setup()` siempre parte de un estado inicial limpio y no acumula
|
||||
//! mutaciones entre peticiones.
|
||||
//!
|
||||
//! Como cualquier otro componente, antes de renderizarse pasa por
|
||||
//! [`is_renderable()`](crate::core::component::Component::is_renderable), el primer paso del
|
||||
//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una
|
||||
//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o
|
||||
//! si el usuario actual está autenticado.
|
||||
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo,
|
||||
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
|
||||
|
||||
use crate::async_trait;
|
||||
use crate::core::component::Context;
|
||||
|
|
@ -99,7 +44,7 @@ use crate::{AutoDefault, util};
|
|||
/// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas
|
||||
/// implementaciones de [`Region`] que devuelvan el mismo nombre compartirán el mismo conjunto de
|
||||
/// componentes registrados en el [`Context`], aunque cada región puede renderizar ese contenido de
|
||||
/// forma diferente. Por ejemplo, [`DefaultRegions::Header`] y `BootsierRegions::Header` mostrarían
|
||||
/// forma diferente. Por ejemplo, [`DefaultRegion::Header`] y `BootsierRegion::Header` mostrarían
|
||||
/// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de
|
||||
/// manera distinta.
|
||||
///
|
||||
|
|
@ -157,7 +102,7 @@ pub trait Region: Send + Sync {
|
|||
/// Referencia estática a una región.
|
||||
pub type RegionRef = &'static dyn Region;
|
||||
|
||||
// **< DefaultRegions >*****************************************************************************
|
||||
// **< DefaultRegion >******************************************************************************
|
||||
|
||||
/// Regiones básicas que PageTop proporciona por defecto.
|
||||
///
|
||||
|
|
@ -165,7 +110,7 @@ pub type RegionRef = &'static dyn Region;
|
|||
/// equivalente definida por otros temas, por lo que comparten también el contenido registrado bajo
|
||||
/// esos nombres.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum DefaultRegions {
|
||||
pub enum DefaultRegion {
|
||||
/// Región estándar para la **cabecera** del documento, de nombre `"header"`.
|
||||
///
|
||||
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
||||
|
|
@ -184,7 +129,7 @@ pub enum DefaultRegions {
|
|||
Footer,
|
||||
}
|
||||
|
||||
impl Region for DefaultRegions {
|
||||
impl Region for DefaultRegion {
|
||||
#[inline]
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -215,8 +160,8 @@ impl Region for DefaultRegions {
|
|||
pub trait Template: Send + Sync {
|
||||
/// Renderiza el contenido de la plantilla.
|
||||
///
|
||||
/// Por defecto, renderiza las regiones básicas de [`DefaultRegions`] en este orden:
|
||||
/// [`DefaultRegions::Header`], [`DefaultRegions::Content`] y [`DefaultRegions::Footer`].
|
||||
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
|
||||
/// [`DefaultRegion::Header`], [`DefaultRegion::Content`] y [`DefaultRegion::Footer`].
|
||||
///
|
||||
/// Se puede sobrescribir este método para:
|
||||
///
|
||||
|
|
@ -230,9 +175,9 @@ pub trait Template: Send + Sync {
|
|||
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
|
||||
async fn render(&self, cx: &mut Context) -> Markup {
|
||||
html! {
|
||||
(DefaultRegions::Header.render(cx).await)
|
||||
(DefaultRegions::Content.render(cx).await)
|
||||
(DefaultRegions::Footer.render(cx).await)
|
||||
(DefaultRegion::Header.render(cx).await)
|
||||
(DefaultRegion::Content.render(cx).await)
|
||||
(DefaultRegion::Footer.render(cx).await)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,11 +185,11 @@ pub trait Template: Send + Sync {
|
|||
/// Referencia estática a una plantilla.
|
||||
pub type TemplateRef = &'static dyn Template;
|
||||
|
||||
// **< DefaultTemplates >***************************************************************************
|
||||
// **< DefaultTemplate >****************************************************************************
|
||||
|
||||
/// Plantillas que PageTop proporciona por defecto.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum DefaultTemplates {
|
||||
pub enum DefaultTemplate {
|
||||
/// Plantilla predeterminada.
|
||||
///
|
||||
/// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se
|
||||
|
|
@ -252,15 +197,15 @@ pub enum DefaultTemplates {
|
|||
#[default]
|
||||
Standard,
|
||||
|
||||
/// Plantilla para la **interfaz de administración**.
|
||||
/// Plantilla de error.
|
||||
///
|
||||
/// Se utiliza para páginas de administración o paneles de control. Por defecto utiliza la misma
|
||||
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto utiliza la misma
|
||||
/// implementación de [`Template::render()`] que [`Self::Standard`].
|
||||
Admin,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Template for DefaultTemplates {}
|
||||
impl Template for DefaultTemplate {}
|
||||
|
||||
// **< render_component! >**************************************************************************
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::async_trait;
|
|||
use crate::base::component::{Html, Intro, IntroOpening};
|
||||
use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual};
|
||||
use crate::core::extension::Extension;
|
||||
use crate::core::theme::{DefaultRegions, DefaultTemplates, TemplateRef};
|
||||
use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef};
|
||||
use crate::global;
|
||||
use crate::html::{Markup, html};
|
||||
use crate::locale::L10n;
|
||||
|
|
@ -66,25 +66,14 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// ([`Page`](crate::response::page::Page)) por si no se elige ninguna otra plantilla con
|
||||
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
|
||||
///
|
||||
/// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Standard`] con una
|
||||
/// estructura básica para la página. Los temas pueden sobrescribir este método para seleccionar
|
||||
/// otra plantilla predeterminada o una plantilla propia.
|
||||
/// La implementación por defecto devuelve la plantilla estándar ([`DefaultTemplate::Standard`])
|
||||
/// con una estructura básica para la página. Los temas pueden sobrescribir este método para
|
||||
/// seleccionar otra plantilla predeterminada o una plantilla propia.
|
||||
#[inline]
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
self.parent()
|
||||
.map_or(&DefaultTemplates::Standard, |p| p.default_template())
|
||||
}
|
||||
|
||||
/// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el tema propone para
|
||||
/// la interfaz de administración.
|
||||
///
|
||||
/// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Admin`] con una
|
||||
/// estructura básica para la interfaz de administración. Los temas pueden sobrescribir este
|
||||
/// método para seleccionar otra plantilla predeterminada o una plantilla propia.
|
||||
#[inline]
|
||||
fn admin_template(&self) -> TemplateRef {
|
||||
self.parent()
|
||||
.map_or(&DefaultTemplates::Admin, |p| p.admin_template())
|
||||
self.parent().map_or(&DefaultTemplate::Standard, |parent| {
|
||||
parent.default_template()
|
||||
})
|
||||
}
|
||||
|
||||
/// Acciones específicas del tema antes de renderizar el `<body>` de la página.
|
||||
|
|
@ -112,9 +101,9 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// partir de las regiones.
|
||||
///
|
||||
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
|
||||
/// [`DefaultRegions::Header`](crate::core::theme::DefaultRegions::Header),
|
||||
/// [`DefaultRegions::Content`](crate::core::theme::DefaultRegions::Content) y
|
||||
/// [`DefaultRegions::Footer`](crate::core::theme::DefaultRegions::Footer) en ese orden.
|
||||
/// [`DefaultRegion::Header`](crate::core::theme::DefaultRegion::Header),
|
||||
/// [`DefaultRegion::Content`](crate::core::theme::DefaultRegion::Content) y
|
||||
/// [`DefaultRegion::Footer`](crate::core::theme::DefaultRegion::Footer) en ese orden.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para:
|
||||
///
|
||||
|
|
@ -247,16 +236,16 @@ pub trait Theme: Extension + Send + Sync {
|
|||
|
||||
/// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado).
|
||||
///
|
||||
/// Normalmente se renderiza con la plantilla predeterminada del tema (por defecto suele ser
|
||||
/// [`DefaultTemplates::Standard`]), para que el usuario no pierda el contexto de navegación del
|
||||
/// sitio. Los temas pueden sobrescribir este método para personalizar completamente el diseño y
|
||||
/// el contenido de la página de error.
|
||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||
/// página de error.
|
||||
fn error_403(&self, page: &mut Page) {
|
||||
if let Some(parent) = self.parent() {
|
||||
return parent.error_403(page);
|
||||
}
|
||||
page.alter_title(L10n::l("error403_title")).alter_child_in(
|
||||
&DefaultRegions::Content,
|
||||
page.alter_title(L10n::l("error403_title"))
|
||||
.alter_template(&DefaultTemplate::Error)
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Prepend(
|
||||
Html::with(move |cx| {
|
||||
html! {
|
||||
|
|
@ -273,15 +262,16 @@ pub trait Theme: Extension + Send + Sync {
|
|||
|
||||
/// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado).
|
||||
///
|
||||
/// Normalmente se renderiza con la plantilla predeterminada del tema (por defecto suele ser
|
||||
/// [`DefaultTemplates::Standard`]). Los temas pueden sobrescribir este método para personalizar
|
||||
/// completamente el diseño y el contenido de la página de error.
|
||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||
/// página de error.
|
||||
fn error_404(&self, page: &mut Page) {
|
||||
if let Some(parent) = self.parent() {
|
||||
return parent.error_404(page);
|
||||
}
|
||||
page.alter_title(L10n::l("error404_title")).alter_child_in(
|
||||
&DefaultRegions::Content,
|
||||
page.alter_title(L10n::l("error404_title"))
|
||||
.alter_template(&DefaultTemplate::Error)
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Prepend(
|
||||
Html::with(move |cx| {
|
||||
html! {
|
||||
|
|
@ -296,37 +286,25 @@ pub trait Theme: Extension + Send + Sync {
|
|||
);
|
||||
}
|
||||
|
||||
/// Permite al tema preparar y componer una página de **error fatal controlado**.
|
||||
/// Permite al tema preparar y componer una página de error fatal.
|
||||
///
|
||||
/// Esta función decide explícitamente devolver
|
||||
/// [`ErrorPage::BadRequest`](crate::response::page::ErrorPage::BadRequest),
|
||||
/// [`ErrorPage::InternalError`](crate::response::page::ErrorPage::InternalError),
|
||||
/// [`ErrorPage::ServiceUnavailable`](crate::response::page::ErrorPage::ServiceUnavailable) o
|
||||
/// [`ErrorPage::GatewayTimeout`](crate::response::page::ErrorPage::GatewayTimeout) porque algo
|
||||
/// ha fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de
|
||||
/// componentes funcionan con normalidad.
|
||||
///
|
||||
/// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya
|
||||
/// activa en la página (normalmente [`DefaultTemplates::Standard`]) y muestra un componente
|
||||
/// [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y
|
||||
/// `help`) como descripción del error.
|
||||
/// Por defecto, asigna el título al documento (`title`) y muestra un componente [`Intro`] con
|
||||
/// el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y `help`) como
|
||||
/// descripción del error.
|
||||
///
|
||||
/// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni
|
||||
/// [`Self::error_404()`], que definen su propio contenido específico.
|
||||
///
|
||||
/// Tampoco cubre los **fallos catastróficos** (un `panic!` en un handler, componente o
|
||||
/// plantilla). Ese caso se intercepta en una última capa que responde con un HTML mínimo y
|
||||
/// autónomo, sin pasar por el tema ni por el ciclo de renderizado de componentes, porque no es
|
||||
/// seguro asumir que ese ciclo sigue funcionando tras un `panic!`.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||
/// página de error.
|
||||
fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) {
|
||||
if let Some(parent) = self.parent() {
|
||||
return parent.error_fatal(page, code, title, alert, help);
|
||||
}
|
||||
page.alter_title(title).alter_child_in(
|
||||
&DefaultRegions::Content,
|
||||
page.alter_title(title)
|
||||
.alter_template(&DefaultTemplate::Error)
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Prepend(
|
||||
Intro::new()
|
||||
.with_title(L10n::l("error_code").with_arg("code", code.to_string()))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::core::component::{Child, ChildOp, Children, Component};
|
||||
use crate::core::theme::{DefaultRegions, RegionRef, ThemeRef};
|
||||
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
||||
use crate::{AutoDefault, UniqueId, builder_fn};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
|
@ -118,15 +118,15 @@ impl ChildrenInRegions {
|
|||
/// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" }));
|
||||
///
|
||||
/// // Texto en la cabecera, visible en todos los temas.
|
||||
/// InRegion::Global(&DefaultRegions::Header).add(Html::with(|_| html! { "Publicidad" }));
|
||||
/// InRegion::Global(&DefaultRegion::Header).add(Html::with(|_| html! { "Publicidad" }));
|
||||
/// ```
|
||||
pub enum InRegion {
|
||||
/// Región principal de **contenido** por defecto.
|
||||
///
|
||||
/// Añade el componente a la región lógica de contenido principal de la aplicación. Por
|
||||
/// convención, esta región corresponde a [`DefaultRegions::Content`], cuyo nombre es
|
||||
/// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es
|
||||
/// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando
|
||||
/// directamente [`DefaultRegions::Content`] o cualquier otra implementación de
|
||||
/// directamente [`DefaultRegion::Content`] o cualquier otra implementación de
|
||||
/// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los
|
||||
/// componentes registrados aquí, aunque lo harán según su propio método de renderizado
|
||||
/// ([`Region::render()`](crate::core::theme::Region::render)).
|
||||
|
|
@ -163,19 +163,19 @@ impl InRegion {
|
|||
/// }));
|
||||
///
|
||||
/// // Texto en la cabecera.
|
||||
/// InRegion::Global(&DefaultRegions::Header).add(Html::with(|_| {
|
||||
/// InRegion::Global(&DefaultRegion::Header).add(Html::with(|_| {
|
||||
/// html! { "Publicidad" }
|
||||
/// }));
|
||||
///
|
||||
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
||||
/// InRegion::ForTheme(&theme::Basic, &DefaultRegions::Footer).add(Html::with(|_| {
|
||||
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Html::with(|_| {
|
||||
/// html! { "Aviso legal" }
|
||||
/// }));
|
||||
/// ```
|
||||
pub fn add(&self, component: impl Component + Clone + 'static) -> &Self {
|
||||
let proto: Arc<dyn ComponentGlobal> = Arc::new(component);
|
||||
match self {
|
||||
InRegion::Content => Self::add_to_common(&DefaultRegions::Content, proto),
|
||||
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, proto),
|
||||
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, proto),
|
||||
InRegion::ForTheme(theme_ref, region_ref) => {
|
||||
THEME_REGIONS
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::{AutoDefault, CowStr, Weight, util};
|
|||
/// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto a otros scripts.
|
||||
/// - [`Inline`] - Inserta el código directamente en la etiqueta `<script>`.
|
||||
/// - [`OnLoad`] - Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
|
||||
/// - [`OnLoadAsync`] - Igual que [`OnLoad`], pero con handler asíncrono (`async`), útil si dentro
|
||||
/// - [`OnLoadAsync`] - Igual que [`OnLoad`], pero con *handler* asíncrono (`async`), útil si dentro
|
||||
/// del código JavaScript se utiliza `await`.
|
||||
#[derive(AutoDefault)]
|
||||
enum Source {
|
||||
|
|
@ -27,7 +27,7 @@ enum Source {
|
|||
Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
/// `name`, `closure(&mut Context) -> String` (se ejecuta tras `DOMContentLoaded`).
|
||||
OnLoad(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
/// `name`, `closure(&mut Context) -> String` (handler `async` tras `DOMContentLoaded`).
|
||||
/// `name`, `closure(&mut Context) -> String` (*handler* `async` tras `DOMContentLoaded`).
|
||||
OnLoadAsync(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ enum Source {
|
|||
/// }
|
||||
/// "#.to_string());
|
||||
///
|
||||
/// // Script embebido con handler asíncrono (`async`) que puede usar `await`.
|
||||
/// // Script embebido con *handler* asíncrono (`async`) que puede usar `await`.
|
||||
/// let mut cx = Context::new(None).with_param("user_id", 7u32);
|
||||
///
|
||||
/// let js = JavaScript::on_load_async("hydrate", |cx| {
|
||||
|
|
@ -150,7 +150,7 @@ impl JavaScript {
|
|||
}
|
||||
}
|
||||
|
||||
/// Crea un **script embebido** con un **handler asíncrono**.
|
||||
/// Crea un **script embebido** con un ***handler* asíncrono**.
|
||||
///
|
||||
/// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
|
||||
/// emplea una función `async` para que el cuerpo devuelto por la función *closure* pueda usar
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@
|
|||
|
||||
mod error;
|
||||
pub use error::ErrorPage;
|
||||
pub(crate) use error::{render_error_pages, response_for_panic, route_not_found};
|
||||
pub(crate) use error::render_error_pages;
|
||||
|
||||
use crate::auth::CurrentUser;
|
||||
use crate::base::action;
|
||||
use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual};
|
||||
use crate::core::theme::{DefaultRegions, Region, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
use crate::html::{Attr, Props, PropsOp};
|
||||
use crate::html::{DOCTYPE, Markup, html};
|
||||
|
|
@ -98,8 +98,8 @@ impl Page {
|
|||
/// Crea una nueva instancia de página.
|
||||
///
|
||||
/// La petición HTTP se guarda en el contexto de renderizado, que extrae automáticamente el
|
||||
/// [`CurrentUser`] inyectado por middleware en sus extensiones (ver
|
||||
/// [`Context::new`](crate::core::component::Context::new)). Cualquier handler tiene acceso al
|
||||
/// [`CurrentUser`] inyectado por *middleware* en sus extensiones (ver
|
||||
/// [`Context::new`](crate::core::component::Context::new)). Cualquier *handler* tiene acceso al
|
||||
/// usuario actual desde el momento en que se crea la página, sin llamadas adicionales.
|
||||
#[rustfmt::skip]
|
||||
pub fn new(request: HttpRequest) -> Self {
|
||||
|
|
@ -112,13 +112,6 @@ impl Page {
|
|||
}
|
||||
}
|
||||
|
||||
/// Crea una nueva instancia de página con la plantilla de administración del tema activo.
|
||||
pub fn admin(request: HttpRequest) -> Self {
|
||||
let mut page = Page::new(request);
|
||||
page.context().use_admin_template();
|
||||
page
|
||||
}
|
||||
|
||||
// **< Page BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el título de la página como un valor traducible.
|
||||
|
|
@ -311,7 +304,7 @@ impl Contextual for Page {
|
|||
#[builder_fn]
|
||||
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.context
|
||||
.alter_child_in(&DefaultRegions::Content, op.into());
|
||||
.alter_child_in(&DefaultRegion::Content, op.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,12 @@ use axum::extract::Request;
|
|||
use axum::middleware::Next;
|
||||
|
||||
use crate::core::component::Contextual;
|
||||
use crate::html::Markup;
|
||||
use crate::locale::L10n;
|
||||
use crate::util;
|
||||
use crate::web::{HttpRequest, IntoResponse, Response, http};
|
||||
use crate::{trace, util};
|
||||
|
||||
use super::Page;
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
// **< Errores controlados >************************************************************************
|
||||
|
||||
/// Página de error asociada a un código de estado HTTP.
|
||||
///
|
||||
/// Este enumerado agrupa tipos esenciales de error que pueden devolverse como página HTML completa.
|
||||
|
|
@ -20,7 +15,7 @@ use std::any::Any;
|
|||
/// de estado concreto.
|
||||
///
|
||||
/// Para cada error se construye una [`Page`] usando el tema activo, lo que permite personalizar la
|
||||
/// plantilla y el contenido del mensaje mediante los métodos específicos del tema (como
|
||||
/// plantilla y el contenido del mensaje mediante los métodos específicos del tema (por ejemplo,
|
||||
/// [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
||||
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) o
|
||||
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
||||
|
|
@ -100,58 +95,14 @@ impl IntoResponse for ErrorPage {
|
|||
}
|
||||
}
|
||||
|
||||
// Gestión de las rutas sin coincidencia.
|
||||
// Intercepta respuestas con un [`ErrorPage`] pendiente y las convierte en páginas HTML completas
|
||||
// usando el tema activo.
|
||||
//
|
||||
// Se registra como `.fallback()` del router principal desde [`Application`](crate::Application).
|
||||
pub(crate) async fn route_not_found(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
Err(ErrorPage::NotFound(request))
|
||||
}
|
||||
|
||||
// Intercepta respuestas con un [`ErrorPage`] pendiente y las convierte en páginas HTML.
|
||||
//
|
||||
// Se registra globalmente sobre el router principal desde [`Application`](crate::Application).
|
||||
pub(crate) async fn render_error_pages(request: Request, next: Next) -> Response {
|
||||
let mut response = next.run(request).await;
|
||||
// Se registra globalmente sobre el router principal desde [`crate::app`].
|
||||
pub(crate) async fn render_error_pages(req: Request, next: Next) -> Response {
|
||||
let mut response = next.run(req).await;
|
||||
if let Some(error_page) = response.extensions_mut().remove::<ErrorPage>() {
|
||||
return error_page.render_html().await;
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
// **< Fallo catastrófico >*************************************************************************
|
||||
|
||||
// HTML mínimo para un fallo catastrófico (`panic!`). No usa el tema, ni componentes, ni acceso a
|
||||
// datos, porque el fallo podría estar precisamente ahí. Tampoco se traduce porque ni siquiera el
|
||||
// sistema de localización es seguro; tampoco hay forma de elegir idioma vía `Accept-Language`.
|
||||
const FATAL_ERROR_HTML: &str = concat!(
|
||||
"<!DOCTYPE html>",
|
||||
"<html lang=\"en\">",
|
||||
"<head>",
|
||||
"<meta charset=\"utf-8\">",
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
|
||||
"<title>Unexpected error</title>",
|
||||
"</head><body>",
|
||||
"<h1>An unexpected error has occurred</h1>",
|
||||
"<p>Sorry for the inconvenience. Please try again or contact your system administrator.</p>",
|
||||
"</body>",
|
||||
"</html>",
|
||||
);
|
||||
|
||||
// Captura el fallo catastrófico (`panic!`) con el motivo para diagnóstico y responde un HTTP 500.
|
||||
//
|
||||
// Se registra sobre el router principal desde [`Application`](crate::Application).
|
||||
pub(crate) fn response_for_panic(err: Box<dyn Any + Send + 'static>) -> Response {
|
||||
let reason = err
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| err.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "panic with no message".to_string());
|
||||
trace::error!(panic = %reason, "Unhandled panic caught by CatchPanicLayer");
|
||||
|
||||
(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
FATAL_ERROR_HTML,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
|
|
|||
32
src/web.rs
32
src/web.rs
|
|
@ -39,9 +39,9 @@ use std::task::{Context, Poll};
|
|||
///
|
||||
/// Almacena los datos necesarios para negociar el idioma y renderizar las páginas de error,
|
||||
/// incluyendo la URI completa, las cabeceras de la petición original y las extensiones de tipo que
|
||||
/// los *middlewares* pueden inyectar antes de que el handler se ejecute.
|
||||
/// los *middlewares* pueden inyectar antes de que el *handler* se ejecute.
|
||||
///
|
||||
/// Puede declararse directamente como parámetro en un handler para pasarlo al
|
||||
/// Puede declararse directamente como parámetro en un *handler* para pasarlo al
|
||||
/// [`Context`](crate::core::component::Context) de renderizado y a las variantes de
|
||||
/// [`ErrorPage`](crate::response::page::ErrorPage):
|
||||
///
|
||||
|
|
@ -53,8 +53,8 @@ use std::task::{Context, Poll};
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Las extensiones inyectadas por middleware son accesibles vía [`extension`](Self::extension). Por
|
||||
/// ejemplo, desde un handler, tras añadir `MyData` en un middleware:
|
||||
/// Las extensiones inyectadas por *middleware* son accesibles vía [`extension`](Self::extension).
|
||||
/// Por ejemplo, desde un *handler*, tras añadir `MyData` en un *middleware*:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// if let Some(data) = request.extension::<MyData>() {
|
||||
|
|
@ -62,11 +62,11 @@ use std::task::{Context, Poll};
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Orden de parámetros en el handler
|
||||
/// # Orden de parámetros en el *handler*
|
||||
///
|
||||
/// `HttpRequest` toma las extensiones de la petición al extraerse. Los extractores que leen de ahí
|
||||
/// (como `Path<T>` o `Extension<T>`) deben aparecer **antes** en la lista de parámetros del
|
||||
/// handler:
|
||||
/// *handler*:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -114,10 +114,10 @@ impl HttpRequest {
|
|||
&self.headers
|
||||
}
|
||||
|
||||
/// Accede a un valor inyectado por middleware en las extensiones de la petición.
|
||||
/// Accede a un valor inyectado por *middleware* en las extensiones de la petición.
|
||||
///
|
||||
/// Devuelve una referencia al tipo `T` si algún middleware lo insertó antes de que el handler
|
||||
/// recibiera la petición, o `None` en caso contrario.
|
||||
/// Devuelve una referencia al tipo `T` si algún *middleware* lo insertó antes de que el
|
||||
/// *handler* recibiera la petición, o `None` en caso contrario.
|
||||
///
|
||||
/// El tipo debe implementar `Send + Sync + 'static` (requisito de [`http::Extensions`]).
|
||||
pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
|
||||
|
|
@ -128,11 +128,11 @@ impl HttpRequest {
|
|||
impl<S: Send + Sync> FromRequestParts<S> for HttpRequest {
|
||||
type Rejection = Infallible;
|
||||
|
||||
// Extrae la petición y toma las extensiones que han sido inyectadas por middleware. Las
|
||||
// Extrae la petición y toma las extensiones que han sido inyectadas por *middleware*. Las
|
||||
// extensiones se mueven a un `Arc` compartido para que `HttpRequest` sea `Clone`.
|
||||
//
|
||||
// Nota: tras este extractor `parts.extensions` queda vacío; otros extractores que dependan de
|
||||
// `Extension<T>` deben registrarse antes en la cadena del handler.
|
||||
// `Extension<T>` deben registrarse antes en la cadena del *handler*.
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
_state: &S,
|
||||
|
|
@ -388,7 +388,7 @@ pub mod test {
|
|||
|
||||
/// Inserta un valor en las extensiones de la petición.
|
||||
///
|
||||
/// Útil para simular lo que un middleware haría en producción antes de que el handler
|
||||
/// Útil para simular lo que un *middleware* haría en producción antes de que el *handler*
|
||||
/// reciba la petición. El tipo debe implementar `Clone + Send + Sync + 'static`.
|
||||
pub fn with_extension<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
|
||||
self.extensions.insert(value);
|
||||
|
|
@ -422,12 +422,4 @@ pub mod test {
|
|||
pub async fn send_request(router: &Router, req: http::Request<Body>) -> http::Response<Body> {
|
||||
router.clone().oneshot(req).await.unwrap()
|
||||
}
|
||||
|
||||
/// Devuelve el cuerpo de una respuesta como texto UTF-8, para comprobaciones en tests.
|
||||
pub async fn read_body_text(response: http::Response<Body>) -> String {
|
||||
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< CatchPanicLayer >****************************************************************************
|
||||
|
||||
struct PanicExtension;
|
||||
|
||||
#[async_trait]
|
||||
impl Extension for PanicExtension {
|
||||
fn configure_router(&self, router: Router) -> Router {
|
||||
router.route("/boom", web::get(boom))
|
||||
}
|
||||
}
|
||||
|
||||
async fn boom() -> Result<Markup, ErrorPage> {
|
||||
panic!("boom")
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn panic_in_handler_returns_minimal_500_page_instead_of_crashing() {
|
||||
let app = web::test::init_router(Application::prepare(&PanicExtension).await.test());
|
||||
|
||||
let req = web::test::TestRequest::get().uri("/boom").to_request();
|
||||
let resp = web::test::send_request(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), web::http::StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(
|
||||
resp.headers().get(web::http::header::CONTENT_TYPE).unwrap(),
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
|
||||
let body = web::test::read_body_text(resp).await;
|
||||
assert!(body.contains("An unexpected error has occurred"));
|
||||
}
|
||||
|
||||
// **< ErrorPage::NotFound >************************************************************************
|
||||
|
||||
// `EXTENSIONS` es un `OnceLock` global (`core/extension/all.rs`): se inicializa una sola vez por
|
||||
// binario de test. Todos los tests de este fichero comparten la misma extensión raíz
|
||||
// (`PanicExtension`) para que el orden de ejecución en paralelo no cambie qué rutas quedan
|
||||
// registradas.
|
||||
#[pagetop::test]
|
||||
async fn unknown_route_returns_themed_404_page() {
|
||||
let app = web::test::init_router(Application::prepare(&PanicExtension).await.test());
|
||||
|
||||
let req = web::test::TestRequest::get()
|
||||
.uri("/does-not-exist")
|
||||
.to_request();
|
||||
let resp = web::test::send_request(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), web::http::StatusCode::NOT_FOUND);
|
||||
|
||||
let body = web::test::read_body_text(resp).await;
|
||||
assert!(body.contains("<html"));
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Tema con plantilla propia >******************************************************************
|
||||
|
||||
struct MarkerTemplate;
|
||||
|
||||
#[async_trait]
|
||||
impl Template for MarkerTemplate {
|
||||
async fn render(&self, _cx: &mut Context) -> Markup {
|
||||
html! { "marker-template-output" }
|
||||
}
|
||||
}
|
||||
|
||||
struct MarkerTheme;
|
||||
|
||||
#[async_trait]
|
||||
impl Extension for MarkerTheme {
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
Some(&Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Theme for MarkerTheme {
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
&MarkerTemplate
|
||||
}
|
||||
|
||||
fn admin_template(&self) -> TemplateRef {
|
||||
&MarkerTemplate
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_active_template(cx: &mut Context) -> String {
|
||||
let template = cx.template();
|
||||
template.render(cx).await.into_string()
|
||||
}
|
||||
|
||||
// **< Context::template() sigue al tema activo >***************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn with_theme_updates_the_effective_template() {
|
||||
// Sin cambiar de tema, la plantilla activa no es la de `MarkerTheme`.
|
||||
let mut cx = Context::new(None);
|
||||
assert_ne!(
|
||||
render_active_template(&mut cx).await,
|
||||
"marker-template-output"
|
||||
);
|
||||
|
||||
// Tras cambiar de tema con `with_theme()`, la plantilla activa pasa a ser la de ese tema, sin
|
||||
// necesidad de llamar a `with_template()` explícitamente.
|
||||
let mut cx = Context::new(None).with_theme(&MarkerTheme);
|
||||
assert_eq!(
|
||||
render_active_template(&mut cx).await,
|
||||
"marker-template-output"
|
||||
);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
|
||||
// Una plantilla fijada explícitamente con `with_template()` prevalece aunque `with_theme()` se
|
||||
// llame después, en cualquier orden.
|
||||
let mut cx = Context::new(None)
|
||||
.with_template(&MarkerTemplate)
|
||||
.with_theme(&pagetop::base::theme::Basic);
|
||||
|
||||
assert_eq!(
|
||||
render_active_template(&mut cx).await,
|
||||
"marker-template-output"
|
||||
);
|
||||
}
|
||||
|
||||
// **< Page::admin() sigue al tema activo >*********************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn page_admin_template_follows_a_later_with_theme() {
|
||||
let request = web::test::TestRequest::get().to_http_request();
|
||||
|
||||
let mut page = Page::admin(request).with_theme(&MarkerTheme);
|
||||
let markup = page.context().template().render(page.context()).await;
|
||||
|
||||
assert_eq!(markup.into_string(), "marker-template-output");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue