🐛 (web): HttpRequest clona ahora las extensiones

Documenta el nuevo comportamiento y aprovecha para pulir varios
comentarios y restringir visibilidad de símbolos internos.
This commit is contained in:
Manuel Cillero 2026-07-16 19:54:09 +02:00
parent 370bc8ee47
commit 6f82da220b
11 changed files with 55 additions and 56 deletions

View file

@ -10,8 +10,8 @@ impl Extension for HelloName {
}
async fn hello_name(
web::Path(name): web::Path<String>,
request: HttpRequest,
web::Path(name): web::Path<String>,
) -> Result<Markup, ErrorPage> {
Page::new(request)
.with_child(Html::with(move |_| {

View file

@ -21,7 +21,7 @@ use std::sync::Arc;
/// });
/// ```
///
/// Para renderizar contenido que dependa del contexto, se puede acceder a él dentro del *closure*:
/// Para renderizar contenido que dependa del contexto, se puede acceder a él dentro del closure:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;

View file

@ -73,7 +73,7 @@ where
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
///
/// Funciona igual que [`dispatch_actions`], pero el *closure* puede devolver
/// Funciona igual que [`dispatch_actions`], pero el closure puede devolver
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente.
///

View file

@ -310,7 +310,8 @@ impl Children {
}
}
// Añade un componente hijo al final de la lista.
// Añade un componente hijo al final de la lista. También lo usa `core::theme::regions` al
// fusionar regiones, fuera de este módulo.
#[inline]
pub(crate) fn add(&mut self, child: Child) -> &mut Self {
self.0.push(child);

View file

@ -110,15 +110,27 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
///
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras
/// [`setup()`](Self::setup) y la acción
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solamente si ningún
/// tema en la cadena devuelve `Some` en
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
/// Este es el cuarto paso del [ciclo de renderizado](ComponentRender) tras llamar al
/// [`setup()`](Self::setup) del componente y despachar la acción
/// [`BeforeRender`](crate::base::action::component::BeforeRender) que atiende los cambios de
/// otras extensiones antes de renderizar. Se invoca sólo si ningún tema en la cadena devuelve
/// `Some` en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) para
/// este componente.
///
/// Es `async`, a diferencia de [`setup()`](Self::setup), para permitir a los componentes
/// realizar aquí sus llamadas asíncronas, como consultas a base de datos, peticiones a
/// servicios externos, o cualquier operación de E/S, que necesiten para preparar su contenido.
///
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
/// temas puedan implementar `handle_component()` sin depender de los detalles internos.
///
/// Los campos que representen contenido no deben almacenar [`Markup`] ya generado, sino que
/// guardarán el dato en bruto, por ejemplo [`L10n`](crate::locale::L10n) para textos
/// traducibles, o un componente anidado como [`Html`](crate::base::component::Html), o
/// cualquier otro `Component`, normalmente a través de [`Embed`](crate::core::component::Embed)
/// o [`Child`](crate::core::component::Child); y será este método quien lo convierta en HTML en
/// el momento del renderizado, no quien construye la instancia.
///
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
#[allow(unused_variables)]

View file

@ -51,10 +51,9 @@ impl ComponentError {
// **< ComponentError GETTERS >*****************************************************************
/// Consume el error y devuelve su marcado alternativo.
///
/// Se invoca internamente en [`ComponentRender`](crate::core::component::ComponentRender).
pub(crate) fn into_fallback(self) -> Markup {
// Consume el error y devuelve su marcado alternativo. Se invoca internamente desde
// `ComponentRender::render()`, en el mismo módulo `core::component`.
pub(super) fn into_fallback(self) -> Markup {
self.fallback
}
}

View file

@ -119,7 +119,7 @@ impl JavaScript {
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
/// script.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
@ -139,7 +139,7 @@ impl JavaScript {
///
/// En condiciones normales, los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn on_load<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,
@ -153,11 +153,11 @@ impl JavaScript {
/// 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
/// emplea una función `async` para que el cuerpo devuelto por la función closure pueda usar
/// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas
/// iniciales.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn on_load_async<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,

View file

@ -7,7 +7,7 @@ use crate::{AutoDefault, CowStr, Weight, util};
// Informa al navegador de la naturaleza del recurso para que pueda asignarle la prioridad correcta
// y aplicar la política de caché adecuada.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub(crate) enum AsType {
enum AsType {
// Fuente web (`@font-face`). Implica `crossorigin` automático.
#[default]
Font,

View file

@ -100,7 +100,7 @@ impl StyleSheet {
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
/// recurso.
///
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
/// La función closure recibirá el [`Context`] por si se necesita durante el renderizado.
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
where
F: Fn(&mut Context) -> String + Send + Sync + 'static,

View file

@ -5,17 +5,19 @@ use super::{LanguageIdentifier, langid};
use std::collections::HashMap;
use std::sync::LazyLock;
/// Tabla de idiomas soportados por PageTop.
///
/// Cada entrada asocia un código de idioma en minúsculas (por ejemplo, `"en"` o `"es-es"`) con:
///
/// - Su [`LanguageIdentifier`] canónico.
/// - La clave de traducción definida en `src/locale/{lang}/languages.ftl` para mostrar su nombre en
/// el idioma activo.
///
/// Esto permite admitir alias de idioma como `"en"` o `"es"` y, al mismo tiempo, mantener un
/// identificador de idioma canónico (por ejemplo, `langid!("en-US")` o `langid!("es-ES")`).
pub(crate) static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> =
// Tabla de idiomas soportados por PageTop.
//
// Cada entrada asocia un código de idioma en minúsculas (por ejemplo, "en" o "es-es") con:
//
// - Su `LanguageIdentifier` canónico.
// - La clave de traducción definida en `src/locale/{lang}/languages.ftl` para mostrar su nombre en
// el idioma activo.
//
// Esto permite admitir alias de idioma como "en" o "es" y, al mismo tiempo, mantener un
// identificador de idioma canónico (por ejemplo, `langid!("en-US")` o `langid!("es-ES")`).
//
// Sólo lo usa `locale::definition`, en el mismo módulo `locale`.
pub(super) static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> =
LazyLock::new(|| {
util::kv![
"en" => ( langid!("en-US"), "english" ),

View file

@ -66,25 +66,12 @@ use std::task::{Context, Poll};
/// }
/// ```
///
/// # 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:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// // Correcto: Path<T> se declara antes que HttpRequest.
/// async fn view_post(
/// web::Path(id): web::Path<u32>,
/// request: HttpRequest,
/// ) -> Result<Markup, ErrorPage> {
/// # todo!()
/// }
/// ```
///
/// Los extractores que no dependen de las extensiones, como `Query<T>` o `Method`, no están sujetos
/// a esta restricción.
/// `HttpRequest` no consume las extensiones de la petición, por lo que el resto de extractores del
/// handler que también las necesiten (como `Path<T>`, `Extension<T>`...) las siguen viendo
/// intactas, sea cual sea la posición en la que se declare `HttpRequest`. Por convención, se
/// declara como primer parámetro del handler. El único orden que sigue siendo obligatorio es el que
/// impone Axum: un extractor que consuma el cuerpo de la petición (`Form<T>`, `RawForm`...) debe ir
/// siempre el último.
#[derive(Clone, Debug)]
pub struct HttpRequest {
uri: http::Uri,
@ -132,20 +119,18 @@ 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
// 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.
// Clona (no toma) las extensiones inyectadas por middleware, para que otros extractores del
// handler (`Path<T>`, `Extension<T>`...) las sigan viendo intactas sin importar en qué posición
// se declare `HttpRequest`. El clon se envuelve en un `Arc` compartido para que `HttpRequest`
// sea `Clone` a coste mínimo en el resto de su ciclo de vida.
async fn from_request_parts(
parts: &mut http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
let extensions = std::mem::take(&mut parts.extensions);
Ok(HttpRequest {
uri: parts.uri.clone(),
headers: parts.headers.clone(),
extensions: Arc::new(extensions),
extensions: Arc::new(parts.extensions.clone()),
})
}
}