🐛 (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( async fn hello_name(
web::Path(name): web::Path<String>,
request: HttpRequest, request: HttpRequest,
web::Path(name): web::Path<String>,
) -> Result<Markup, ErrorPage> { ) -> Result<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(move |_| { .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 /// ```rust,no_run
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;

View file

@ -73,7 +73,7 @@ where
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada. /// 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::Continue`] para continuar ejecutando la siguiente acción; o
/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente. /// [`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] #[inline]
pub(crate) fn add(&mut self, child: Child) -> &mut Self { pub(crate) fn add(&mut self, child: Child) -> &mut Self {
self.0.push(child); 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. /// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
/// ///
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras /// Este es el cuarto paso del [ciclo de renderizado](ComponentRender) tras llamar al
/// [`setup()`](Self::setup) y la acción /// [`setup()`](Self::setup) del componente y despachar la acción
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solamente si ningún /// [`BeforeRender`](crate::base::action::component::BeforeRender) que atiende los cambios de
/// tema en la cadena devuelve `Some` en /// otras extensiones antes de renderizar. Se invoca sólo si ningún tema en la cadena devuelve
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component). /// `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 /// 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. /// 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 /// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*). /// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
#[allow(unused_variables)] #[allow(unused_variables)]

View file

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

View file

@ -119,7 +119,7 @@ impl JavaScript {
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del /// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
/// script. /// 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 pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
where where
F: Fn(&mut Context) -> String + Send + Sync + 'static, 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`. /// 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 pub fn on_load<F>(name: impl Into<CowStr>, f: F) -> Self
where where
F: Fn(&mut Context) -> String + Send + Sync + 'static, F: Fn(&mut Context) -> String + Send + Sync + 'static,
@ -153,11 +153,11 @@ 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 /// 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 /// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas
/// iniciales. /// 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 pub fn on_load_async<F>(name: impl Into<CowStr>, f: F) -> Self
where where
F: Fn(&mut Context) -> String + Send + Sync + 'static, 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 // Informa al navegador de la naturaleza del recurso para que pueda asignarle la prioridad correcta
// y aplicar la política de caché adecuada. // y aplicar la política de caché adecuada.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub(crate) enum AsType { enum AsType {
// Fuente web (`@font-face`). Implica `crossorigin` automático. // Fuente web (`@font-face`). Implica `crossorigin` automático.
#[default] #[default]
Font, Font,

View file

@ -100,7 +100,7 @@ impl StyleSheet {
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del /// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
/// recurso. /// 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 pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
where where
F: Fn(&mut Context) -> String + Send + Sync + 'static, F: Fn(&mut Context) -> String + Send + Sync + 'static,

View file

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

View file

@ -66,25 +66,12 @@ use std::task::{Context, Poll};
/// } /// }
/// ``` /// ```
/// ///
/// # Orden de parámetros en el handler /// `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
/// `HttpRequest` toma las extensiones de la petición al extraerse. Los extractores que leen de ahí /// intactas, sea cual sea la posición en la que se declare `HttpRequest`. Por convención, se
/// (como `Path<T>` o `Extension<T>`) deben aparecer **antes** en la lista de parámetros del /// declara como primer parámetro del handler. El único orden que sigue siendo obligatorio es el que
/// handler: /// impone Axum: un extractor que consuma el cuerpo de la petición (`Form<T>`, `RawForm`...) debe ir
/// /// siempre el último.
/// ```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.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct HttpRequest { pub struct HttpRequest {
uri: http::Uri, uri: http::Uri,
@ -132,20 +119,18 @@ impl HttpRequest {
impl<S: Send + Sync> FromRequestParts<S> for HttpRequest { impl<S: Send + Sync> FromRequestParts<S> for HttpRequest {
type Rejection = Infallible; type Rejection = Infallible;
// Extrae la petición y toma las extensiones que han sido inyectadas por middleware. Las // Clona (no toma) las extensiones inyectadas por middleware, para que otros extractores del
// extensiones se mueven a un `Arc` compartido para que `HttpRequest` sea `Clone`. // 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`
// Nota: tras este extractor `parts.extensions` queda vacío; otros extractores que dependan de // sea `Clone` a coste mínimo en el resto de su ciclo de vida.
// `Extension<T>` deben registrarse antes en la cadena del handler.
async fn from_request_parts( async fn from_request_parts(
parts: &mut http::request::Parts, parts: &mut http::request::Parts,
_state: &S, _state: &S,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
let extensions = std::mem::take(&mut parts.extensions);
Ok(HttpRequest { Ok(HttpRequest {
uri: parts.uri.clone(), uri: parts.uri.clone(),
headers: parts.headers.clone(), headers: parts.headers.clone(),
extensions: Arc::new(extensions), extensions: Arc::new(parts.extensions.clone()),
}) })
} }
} }