Compare commits

..

6 commits

Author SHA1 Message Date
74dbcde5c5 ♻️ (core): Cambia HashMap x Vec en RegionComponents 2026-07-26 13:41:32 +02:00
7e6a9f8557 ♻️ (core): Clave &'static str en RegionComponents 2026-07-26 12:50:33 +02:00
845d91d8c1 ♻️ (core): Evita doble clonado de InRegion en Child
Los prototipos registrados con `InRegion` se clonaban dos veces por
petición: una al ensamblar la región (`ComponentGlobal::as_child`) y
otra al renderizar (`Child::render`). Ahora sólo se clonan al
renderizar.
2026-07-26 12:39:01 +02:00
ce6b4d2581 ♻️ (core): Simplifica Child a Arc<dyn Component>
Elimina el `RwLock<Box<dyn Component>>` interno ya que ningún punto del
ciclo de renderizado tomaba el write-lock.

De paso, `From<Embed<C>> for Child` pasa a una coerción de puntero sin
clonar, gracias al copy-on-write que ya ofrece `Embed::get_mut()`. Los
doctests en `rust,ignore` pasan a `rust,no_run`, verificados por el
compilador.
2026-07-26 11:30:16 +02:00
65c8a788c2 (web): Añade cabeceras HTTP a TestRequest 2026-07-26 10:03:54 +02:00
39f494539e ♻️ (htmx): Expone request y response como públicos 2026-07-26 10:02:36 +02:00
8 changed files with 154 additions and 114 deletions

View file

