🎨 Convierte el ciclo de renderizado en async

`prepare()` de `Component`, `render()` de `Region` y `Template`, y la
implementación de `ComponentRender` pasan a ser funciones async. Se
actualizan todos los componentes base, helpers, tests y ejemplos.
This commit is contained in:
Manuel Cillero 2026-07-06 00:48:41 +02:00
parent 9acd8cc51a
commit 9354894b3a
48 changed files with 436 additions and 365 deletions

View file

@ -1,3 +1,6 @@
use axum::extract::Request;
use axum::middleware::Next;
use crate::core::component::Contextual;
use crate::locale::L10n;
use crate::util;
@ -5,20 +8,18 @@ use crate::web::{HttpRequest, IntoResponse, Response, http};
use super::Page;
use std::fmt;
/// 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.
/// Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con un código
/// 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
/// (por ejemplo, [`Theme::error_403()`](crate::core::theme::Theme::error_403),
/// 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 (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)).
#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum ErrorPage {
BadRequest(HttpRequest),
AccessDenied(HttpRequest),
@ -29,26 +30,6 @@ pub enum ErrorPage {
}
impl ErrorPage {
// Renderiza una página de error genérica usando el tema activo. Deriva las claves de
// localización del código de estado (`error<code>_title`, `_alert`, `_help`). Si el
// renderizado falla, escribe el texto plano del código de estado.
fn display_error_page(&self, f: &mut fmt::Formatter<'_>, request: &HttpRequest) -> fmt::Result {
let mut page = Page::new(request.clone());
let code = self.status_code();
page.theme().error_fatal(
&mut page,
code,
L10n::l(util::join!("error", code.as_str(), "_title")),
L10n::l(util::join!("error", code.as_str(), "_alert")),
L10n::l(util::join!("error", code.as_str(), "_help")),
);
if let Ok(rendered) = page.render() {
write!(f, "{}", rendered.into_string())
} else {
f.write_str(code.as_str())
}
}
/// Devuelve el código de estado HTTP asociado a la variante de error.
pub fn status_code(&self) -> http::StatusCode {
match self {
@ -60,59 +41,68 @@ impl ErrorPage {
ErrorPage::GatewayTimeout(_) => http::StatusCode::GATEWAY_TIMEOUT,
}
}
}
impl fmt::Display for ErrorPage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Error 400.
Self::BadRequest(request) => self.display_error_page(f, request),
// Error 403.
// Renderiza la página de error y construye la respuesta HTTP completa con el HTML generado. Si
// el renderizado falla, devuelve sólo el código de estado sin cuerpo.
async fn render_html(self) -> Response {
let status = self.status_code();
let mut page = match self {
Self::AccessDenied(request) => {
let mut page = Page::new(request.clone());
let mut page = Page::new(request);
page.theme().error_403(&mut page);
if let Ok(rendered) = page.render() {
write!(f, "{}", rendered.into_string())
} else {
f.write_str(self.status_code().as_str())
}
page
}
// Error 404.
Self::NotFound(request) => {
let mut page = Page::new(request.clone());
let mut page = Page::new(request);
page.theme().error_404(&mut page);
if let Ok(rendered) = page.render() {
write!(f, "{}", rendered.into_string())
} else {
f.write_str(self.status_code().as_str())
}
page
}
// Error 500.
Self::InternalError(request) => self.display_error_page(f, request),
// Error 503.
Self::ServiceUnavailable(request) => self.display_error_page(f, request),
// Error 504.
Self::GatewayTimeout(request) => self.display_error_page(f, request),
Self::BadRequest(request)
| Self::InternalError(request)
| Self::ServiceUnavailable(request)
| Self::GatewayTimeout(request) => {
let mut page = Page::new(request);
page.theme().error_fatal(
&mut page,
status,
L10n::l(util::join!("error", status.as_str(), "_title")),
L10n::l(util::join!("error", status.as_str(), "_alert")),
L10n::l(util::join!("error", status.as_str(), "_help")),
);
page
}
};
match page.render().await {
Ok(rendered) => (
status,
[(http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
rendered.into_string(),
)
.into_response(),
Err(_) => status.into_response(),
}
}
}
/// Convierte un [`ErrorPage`] en una respuesta HTTP con el código de estado adecuado y el cuerpo
/// HTML generado por el tema activo.
/// Convierte un [`ErrorPage`] en una respuesta HTTP que luego se convertirá en una página HTML
/// completa usando el tema activo.
impl IntoResponse for ErrorPage {
fn into_response(self) -> Response {
let status = self.status_code();
let body = self.to_string();
(
status,
[(http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
body,
)
.into_response()
let mut response = status.into_response();
response.extensions_mut().insert(self);
response
}
}
// Intercepta respuestas con un [`ErrorPage`] pendiente y las convierte en páginas HTML completas
// usando el tema activo.
//
// 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
}