@ -381,7 +381,8 @@ pub fn on_htmx(event: &str) -> String {
/// ///
/// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con /// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con
/// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores /// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores
/// directamente, aunque lo habitual es usar el trait [`HtmxRequestExt`](crate::HtmxRequestExt). /// directamente, aunque lo habitual es usar el trait
/// [`HtmxRequestExt`](crate::request::HtmxRequestExt).
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
@ -417,7 +418,8 @@ pub mod request {
/// Cabeceras de respuesta HTTP para HTMX. /// Cabeceras de respuesta HTTP para HTMX.
/// ///
/// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas /// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas
/// manualmente, aunque lo habitual es usar el constructor [`HtmxResponse`](crate::HtmxResponse). /// manualmente, aunque lo habitual es usar el constructor
/// [`HtmxResponse`](crate::response::HtmxResponse).
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
@ -431,38 +433,38 @@ pub mod request {
/// ``` /// ```
pub mod response { pub mod response {
/// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver /// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver
/// [`HtmxResponse::location()`](crate::HtmxResponse::location) y /// [`HtmxResponse::location()`](crate::response::HtmxResponse::location) y
/// [`HtmxResponse::location_json()`](crate::HtmxResponse::location_json). /// [`HtmxResponse::location_json()`](crate::response::HtmxResponse::location_json).
pub const LOCATION: &str = "HX-Location"; pub const LOCATION: &str = "HX-Location";
/// Empuja la URL indicada al historial del navegador. Ver /// Empuja la URL indicada al historial del navegador. Ver
/// [`HtmxResponse::push_url()`](crate::HtmxResponse::push_url). /// [`HtmxResponse::push_url()`](crate::response::HtmxResponse::push_url).
pub const PUSH_URL: &str = "HX-Push-Url"; pub const PUSH_URL: &str = "HX-Push-Url";
/// Provoca una redirección completa del navegador. Ver /// Provoca una redirección completa del navegador. Ver
/// [`HtmxResponse::redirect()`](crate::HtmxResponse::redirect). /// [`HtmxResponse::redirect()`](crate::response::HtmxResponse::redirect).
pub const REDIRECT: &str = "HX-Redirect"; pub const REDIRECT: &str = "HX-Redirect";
/// Provoca una recarga completa de la página. Ver /// Provoca una recarga completa de la página. Ver
/// [`HtmxResponse::refresh()`](crate::HtmxResponse::refresh). /// [`HtmxResponse::refresh()`](crate::response::HtmxResponse::refresh).
pub const REFRESH: &str = "HX-Refresh"; pub const REFRESH: &str = "HX-Refresh";
/// Reemplaza la URL actual en el historial. Ver /// Reemplaza la URL actual en el historial. Ver
/// [`HtmxResponse::replace_url()`](crate::HtmxResponse::replace_url). /// [`HtmxResponse::replace_url()`](crate::response::HtmxResponse::replace_url).
pub const REPLACE_URL: &str = "HX-Replace-Url"; pub const REPLACE_URL: &str = "HX-Replace-Url";
/// Anula el `hx-swap` del elemento. Ver /// Anula el `hx-swap` del elemento. Ver
/// [`HtmxResponse::reswap()`](crate::HtmxResponse::reswap). /// [`HtmxResponse::reswap()`](crate::response::HtmxResponse::reswap).
pub const RESWAP: &str = "HX-Reswap"; pub const RESWAP: &str = "HX-Reswap";
/// Anula el `hx-target` del elemento. Ver /// Anula el `hx-target` del elemento. Ver
/// [`HtmxResponse::retarget()`](crate::HtmxResponse::retarget). /// [`HtmxResponse::retarget()`](crate::response::HtmxResponse::retarget).
pub const RETARGET: &str = "HX-Retarget"; pub const RETARGET: &str = "HX-Retarget";
/// Anula el `hx-select` del elemento. Ver /// Anula el `hx-select` del elemento. Ver
/// [`HtmxResponse::reselect()`](crate::HtmxResponse::reselect). /// [`HtmxResponse::reselect()`](crate::response::HtmxResponse::reselect).
pub const RESELECT: &str = "HX-Reselect"; pub const RESELECT: &str = "HX-Reselect";
/// Dispara eventos JavaScript al completar la respuesta. Ver /// Dispara eventos JavaScript al completar la respuesta. Ver
/// [`HtmxResponse::trigger()`](crate::HtmxResponse::trigger). /// [`HtmxResponse::trigger()`](crate::response::HtmxResponse::trigger).
pub const TRIGGER: &str = "HX-Trigger"; pub const TRIGGER: &str = "HX-Trigger";
/// Dispara eventos tras la fase *settle*. Ver /// Dispara eventos tras la fase *settle*. Ver
/// [`HtmxResponse::trigger_after_settle()`](crate::HtmxResponse::trigger_after_settle). /// [`HtmxResponse::trigger_after_settle()`](crate::response::HtmxResponse::trigger_after_settle).
pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle"; pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle";
/// Dispara eventos tras el *swap*. Ver /// Dispara eventos tras el *swap*. Ver
/// [`HtmxResponse::trigger_after_swap()`](crate::HtmxResponse::trigger_after_swap). /// [`HtmxResponse::trigger_after_swap()`](crate::response::HtmxResponse::trigger_after_swap).
pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap"; pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap";
} }

View file

@ -93,18 +93,13 @@ include_locales!(LOCALES_HTMX);
pub mod hx; pub mod hx;
pub mod hx_table; pub mod hx_table;
pub mod request;
mod request; pub mod response;
pub use request::HtmxRequestExt;
mod response;
pub use response::HtmxResponse;
/// Prelude de `pagetop-htmx`. /// Prelude de `pagetop-htmx`.
pub mod prelude { pub mod prelude {
pub use crate::hx; pub use crate::hx;
pub use crate::hx_table; pub use crate::hx_table;
pub use crate::request::HtmxRequestExt; pub use crate::request::HtmxRequestExt;
pub use crate::response::HtmxResponse; pub use crate::response::HtmxResponse;
} }

View file

@ -4,8 +4,7 @@ use pagetop::prelude::*;
// **< HtmxRequestExt >***************************************************************************** // **< HtmxRequestExt >*****************************************************************************
/// Extiende [`HttpRequest`](pagetop::web::HttpRequest) con métodos para detectar y leer peticiones /// Extiende [`HttpRequest`] con métodos para detectar y leer peticiones HTMX.
/// HTMX.
/// ///
/// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de /// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de
/// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente. /// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente.

View file

@ -1,4 +1,4 @@
//! Implementación de [`HtmxResponse`] e [`IntoResponse`](pagetop::web::IntoResponse) para HTMX. //! Implementación de [`HtmxResponse`] e [`IntoResponse`] para HTMX.
use pagetop::prelude::*; use pagetop::prelude::*;
@ -10,8 +10,7 @@ use pagetop::prelude::*;
/// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la /// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la
/// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc. /// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc.
/// ///
/// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse /// Implementa [`IntoResponse`], por lo que puede devolverse directamente desde cualquier handler.
/// directamente desde cualquier handler.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///

View file

@ -1,5 +1,5 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use pagetop_htmx::HtmxRequestExt; use pagetop_htmx::prelude::*;
struct TestApp; struct TestApp;

View file

@ -2,8 +2,6 @@ use crate::core::component::{Component, Context};
use crate::html::{Markup, html}; use crate::html::{Markup, html};
use crate::{AutoDefault, UniqueId, builder_fn}; use crate::{AutoDefault, UniqueId, builder_fn};
use parking_lot::RwLock;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use std::vec::IntoIter; use std::vec::IntoIter;
@ -11,14 +9,19 @@ use std::vec::IntoIter;
// **< Child >************************************************************************************** // **< Child >**************************************************************************************
/// Representa un componente hijo encapsulado para su uso en una lista [`Children`]. /// Representa un componente hijo encapsulado para su uso en una lista [`Children`].
///
/// Envuelve el componente en `Arc<dyn Component>`, compartido y de sólo lectura. Clonar un `Child`
/// sólo incrementa el contador de referencias. Para renderizar obtiene una copia propia con
/// [`ComponentClone::clone_box()`](crate::core::component::ComponentClone::clone_box), de modo que
/// el componente original nunca se modifica.
#[derive(AutoDefault, Clone)] #[derive(AutoDefault, Clone)]
pub struct Child(Option<Arc<RwLock<Box<dyn Component>>>>); pub struct Child(Option<Arc<dyn Component>>);
impl fmt::Debug for Child { impl fmt::Debug for Child {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 { match &self.0 {
None => write!(f, "Child(None)"), None => write!(f, "Child(None)"),
Some(c) => write!(f, "Child({})", c.read().name()), Some(c) => write!(f, "Child({})", c.name()),
} }
} }
} }
@ -26,7 +29,14 @@ impl fmt::Debug for Child {
impl Child { impl Child {
/// Crea un nuevo `Child` a partir de un componente. /// Crea un nuevo `Child` a partir de un componente.
pub fn with(component: impl Component) -> Self { pub fn with(component: impl Component) -> Self {
Child(Some(Arc::new(RwLock::new(Box::new(component))))) Child(Some(Arc::new(component)))
}
// Envuelve un `Arc` ya construido, sin clonar el componente. La usa `core::theme::regions` para
// registrar los prototipos de `InRegion` sin clonar su estado hasta que `render()` obtenga la
// copia propia que necesita mutar.
pub(crate) fn from_arc(component: Arc<dyn Component>) -> Self {
Child(Some(component))
} }
// **< Child BUILDER >************************************************************************** // **< Child BUILDER >**************************************************************************
@ -36,7 +46,7 @@ impl Child {
/// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia. /// Si se proporciona `Some(component)`, se encapsula como [`Child`]; y si es `None`, se limpia.
#[builder_fn] #[builder_fn]
pub fn with_component<C: Component>(mut self, component: Option<C>) -> Self { pub fn with_component<C: Component>(mut self, component: Option<C>) -> Self {
self.0 = component.map(|c| Arc::new(RwLock::new(Box::new(c) as Box<dyn Component>))); self.0 = component.map(|c| Arc::new(c) as Arc<dyn Component>);
self self
} }
@ -45,7 +55,7 @@ impl Child {
/// Devuelve el identificador del componente, si existe y está definido. /// Devuelve el identificador del componente, si existe y está definido.
#[inline] #[inline]
pub fn id(&self) -> Option<String> { pub fn id(&self) -> Option<String> {
self.0.as_ref().and_then(|c| c.read().id()) self.0.as_ref().and_then(|c| c.id())
} }
// **< Child RENDER >*************************************************************************** // **< Child RENDER >***************************************************************************
@ -55,7 +65,7 @@ impl Child {
match &self.0 { match &self.0 {
None => html! {}, None => html! {},
Some(m) => { Some(m) => {
let mut component = m.read().clone_box(); let mut component = m.clone_box();
component.render(cx).await component.render(cx).await
} }
} }
@ -66,39 +76,39 @@ impl Child {
// Devuelve el [`UniqueId`] del tipo del componente, si el Child no está vacío. // Devuelve el [`UniqueId`] del tipo del componente, si el Child no está vacío.
#[inline] #[inline]
fn type_id(&self) -> Option<UniqueId> { fn type_id(&self) -> Option<UniqueId> {
self.0.as_ref().map(|c| c.read().type_id()) self.0.as_ref().map(|c| c.type_id())
} }
} }
impl<C: Component + 'static> From<Embed<C>> for Child { impl<C: Component> From<Embed<C>> for Child {
/// Convierte un [`Embed<C>`] en un [`Child`], consumiendo el componente tipado. /// Convierte un [`Embed<C>`] en un [`Child`], consumiendo el componente tipado.
/// ///
/// Útil cuando se tiene un [`Embed`] y se necesita añadirlo a una lista [`Children`]: /// Útil cuando se tiene un [`Embed`] para añadir a una lista [`Children`]:
/// ///
/// ```rust,ignore /// ```rust,no_run
/// children.with_child(Child::from(my_embed)); /// # use pagetop::prelude::*;
/// // o equivalentemente: /// let my_embed = Embed::with(Html::with(|_| html! { "Text" }));
/// children.with_child(my_embed.into()); /// let children = Children::new().with_child(Child::from(my_embed));
///
/// // De forma equivalente se puede usar la conversión implícita hacia `Child`:
/// let my_embed = Embed::with(Html::with(|_| html! { "Text" }));
/// let child: Child = my_embed.into();
/// let children = children.with_child(child);
/// ``` /// ```
fn from(embed: Embed<C>) -> Self { fn from(embed: Embed<C>) -> Self {
match embed.0 { Child(embed.0.map(|arc| arc as Arc<dyn Component>))
None => Child(None),
Some(arc) => Child(Some(Arc::new(RwLock::new(match Arc::try_unwrap(arc) {
Ok(c) => Box::new(c) as Box<dyn Component>,
Err(arc) => arc.clone_box(),
})))),
}
} }
} }
impl<T: Component + 'static> From<T> for Child { impl<T: Component> From<T> for Child {
/// Convierte cualquier componente en un [`Child`], equivalente a [`Child::with()`].
#[inline] #[inline]
fn from(component: T) -> Self { fn from(component: T) -> Self {
Child::with(component) Child::with(component)
} }
} }
impl<T: Component + 'static> From<T> for ChildOp { impl<T: Component> From<T> for ChildOp {
/// Convierte un componente en [`ChildOp::Add`], permitiendo pasar componentes directamente a /// Convierte un componente en [`ChildOp::Add`], permitiendo pasar componentes directamente a
/// métodos como [`Children::with_child`] sin envolverlos explícitamente. /// métodos como [`Children::with_child`] sin envolverlos explícitamente.
#[inline] #[inline]
@ -270,12 +280,13 @@ pub enum ChildOp {
/// Gracias a esto, [`with_child`](Self::with_child) acepta un componente directamente o cualquier /// Gracias a esto, [`with_child`](Self::with_child) acepta un componente directamente o cualquier
/// variante de [`ChildOp`]: /// variante de [`ChildOp`]:
/// ///
/// ```rust,ignore /// ```rust,no_run
/// # use pagetop::prelude::*;
/// // Añadir al final de la lista (implícito): /// // Añadir al final de la lista (implícito):
/// children.with_child(MiComponente::new()); /// let children = Children::new().with_child(Html::new());
/// ///
/// // Operación explícita: /// // Operación explícita:
/// children.with_child(ChildOp::Prepend(MiComponente::new().into())); /// let children = children.with_child(ChildOp::Prepend(Html::new().into()));
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
pub struct Children(Vec<Child>); pub struct Children(Vec<Child>);
@ -370,8 +381,12 @@ impl Children {
// **< Children HELPERS >*********************************************************************** // **< Children HELPERS >***********************************************************************
// Añade más de un componente hijo al final de la lista (en el orden recibido). // Añade más de un componente hijo al final de la lista (en el orden recibido).
//
// Usa `Vec::extend()`, que reserva la capacidad necesaria de una vez a partir del `size_hint()`
// del iterador, en vez de una reasignación incremental por cada `push()`. También lo usa
// `core::theme::regions` para fusionar las fuentes de una región.
#[inline] #[inline]
fn add_many<I>(&mut self, iter: I) -> &mut Self pub(crate) fn add_many<I>(&mut self, iter: I) -> &mut Self
where where
I: IntoIterator<Item = Child>, I: IntoIterator<Item = Child>,
{ {
@ -458,8 +473,9 @@ impl IntoIterator for Children {
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,no_run
/// let children = Children::new().with_child(child1).with_child(child2); /// # use pagetop::prelude::*;
/// let children = Children::new().with_child(Html::new()).with_child(Html::new());
/// for child in children { /// for child in children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
/// } /// }
@ -477,8 +493,9 @@ impl<'a> IntoIterator for &'a Children {
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,no_run
/// let children = Children::new().with_child(child1).with_child(child2); /// # use pagetop::prelude::*;
/// let children = Children::new().with_child(Html::new()).with_child(Html::new());
/// for child in &children { /// for child in &children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
/// } /// }
@ -496,10 +513,12 @@ impl<'a> IntoIterator for &'a mut Children {
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,no_run
/// let mut children = Children::new().with_child(child1).with_child(child2); /// # use pagetop::prelude::*;
/// async fn render_all(mut children: Children, context: &mut Context) {
/// for child in &mut children { /// for child in &mut children {
/// child.render(&mut context).await; /// child.render(context).await;
/// }
/// } /// }
/// ``` /// ```
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {

View file

@ -7,40 +7,53 @@ use parking_lot::RwLock;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
// Permite almacenar un componente como prototipo en regiones globales. // Lista de prototipos de componentes por nombre de región.
// //
// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. En cada // Utiliza Vec en lugar de HashMap. El número de regiones registradas por tema o aplicación es casi
// llamada a [`as_child`](Self::as_child) produce un clon fresco del estado original, de modo que // siempre de un dígito, así que una búsqueda lineal por igualdad de `&str` evita el coste de
// cada página renderiza el componente desde su estado inicial sin acumular mutaciones de peticiones // hashear la clave. Además, el trabajo para recorrer regiones vacías es mínimo.
// anteriores. //
trait ComponentGlobal: Send + Sync { // La clave es `&'static str` (lo que ya devuelve `RegionName::name()`) en lugar de `String`. No
// Devuelve un nuevo [`Child`] con una copia independiente del componente original. // hace falta reservar en el heap una copia de un dato que ya vive de forma estática.
fn as_child(&self) -> Child; #[derive(AutoDefault)]
} struct RegionComponents(Vec<(&'static str, Vec<Arc<dyn Component>>)>);
impl<T: Component + Clone + 'static> ComponentGlobal for T { impl RegionComponents {
#[inline] // Devuelve los prototipos registrados para la región indicada, si hay alguno.
fn as_child(&self) -> Child { fn get(&self, region_name: &str) -> Option<&Vec<Arc<dyn Component>>> {
Child::with(self.clone()) self.0
.iter()
.find(|(name, _)| *name == region_name)
.map(|(_, protos)| protos)
}
// Añade un prototipo a la región indicada, creando la entrada si es la primera.
//
// Se comparte como `Arc<dyn Component>`. El prototipo no se clona aquí ni al ensamblar la
// región (sólo se clona el `Arc`, barato). El único clonado real del componente ocurre en
// `Child::render()`, cuando cada petición necesita su propia copia mutable para pasar por
// `setup()` desde un estado inicial limpio.
fn push(&mut self, region_name: &'static str, proto: Arc<dyn Component>) {
match self.0.iter_mut().find(|(name, _)| *name == region_name) {
Some((_, protos)) => protos.push(proto),
None => self.0.push((region_name, vec![proto])),
}
} }
} }
// Mapa de nombre de región a lista de prototipos de componentes.
type RegionComponents = HashMap<String, Vec<Arc<dyn ComponentGlobal>>>;
// Regiones globales con prototipos asociados a un tema específico. // Regiones globales con prototipos asociados a un tema específico.
static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, RegionComponents>>> = static THEME_REGIONS: LazyLock<RwLock<HashMap<UniqueId, RegionComponents>>> =
LazyLock::new(|| RwLock::new(HashMap::new())); LazyLock::new(|| RwLock::new(HashMap::new()));
// Regiones globales con prototipos comunes a todos los temas. // Regiones globales con prototipos comunes a todos los temas.
static COMMON_REGIONS: LazyLock<RwLock<RegionComponents>> = static COMMON_REGIONS: LazyLock<RwLock<RegionComponents>> =
LazyLock::new(|| RwLock::new(HashMap::new())); LazyLock::new(|| RwLock::new(RegionComponents::default()));
// ************************************************************************************************* // *************************************************************************************************
// Contenedor interno de componentes agrupados por región. // Contenedor interno de componentes agrupados por región.
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub(crate) struct ChildrenInRegions(HashMap<String, Children>); pub(crate) struct ChildrenInRegions(HashMap<&'static str, Children>);
impl ChildrenInRegions { impl ChildrenInRegions {
pub fn with(region: RegionRef, child: Child) -> Self { pub fn with(region: RegionRef, child: Child) -> Self {
@ -55,7 +68,7 @@ impl ChildrenInRegions {
region.alter_child(child); region.alter_child(child);
} else { } else {
let children = Children::new().with_child(child); let children = Children::new().with_child(child);
self.0.insert(region_name.to_owned(), children); self.0.insert(region_name, children);
} }
self self
} }
@ -64,38 +77,39 @@ impl ChildrenInRegions {
/// ///
/// Se recogen desde tres fuentes disponibles, en el siguiente orden: /// Se recogen desde tres fuentes disponibles, en el siguiente orden:
/// ///
/// 1. Prototipos globales comunes, disponibles en cualquier tema. Se clonan en cada petición /// 1. Prototipos globales comunes, disponibles en cualquier tema. Se comparten como `Arc` (sin
/// clonar el componente); `Child::render()` obtiene su propia copia mutable más adelante,
/// para que `setup()` parta siempre de un estado inicial limpio. /// para que `setup()` parta siempre de un estado inicial limpio.
/// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en /// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en
/// lugar de clonarse, ya que son de un único uso. /// lugar de clonarse, ya que son de un único uso.
/// 3. Prototipos del tema activo, exclusivos del tema en curso. También se clonan para asegurar /// 3. Prototipos del tema activo, exclusivos del tema en curso. Se comparten igual que los
/// que llegan a `setup()` con el mismo estado inicial. /// comunes.
pub fn assemble_region(&mut self, theme: ThemeRef, region: RegionRef) -> Children { pub fn assemble_region(&mut self, theme: ThemeRef, region: RegionRef) -> Children {
let region_name = region.name();
let common = COMMON_REGIONS.read();
let themed = THEME_REGIONS.read();
let mut result = Children::new(); let mut result = Children::new();
let region_name = region.name();
// 1. Prototipos globales comunes. // 1. Prototipos globales comunes.
if let Some(protos) = common.get(region_name) { if let Some(global_protos) = COMMON_REGIONS.read().get(region_name) {
for proto in protos { result.add_many(
result.add(proto.as_child()); global_protos
} .iter()
.map(|proto| Child::from_arc(Arc::clone(proto))),
);
} }
// 2. Componentes propios de la página: se mueven, no se clonan. // 2. Componentes propios de la página: se mueven, no se clonan.
if let Some(page_children) = self.0.remove(region_name) { if let Some(page_children) = self.0.remove(region_name) {
for child in page_children { result.add_many(page_children);
result.add(child);
}
} }
// 3. Prototipos del tema activo. // 3. Prototipos del tema activo.
if let Some(theme_map) = themed.get(&theme.type_id()) { if let Some(theme_region) = THEME_REGIONS.read().get(&theme.type_id())
if let Some(protos) = theme_map.get(region_name) { && let Some(theme_protos) = theme_region.get(region_name)
for proto in protos { {
result.add(proto.as_child()); result.add_many(
} theme_protos
} .iter()
.map(|proto| Child::from_arc(Arc::clone(proto))),
);
} }
result result
@ -168,8 +182,8 @@ impl InRegion {
/// html! { "Aviso legal" } /// html! { "Aviso legal" }
/// })); /// }));
/// ``` /// ```
pub fn add(&self, component: impl Component + Clone + 'static) -> &Self { pub fn add(&self, component: impl Component) -> &Self {
let proto: Arc<dyn ComponentGlobal> = Arc::new(component); let proto: Arc<dyn Component> = Arc::new(component);
match self { match self {
InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto), InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto),
InRegion::Global(region) => Self::add_to_common(*region, proto), InRegion::Global(region) => Self::add_to_common(*region, proto),
@ -178,20 +192,14 @@ impl InRegion {
.write() .write()
.entry(theme.type_id()) .entry(theme.type_id())
.or_default() .or_default()
.entry((*region).name().to_owned()) .push((*region).name(), proto);
.or_default()
.push(proto);
} }
} }
self self
} }
#[inline] #[inline]
fn add_to_common(region: RegionRef, proto: Arc<dyn ComponentGlobal>) { fn add_to_common(region: RegionRef, proto: Arc<dyn Component>) {
COMMON_REGIONS COMMON_REGIONS.write().push(region.name(), proto);
.write()
.entry(region.name().to_owned())
.or_default()
.push(proto);
} }
} }

View file

@ -347,6 +347,7 @@ pub mod test {
pub struct TestRequest { pub struct TestRequest {
method: http::Method, method: http::Method,
uri: String, uri: String,
headers: http::HeaderMap,
extensions: http::Extensions, extensions: http::Extensions,
} }
@ -356,6 +357,7 @@ pub mod test {
Self { Self {
method: http::Method::GET, method: http::Method::GET,
uri: "/".to_owned(), uri: "/".to_owned(),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(), extensions: http::Extensions::new(),
} }
} }
@ -365,6 +367,7 @@ pub mod test {
Self { Self {
method: http::Method::POST, method: http::Method::POST,
uri: "/".to_owned(), uri: "/".to_owned(),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(), extensions: http::Extensions::new(),
} }
} }
@ -375,6 +378,20 @@ pub mod test {
self self
} }
/// Añade una cabecera a la petición.
///
/// Si `name` o `value` no son válidos como cabecera HTTP, se descarta en silencio en lugar
/// de entrar en pánico.
pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
if let (Ok(n), Ok(v)) = (
http::HeaderName::from_bytes(name.as_ref().as_bytes()),
http::HeaderValue::from_str(value.as_ref()),
) {
self.headers.insert(n, v);
}
self
}
/// Inserta un valor en las extensiones de la petición. /// 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
@ -391,6 +408,7 @@ pub mod test {
.uri(self.uri) .uri(self.uri)
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
*req.headers_mut() = self.headers;
*req.extensions_mut() = self.extensions; *req.extensions_mut() = self.extensions;
req req
} }
@ -401,7 +419,7 @@ pub mod test {
let uri = self.uri.parse().unwrap(); let uri = self.uri.parse().unwrap();
super::HttpRequest { super::HttpRequest {
uri, uri,
headers: axum::http::HeaderMap::new(), headers: self.headers,
extensions: std::sync::Arc::new(self.extensions), extensions: std::sync::Arc::new(self.extensions),
} }
} }