🎨 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

@ -51,6 +51,7 @@ use pagetop::prelude::*;
struct HelloWorld; struct HelloWorld;
#[async_trait]
impl Extension for HelloWorld { impl Extension for HelloWorld {
fn configure_router(&self, router: Router) -> Router { fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(hello_world)) router.route("/", web::get(hello_world))
@ -60,7 +61,7 @@ impl Extension for HelloWorld {
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(|_| html! { h1 { "Hello World!" } })) .with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render() .render().await
} }
#[pagetop::main] #[pagetop::main]
@ -72,7 +73,6 @@ async fn main() -> std::io::Result<()> {
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
(`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`. (`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`.
## Proyecto ## Proyecto
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos: El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
@ -111,7 +111,6 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**, * **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales. integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
## Pruebas ## Pruebas
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
@ -132,14 +131,12 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente > * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
> `cargo test`. > `cargo test`.
## Advertencia ## Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**. hasta que se libere la versión **1.0.0**.
## Licencia ## Licencia
El código está disponible bajo una doble licencia: El código está disponible bajo una doble licencia:
@ -153,7 +150,6 @@ El código está disponible bajo una doble licencia:
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
el ecosistema Rust. el ecosistema Rust.
## Contribuir ## Contribuir
PageTop mantiene **un único repositorio oficial**: PageTop mantiene **un único repositorio oficial**:

View file

@ -5,6 +5,7 @@ include_locales!(LOC from "examples/locale");
struct FormControls; struct FormControls;
#[async_trait]
impl Extension for FormControls { impl Extension for FormControls {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
@ -297,6 +298,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
), ),
) )
.render() .render()
.await
} }
fn form_lists() -> Form { fn form_lists() -> Form {

View file

@ -2,6 +2,7 @@ use pagetop::prelude::*;
struct HelloName; struct HelloName;
#[async_trait]
impl Extension for HelloName { impl Extension for HelloName {
fn configure_router(&self, router: Router) -> Router { fn configure_router(&self, router: Router) -> Router {
router.route("/hello/{name}", web::get(hello_name)) router.route("/hello/{name}", web::get(hello_name))
@ -19,6 +20,7 @@ async fn hello_name(
} }
})) }))
.render() .render()
.await
} }
#[pagetop::main] #[pagetop::main]

View file

@ -2,6 +2,7 @@ use pagetop::prelude::*;
struct HelloWorld; struct HelloWorld;
#[async_trait]
impl Extension for HelloWorld { impl Extension for HelloWorld {
fn configure_router(&self, router: Router) -> Router { fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(hello_world)) router.route("/", web::get(hello_world))
@ -16,6 +17,7 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
} }
})) }))
.render() .render()
.await
} }
#[pagetop::main] #[pagetop::main]

View file

@ -4,6 +4,7 @@ include_locales!(LOC from "examples/locale");
struct IntroColors; struct IntroColors;
#[async_trait]
impl Extension for IntroColors { impl Extension for IntroColors {
fn configure_router(&self, router: Router) -> Router { fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(intro_colors)) router.route("/", web::get(intro_colors))
@ -74,6 +75,7 @@ async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
), ),
) )
.render() .render()
.await
} }
#[pagetop::main] #[pagetop::main]

View file

@ -34,6 +34,7 @@ use pagetop::prelude::*;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -68,7 +69,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
p { (L10n::l("sample_content").using(cx)) } p { (L10n::l("sample_content").using(cx)) }
})), })),
) )
.render() .render().await
} }
``` ```

View file

@ -35,6 +35,7 @@ use pagetop::prelude::*;
struct MyApp; struct MyApp;
#[async_trait]
impl Extension for MyApp { impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -69,7 +70,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
p { (L10n::l("sample_content").using(cx)) } p { (L10n::l("sample_content").using(cx)) }
})), })),
) )
.render() .render().await
} }
``` ```
*/ */
@ -89,6 +90,7 @@ include_locales!(LOCALES_ALINER);
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias. /// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
pub struct Aliner; pub struct Aliner;
#[async_trait]
impl Extension for Aliner { impl Extension for Aliner {
fn name(&self) -> L10n { fn name(&self) -> L10n {
L10n::t("extension_name", &LOCALES_ALINER) L10n::t("extension_name", &LOCALES_ALINER)
@ -108,6 +110,7 @@ impl Extension for Aliner {
} }
} }
#[async_trait]
impl Theme for Aliner { impl Theme for Aliner {
fn before_render_page_body(&self, page: &mut Page) { fn before_render_page_body(&self, page: &mut Page) {
page.alter_assets(AssetsOp::AddStyleSheet( page.alter_assets(AssetsOp::AddStyleSheet(

View file

@ -96,6 +96,7 @@ use pagetop::prelude::*;
pub struct MyExtension; pub struct MyExtension;
#[async_trait]
impl Extension for MyExtension { impl Extension for MyExtension {
fn configure_router(&self, mut router: Router) -> Router { fn configure_router(&self, mut router: Router) -> Router {
serve_static_files!(router, ["./static/css", app_css] => "/public/css"); serve_static_files!(router, ["./static/css", app_css] => "/public/css");

View file

@ -97,6 +97,7 @@ use pagetop::prelude::*;
pub struct MyExtension; pub struct MyExtension;
#[async_trait]
impl Extension for MyExtension { impl Extension for MyExtension {
fn configure_router(&self, mut router: Router) -> Router { fn configure_router(&self, mut router: Router) -> Router {
serve_static_files!(router, ["./static/css", app_css] => "/public/css"); serve_static_files!(router, ["./static/css", app_css] => "/public/css");

View file

@ -5,7 +5,7 @@ mod figfont;
use crate::core::{extension, extension::ExtensionRef}; use crate::core::{extension, extension::ExtensionRef};
use crate::html::Markup; use crate::html::Markup;
use crate::locale::Locale; use crate::locale::Locale;
use crate::response::page::ErrorPage; use crate::response::page::{ErrorPage, render_error_pages};
use crate::web::{HttpRequest, Router}; use crate::web::{HttpRequest, Router};
use crate::{PAGETOP_VERSION, global, trace}; use crate::{PAGETOP_VERSION, global, trace};
@ -109,7 +109,9 @@ impl Application {
fn build_router() -> Router { fn build_router() -> Router {
let router = extension::all::configure_routes(Router::new()); let router = extension::all::configure_routes(Router::new());
let router = extension::all::configure_middleware(router); let router = extension::all::configure_middleware(router);
router.fallback(route_not_found) router
.fallback(route_not_found)
.layer(axum::middleware::from_fn(render_error_pages))
} }
/// Arranca el servidor web de la aplicación. /// Arranca el servidor web de la aplicación.
@ -123,6 +125,7 @@ impl Application {
/// ///
/// struct MyApp; /// struct MyApp;
/// ///
/// #[async_trait]
/// impl Extension for MyApp {} /// impl Extension for MyApp {}
/// ///
/// #[pagetop::main] /// #[pagetop::main]

View file

@ -94,6 +94,7 @@ pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool);
/// ///
/// pub struct MyAuth; /// pub struct MyAuth;
/// ///
/// #[async_trait]
/// impl Extension for MyAuth { /// impl Extension for MyAuth {
/// fn actions(&self) -> Vec<ActionBox> { /// fn actions(&self) -> Vec<ActionBox> {
/// actions![CheckPermission::new(check_my_permissions)] /// actions![CheckPermission::new(check_my_permissions)]
@ -162,7 +163,7 @@ impl CheckPermission {
/// if !has_permission(page.context(), "myapp.edit") { /// if !has_permission(page.context(), "myapp.edit") {
/// return Err(ErrorPage::NotFound(request)); /// return Err(ErrorPage::NotFound(request));
/// } /// }
/// page.render() /// page.render().await
/// } /// }
/// ``` /// ```
pub fn has_permission(cx: &Context, key: &str) -> bool { pub fn has_permission(cx: &Context, key: &str) -> bool {

View file

@ -14,6 +14,7 @@ pub struct Block {
children: Children, children: Children,
} }
#[async_trait]
impl Component for Block { impl Component for Block {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -31,8 +32,8 @@ impl Component for Block {
self.alter_prop(PropsOp::prepend_classes("block")); self.alter_prop(PropsOp::prepend_classes("block"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let block_body = self.children().render(cx); let block_body = self.children().render(cx).await;
if block_body.is_empty() { if block_body.is_empty() {
return Ok(html! {}); return Ok(html! {});

View file

@ -81,6 +81,7 @@ pub struct Button {
disabled: bool, disabled: bool,
} }
#[async_trait]
impl Component for Button { impl Component for Button {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -94,7 +95,7 @@ impl Component for Button {
self.alter_prop(PropsOp::prepend_classes("button")); self.alter_prop(PropsOp::prepend_classes("button"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! { Ok(html! {
button button
type=(self.kind()) type=(self.kind())

View file

@ -121,6 +121,7 @@ pub struct Field {
inline: bool, inline: bool,
} }
#[async_trait]
impl Component for Field { impl Component for Field {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -145,7 +146,7 @@ impl Component for Field {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap(); let name = self.name().get().unwrap();
let container_id = self.id().unwrap(); let container_id = self.id().unwrap();

View file

@ -61,6 +61,7 @@ pub struct Checkbox {
reverse: bool, reverse: bool,
} }
#[async_trait]
impl Component for Checkbox { impl Component for Checkbox {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -95,7 +96,7 @@ impl Component for Checkbox {
self.alter_prop(PropsOp::prepend_classes(classes)); self.alter_prop(PropsOp::prepend_classes(classes));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap(); let name = self.name().get().unwrap();
let container_id = self.id().unwrap(); let container_id = self.id().unwrap();

View file

@ -57,6 +57,7 @@ pub struct Form {
children: Children, children: Children,
} }
#[async_trait]
impl Component for Form { impl Component for Form {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -70,7 +71,7 @@ impl Component for Form {
self.alter_prop(PropsOp::prepend_classes("form")); self.alter_prop(PropsOp::prepend_classes("form"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let method = match self.method() { let method = match self.method() {
form::Method::Post => Some("post"), form::Method::Post => Some("post"),
form::Method::Get => None, form::Method::Get => None,
@ -82,7 +83,7 @@ impl Component for Form {
method=[method] method=[method]
accept-charset=[self.charset().get()] accept-charset=[self.charset().get()]
{ {
(self.children().render(cx)) (self.children().render(cx).await)
} }
}) })
} }

View file

@ -36,6 +36,7 @@ pub struct Fieldset {
children: Children, children: Children,
} }
#[async_trait]
impl Component for Fieldset { impl Component for Fieldset {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -45,8 +46,8 @@ impl Component for Fieldset {
self.props.get_id() self.props.get_id()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let children = self.children().render(cx); let children = self.children().render(cx).await;
if children.is_empty() { if children.is_empty() {
return Ok(html! {}); return Ok(html! {});

View file

@ -35,12 +35,13 @@ pub struct Hidden {
value: AttrValue, value: AttrValue,
} }
#[async_trait]
impl Component for Hidden { impl Component for Hidden {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
} }
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! { Ok(html! {
input input
type="hidden" type="hidden"

View file

@ -157,6 +157,7 @@ pub struct Field {
inputmode: Attr<Mode>, inputmode: Attr<Mode>,
} }
#[async_trait]
impl Component for Field { impl Component for Field {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -181,7 +182,7 @@ impl Component for Field {
))); )));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self.id();
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
let input_class = if *self.plaintext() { let input_class = if *self.plaintext() {

View file

@ -112,6 +112,7 @@ pub struct Field {
inline: bool, inline: bool,
} }
#[async_trait]
impl Component for Field { impl Component for Field {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -136,7 +137,7 @@ impl Component for Field {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado.
let name = self.name().get().unwrap(); let name = self.name().get().unwrap();
let container_id = self.id().unwrap(); let container_id = self.id().unwrap();

View file

@ -53,6 +53,7 @@ pub struct Range {
disabled: bool, disabled: bool,
} }
#[async_trait]
impl Component for Range { impl Component for Range {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -74,7 +75,7 @@ impl Component for Range {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-range")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-range"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self.id();
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range")); let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
Ok(html! { Ok(html! {

View file

@ -212,6 +212,7 @@ pub struct Field {
disabled: bool, disabled: bool,
} }
#[async_trait]
impl Component for Field { impl Component for Field {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -233,7 +234,7 @@ impl Component for Field {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-select")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-select"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self.id();
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));

View file

@ -63,6 +63,7 @@ pub struct Textarea {
disabled: bool, disabled: bool,
} }
#[async_trait]
impl Component for Textarea { impl Component for Textarea {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -84,7 +85,7 @@ impl Component for Textarea {
self.alter_prop(PropsOp::prepend_classes("form-field form-field-textarea")); self.alter_prop(PropsOp::prepend_classes("form-field form-field-textarea"));
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let container_id = self.id(); let container_id = self.id();
let textarea_id = container_id let textarea_id = container_id
.as_deref() .as_deref()

View file

@ -49,12 +49,13 @@ impl Default for Html {
} }
} }
#[async_trait]
impl Component for Html { impl Component for Html {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(self.html(cx)) Ok(self.html(cx))
} }
} }

View file

@ -107,12 +107,13 @@ impl Default for Intro {
} }
} }
#[async_trait]
impl Component for Intro { impl Component for Intro {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
cx.alter_assets(AssetsOp::AddStyleSheet( cx.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION), StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION),
)); ));
@ -146,7 +147,7 @@ impl Component for Intro {
} }
aside class="intro-header-img" aria-hidden="true" { aside class="intro-header-img" aria-hidden="true" {
div class="intro-header-mascot" { div class="intro-header-mascot" {
(PageTopSvg::Color.render(cx)) (PageTopSvg::Color.markup(cx))
} }
} }
} }
@ -191,7 +192,7 @@ impl Component for Intro {
(L10n::l("intro_text2").using(cx)) (L10n::l("intro_text2").using(cx))
} }
} }
(self.children().render(cx)) (self.children().render(cx).await)
} }
} }
} }
@ -199,7 +200,7 @@ impl Component for Intro {
div class="intro-footer" { div class="intro-footer" {
section class="intro-footer-body" { section class="intro-footer-body" {
div class="intro-footer-logo" { div class="intro-footer-logo" {
(PageTopSvg::LineLight.render(cx)) (PageTopSvg::LineLight.markup(cx))
} }
div class="intro-footer-links" { div class="intro-footer-links" {
a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") } a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") }

View file

@ -14,6 +14,7 @@ pub struct PoweredBy {
copyright: Option<String>, copyright: Option<String>,
} }
#[async_trait]
impl Component for PoweredBy { impl Component for PoweredBy {
/// Crea una nueva instancia de `PoweredBy`. /// Crea una nueva instancia de `PoweredBy`.
/// ///
@ -25,7 +26,7 @@ impl Component for PoweredBy {
PoweredBy { copyright: Some(c) } PoweredBy { copyright: Some(c) }
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! { Ok(html! {
div id=[self.id()] class="poweredby" { div id=[self.id()] class="poweredby" {
@if let Some(c) = self.copyright() { @if let Some(c) = self.copyright() {

View file

@ -11,6 +11,7 @@ use crate::prelude::*;
/// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente. /// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente.
pub struct Welcome; pub struct Welcome;
#[async_trait]
impl Extension for Welcome { impl Extension for Welcome {
fn name(&self) -> L10n { fn name(&self) -> L10n {
L10n::l("welcome_extension_name") L10n::l("welcome_extension_name")
@ -62,4 +63,5 @@ async fn home(request: HttpRequest) -> Result<Markup, ErrorPage> {
), ),
) )
.render() .render()
.await
} }

View file

@ -4,12 +4,14 @@ use crate::prelude::*;
/// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`]. /// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`].
pub struct Basic; pub struct Basic;
#[async_trait]
impl Extension for Basic { impl Extension for Basic {
fn theme(&self) -> Option<ThemeRef> { fn theme(&self) -> Option<ThemeRef> {
Some(&Self) Some(&Self)
} }
} }
#[async_trait]
impl Theme for Basic { impl Theme for Basic {
fn before_render_page_body(&self, page: &mut Page) { fn before_render_page_body(&self, page: &mut Page) {
page.alter_assets(AssetsOp::AddStyleSheet( page.alter_assets(AssetsOp::AddStyleSheet(

View file

@ -26,6 +26,7 @@ pub use all::{dispatch_actions, try_dispatch_actions};
/// Extensión que ajusta un botón antes de renderizarlo y transforma su HTML final: /// Extensión que ajusta un botón antes de renderizarlo y transforma su HTML final:
/// ///
/// ```rust,ignore /// ```rust,ignore
/// #[async_trait]
/// impl Extension for MyExtension { /// impl Extension for MyExtension {
/// fn actions(&self) -> Vec<ActionBox> { /// fn actions(&self) -> Vec<ActionBox> {
/// actions![ /// actions![

View file

@ -10,7 +10,6 @@ pub use definition::{Component, ComponentClone, ComponentRender};
mod children; mod children;
pub use children::Children; pub use children::Children;
pub use children::ComponentGuard;
pub use children::{Child, ChildOp, Embed}; pub use children::{Child, ChildOp, Embed};
mod message; mod message;
@ -34,6 +33,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
/// renderable: Option<FnIsRenderable>, /// renderable: Option<FnIsRenderable>,
/// } /// }
/// ///
/// #[async_trait]
/// impl Component for SampleComponent { /// impl Component for SampleComponent {
/// fn new() -> Self { /// fn new() -> Self {
/// Self::default() /// Self::default()
@ -44,7 +44,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual};
/// self.renderable.map_or(true, |f| f(cx)) /// self.renderable.map_or(true, |f| f(cx))
/// } /// }
/// ///
/// fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> { /// async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
/// Ok(html! { "Visible component" }) /// Ok(html! { "Visible component" })
/// } /// }
/// } /// }

View file

@ -2,28 +2,23 @@ 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::Mutex; use parking_lot::RwLock;
pub use parking_lot::MutexGuard as ComponentGuard;
use std::fmt; use std::fmt;
use std::sync::Arc;
use std::vec::IntoIter; use std::vec::IntoIter;
/// Representa un componente hijo encapsulado para su uso en una lista [`Children`]. // **< Child >**************************************************************************************
#[derive(AutoDefault)]
pub struct Child(Option<Mutex<Box<dyn Component>>>);
impl Clone for Child { /// Representa un componente hijo encapsulado para su uso en una lista [`Children`].
fn clone(&self) -> Self { #[derive(AutoDefault, Clone)]
Child(self.0.as_ref().map(|m| Mutex::new(m.lock().clone_box()))) pub struct Child(Option<Arc<RwLock<Box<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.lock().name()), Some(c) => write!(f, "Child({})", c.read().name()),
} }
} }
} }
@ -31,7 +26,7 @@ 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(Mutex::new(Box::new(component)))) Child(Some(Arc::new(RwLock::new(Box::new(component)))))
} }
// **< Child BUILDER >************************************************************************** // **< Child BUILDER >**************************************************************************
@ -41,7 +36,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| Mutex::new(Box::new(c) as Box<dyn Component>)); self.0 = component.map(|c| Arc::new(RwLock::new(Box::new(c) as Box<dyn Component>)));
self self
} }
@ -50,22 +45,28 @@ 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.lock().id()) self.0.as_ref().and_then(|c| c.read().id())
} }
// **< Child RENDER >*************************************************************************** // **< Child RENDER >***************************************************************************
/// Renderiza el componente con el contexto proporcionado. /// Renderiza el componente con el contexto proporcionado.
pub fn render(&self, cx: &mut Context) -> Markup { pub async fn render(&self, cx: &mut Context) -> Markup {
self.0.as_ref().map_or(html! {}, |c| c.lock().render(cx)) match &self.0 {
None => html! {},
Some(m) => {
let mut component = m.read().clone_box();
component.render(cx).await
}
}
} }
// **< Child HELPERS >************************************************************************** // **< Child HELPERS >**************************************************************************
/// Devuelve el [`UniqueId`] del tipo del componente, si existe. // 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.lock().type_id()) self.0.as_ref().map(|c| c.read().type_id())
} }
} }
@ -80,12 +81,12 @@ impl<C: Component + 'static> From<Embed<C>> for Child {
/// children.with_child(my_embed.into()); /// children.with_child(my_embed.into());
/// ``` /// ```
fn from(embed: Embed<C>) -> Self { fn from(embed: Embed<C>) -> Self {
if let Some(m) = embed.0 { match embed.0 {
Child(Some(Mutex::new( None => Child(None),
Box::new(m.into_inner()) as Box<dyn Component> Some(arc) => Child(Some(Arc::new(RwLock::new(match Arc::try_unwrap(arc) {
))) Ok(c) => Box::new(c) as Box<dyn Component>,
} else { Err(arc) => arc.clone_box(),
Child(None) })))),
} }
} }
} }
@ -114,7 +115,7 @@ impl From<Child> for ChildOp {
} }
} }
// ************************************************************************************************* // **< Embed >**************************************************************************************
/// Contenedor tipado para un *único* componente de un tipo concreto conocido. /// Contenedor tipado para un *único* componente de un tipo concreto conocido.
/// ///
@ -125,11 +126,12 @@ impl From<Child> for ChildOp {
/// Se usa habitualmente para incrustar un componente dentro de otro cuando no se necesita una lista /// Se usa habitualmente para incrustar un componente dentro de otro cuando no se necesita una lista
/// completa de hijos ([`Children`]), sino un único componente tipado en un campo concreto. /// completa de hijos ([`Children`]), sino un único componente tipado en un campo concreto.
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Embed<C: Component>(Option<Mutex<C>>); pub struct Embed<C: Component>(Option<Arc<C>>);
impl<C: Component + Clone> Clone for Embed<C> { // Arc<C>: Clone no requiere C: Clone, pero #[derive(Clone)] añadiría ese bound innecesariamente.
impl<C: Component> Clone for Embed<C> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Embed(self.0.as_ref().map(|m| Mutex::new(m.lock().clone()))) Embed(self.0.clone())
} }
} }
@ -137,7 +139,7 @@ impl<C: Component> fmt::Debug for Embed<C> {
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, "Embed(None)"), None => write!(f, "Embed(None)"),
Some(c) => write!(f, "Embed({})", c.lock().name()), Some(c) => write!(f, "Embed({})", c.name()),
} }
} }
} }
@ -145,7 +147,7 @@ impl<C: Component> fmt::Debug for Embed<C> {
impl<C: Component> Embed<C> { impl<C: Component> Embed<C> {
/// Crea un nuevo `Embed` a partir de un componente. /// Crea un nuevo `Embed` a partir de un componente.
pub fn with(component: C) -> Self { pub fn with(component: C) -> Self {
Embed(Some(Mutex::new(component))) Embed(Some(Arc::new(component)))
} }
// **< Embed BUILDER >************************************************************************** // **< Embed BUILDER >**************************************************************************
@ -155,7 +157,7 @@ impl<C: Component> Embed<C> {
/// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia. /// Si se proporciona `Some(component)`, se encapsula como [`Embed`]; y si es `None`, se limpia.
#[builder_fn] #[builder_fn]
pub fn with_component(mut self, component: Option<C>) -> Self { pub fn with_component(mut self, component: Option<C>) -> Self {
self.0 = component.map(Mutex::new); self.0 = component.map(Arc::new);
self self
} }
@ -164,55 +166,69 @@ impl<C: Component> Embed<C> {
/// 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.lock().id()) self.0.as_deref().and_then(|c| c.id())
} }
/// Devuelve un acceso al componente incrustado. /// Devuelve una referencia inmutable al componente incrustado, si existe.
/// ///
/// - Devuelve `Some(ComponentGuard<C>)` si existe el componente, o `None` si está vacío. /// Para acceso mutable, usa [`get_mut`](Embed::get_mut).
/// - El acceso es **exclusivo**: mientras el *guard* esté activo, no habrá otros accesos.
/// - Se recomienda mantener el *guard* **el menor tiempo posible** para evitar bloqueos
/// innecesarios.
/// - Para modificar el componente, declara el *guard* como `mut`:
/// `if let Some(mut c) = embed.get() { c.alter_title(...); }`.
/// ///
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,no_run /// ```rust,no_run
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let embed = Embed::with(Html::with(|_| html! { "Prueba" })); /// let embed = Embed::with(Html::with(|_| html! { "Prueba" }));
/// {
/// if let Some(component) = embed.get() { /// if let Some(component) = embed.get() {
/// assert_eq!(component.name(), "Html"); /// assert_eq!(component.name(), "Html");
/// } /// }
/// }; // El *guard* se libera aquí, antes del *drop* de `embed`. /// ```
pub fn get(&self) -> Option<&C> {
self.0.as_deref()
}
/// Devuelve una referencia mutable al componente incrustado, si existe.
/// ///
/// let embed = Embed::with(Block::new().with_title(L10n::n("Title"))); /// Si el [`Arc`] interno es compartido (por ejemplo, justo después de clonar el componente
/// { /// padre), aplica *copy-on-write*: clona `C` antes de devolver la referencia mutable. El
/// if let Some(mut component) = embed.get() { /// prototipo almacenado queda intacto.
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// let mut embed = Embed::with(Block::new().with_title(L10n::n("Title")));
/// if let Some(component) = embed.get_mut() {
/// component.alter_title(L10n::n("New Title")); /// component.alter_title(L10n::n("New Title"));
/// } /// }
/// }; // El *guard* se libera aquí, antes del *drop* de `embed`.
/// ``` /// ```
pub fn get(&self) -> Option<ComponentGuard<'_, C>> { pub fn get_mut(&mut self) -> Option<&mut C>
self.0.as_ref().map(|m| m.lock()) where
C: Clone,
{
self.0.as_mut().map(Arc::make_mut)
} }
// **< Embed RENDER >*************************************************************************** // **< Embed RENDER >***************************************************************************
/// Renderiza el componente con el contexto proporcionado. /// Renderiza el componente con el contexto proporcionado.
pub fn render(&self, cx: &mut Context) -> Markup { pub async fn render(&self, cx: &mut Context) -> Markup {
self.0.as_ref().map_or(html! {}, |c| c.lock().render(cx)) match &self.0 {
None => html! {},
Some(m) => {
let mut component = m.clone_box();
component.render(cx).await
}
}
} }
} }
// ************************************************************************************************* // **< Children >***********************************************************************************
/// Operaciones para componentes hijo [`Child`] en una lista [`Children`]. /// Operaciones para componentes hijo [`Child`] en una lista [`Children`].
pub enum ChildOp { pub enum ChildOp {
/// Añade un hijo al final de la lista. /// Añade un hijo al final de la lista.
Add(Child), Add(Child),
/// Añade un hijo solo si la lista está vacía. /// Añade un hijo sólo si la lista está vacía.
AddIfEmpty(Child), AddIfEmpty(Child),
/// Añade varios hijos al final de la lista, en el orden recibido. /// Añade varios hijos al final de la lista, en el orden recibido.
AddMany(Vec<Child>), AddMany(Vec<Child>),
@ -243,13 +259,10 @@ pub enum ChildOp {
/// - [`Child`]: representa un componente hijo encapsulado dentro de la lista. Almacena cualquier /// - [`Child`]: representa un componente hijo encapsulado dentro de la lista. Almacena cualquier
/// componente sin necesidad de conocer su tipo concreto. /// componente sin necesidad de conocer su tipo concreto.
/// - [`Embed<C>`]: contenedor tipado para un *único* componente de tipo `C`. Preferible a /// - [`Embed<C>`]: contenedor tipado para un *único* componente de tipo `C`. Preferible a
/// `Children` cuando el padre solo necesita un componente y quiere acceso directo a los métodos /// `Children` cuando el padre sólo necesita un componente y quiere acceso directo a los métodos
/// de `C`. /// de `C`.
/// - [`ChildOp`]: operaciones disponibles sobre la lista. Cuando se necesita algo más que añadir al /// - [`ChildOp`]: operaciones disponibles sobre la lista. Cuando se necesita algo más que añadir al
/// final, se construye la variante adecuada y se pasa a [`with_child`](Self::with_child). /// final, se construye la variante adecuada y se pasa a [`with_child`](Self::with_child).
/// - [`ComponentGuard`]: devuelto por [`Embed::get`] para garantizar acceso exclusivo al componente
/// tipado. Mientras está activo bloquea cualquier otro acceso por lo que conviene liberarlo
/// cuanto antes.
/// ///
/// # Conversiones implícitas /// # Conversiones implícitas
/// ///
@ -297,14 +310,14 @@ impl Children {
} }
} }
/// Añade un componente hijo al final de la lista. // Añade un componente hijo al final de la lista.
#[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);
self self
} }
/// Añade un componente hijo en la lista sólo si está vacía. // Añade un componente hijo en la lista sólo si está vacía.
#[inline] #[inline]
pub(crate) fn add_if_empty(&mut self, child: Child) -> &mut Self { pub(crate) fn add_if_empty(&mut self, child: Child) -> &mut Self {
if self.0.is_empty() { if self.0.is_empty() {
@ -345,17 +358,17 @@ impl Children {
// **< Children RENDER >************************************************************************ // **< Children RENDER >************************************************************************
/// Renderiza todos los componentes hijo, en orden. /// Renderiza todos los componentes hijo, en orden.
pub fn render(&self, cx: &mut Context) -> Markup { pub async fn render(&self, cx: &mut Context) -> Markup {
html! { html! {
@for c in &self.0 { @for c in &self.0 {
(c.render(cx)) (c.render(cx).await)
} }
} }
} }
// **< 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).
#[inline] #[inline]
fn add_many<I>(&mut self, iter: I) -> &mut Self fn add_many<I>(&mut self, iter: I) -> &mut Self
where where
@ -365,7 +378,7 @@ impl Children {
self self
} }
/// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra. // Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
#[inline] #[inline]
fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self { fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
let id = Some(id.as_ref()); let id = Some(id.as_ref());
@ -376,7 +389,7 @@ impl Children {
self self
} }
/// Inserta un hijo antes del componente con el `id` dado, o al principio si no se encuentra. // Inserta un hijo antes del componente con el `id` dado, o al principio si no se encuentra.
#[inline] #[inline]
fn insert_before_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self { fn insert_before_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
let id = Some(id.as_ref()); let id = Some(id.as_ref());
@ -387,14 +400,14 @@ impl Children {
self self
} }
/// Inserta un hijo al principio de la lista. // Inserta un hijo al principio de la lista.
#[inline] #[inline]
fn prepend(&mut self, child: Child) -> &mut Self { fn prepend(&mut self, child: Child) -> &mut Self {
self.0.insert(0, child); self.0.insert(0, child);
self self
} }
/// Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido). // Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido).
#[inline] #[inline]
fn prepend_many<I>(&mut self, iter: I) -> &mut Self fn prepend_many<I>(&mut self, iter: I) -> &mut Self
where where
@ -405,7 +418,7 @@ impl Children {
self self
} }
/// Elimina el primer hijo con el `id` dado. // Elimina el primer hijo con el `id` dado.
#[inline] #[inline]
fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self { fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self {
let id = Some(id.as_ref()); let id = Some(id.as_ref());
@ -415,7 +428,7 @@ impl Children {
self self
} }
/// Sustituye el primer hijo con el `id` dado por otro componente. // Sustituye el primer hijo con el `id` dado por otro componente.
#[inline] #[inline]
fn replace_by_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self { fn replace_by_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
let id = Some(id.as_ref()); let id = Some(id.as_ref());
@ -428,7 +441,7 @@ impl Children {
self self
} }
/// Elimina todos los componentes hijo de la lista. // Elimina todos los componentes hijo de la lista.
#[inline] #[inline]
fn reset(&mut self) -> &mut Self { fn reset(&mut self) -> &mut Self {
self.0.clear(); self.0.clear();
@ -445,7 +458,7 @@ impl IntoIterator for Children {
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,ignore
/// let children = Children::new().with(child1).with(child2); /// let children = Children::new().with_child(child1).with_child(child2);
/// for child in children { /// for child in children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
/// } /// }
@ -464,7 +477,7 @@ impl<'a> IntoIterator for &'a Children {
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,ignore
/// let children = Children::new().with(child1).with(child2); /// let children = Children::new().with_child(child1).with_child(child2);
/// for child in &children { /// for child in &children {
/// println!("{:?}", child.id()); /// println!("{:?}", child.id());
/// } /// }
@ -483,9 +496,9 @@ impl<'a> IntoIterator for &'a mut Children {
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,ignore /// ```rust,ignore
/// let mut children = Children::new().with(child1).with(child2); /// let mut children = Children::new().with_child(child1).with_child(child2);
/// for child in &mut children { /// for child in &mut children {
/// child.render(&mut context); /// child.render(&mut context).await;
/// } /// }
/// ``` /// ```
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {

View file

@ -10,8 +10,9 @@ use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
use crate::web::HttpRequest; use crate::web::HttpRequest;
use crate::{CowStr, builder_fn, util}; use crate::{CowStr, builder_fn, util};
use parking_lot::Mutex;
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -132,7 +133,7 @@ pub trait Contextual: LangId {
/// .with_param("flags", vec!["a", "b"]); /// .with_param("flags", vec!["a", "b"]);
/// ``` /// ```
#[builder_fn] #[builder_fn]
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self; fn with_param<T: Send + Sync + 'static>(self, key: &'static str, value: T) -> Self;
/// Define los recursos del contexto usando [`AssetsOp`]. /// Define los recursos del contexto usando [`AssetsOp`].
#[builder_fn] #[builder_fn]
@ -172,7 +173,7 @@ pub trait Contextual: LangId {
/// if page.current_user().is_authenticated() { /// if page.current_user().is_authenticated() {
/// // Personalizar la página para el usuario autenticado. /// // Personalizar la página para el usuario autenticado.
/// } /// }
/// page.render() /// page.render().await
/// } /// }
/// ``` /// ```
fn current_user(&self) -> &CurrentUser; fn current_user(&self) -> &CurrentUser;
@ -313,10 +314,10 @@ pub struct Context {
favicon : Option<Favicon>, // Favicon, si se ha definido. favicon : Option<Favicon>, // Favicon, si se ha definido.
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS. stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
javascripts : Assets<JavaScript>, // Scripts JavaScript. javascripts : Assets<JavaScript>, // Scripts JavaScript.
body_props : Props, // Identificador, clases CSS y atributos del <body>. body_props : Props, // Id, clases CSS y atributos del <body>.
regions : ChildrenInRegions, // Regiones de componentes para renderizar. regions : ChildrenInRegions, // Regiones de componentes para renderizar.
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución. params : HashMap<&'static str, (Box<dyn Any + Send + Sync>, &'static str)>, // Parámetros.
id_counters : RefCell<HashMap<TypeId, usize>>, // RefCell permite mutar desde build_id(&self). id_counters : Mutex<HashMap<TypeId, usize>>, // Mutex permite mutar desde build_id(&self).
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados. messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
} }
@ -347,7 +348,7 @@ impl Context {
body_props : Props::default(), body_props : Props::default(),
regions : ChildrenInRegions::default(), regions : ChildrenInRegions::default(),
params : HashMap::default(), params : HashMap::default(),
id_counters: RefCell::new(HashMap::new()), id_counters: Mutex::new(HashMap::new()),
messages : Vec::new(), messages : Vec::new(),
} }
} }
@ -390,10 +391,11 @@ impl Context {
} }
/// Renderiza los componentes de una región. /// Renderiza los componentes de una región.
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup { pub async fn render_region_named(&mut self, region_name: &str) -> Markup {
self.regions self.regions
.children_for(self.theme, region_ref) .assemble_region(self.theme, region_name)
.render(self) .render(self)
.await
} }
// **< Context HELPERS >************************************************************************ // **< Context HELPERS >************************************************************************
@ -436,7 +438,7 @@ impl Context {
segments segments
}; };
let count = { let count = {
let mut map = self.id_counters.borrow_mut(); let mut map = self.id_counters.lock();
let n = map.entry(TypeId::of::<C>()).or_insert(0); let n = map.entry(TypeId::of::<C>()).or_insert(0);
*n += 1; *n += 1;
*n *n
@ -529,7 +531,7 @@ impl Contextual for Context {
} }
#[builder_fn] #[builder_fn]
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self { fn with_param<T: Send + Sync + 'static>(mut self, key: &'static str, value: T) -> Self {
let type_name = TypeInfo::FullName.of::<T>(); let type_name = TypeInfo::FullName.of::<T>();
self.params.insert(key, (Box::new(value), type_name)); self.params.insert(key, (Box::new(value), type_name));
self self

View file

@ -1,3 +1,4 @@
use crate::async_trait;
use crate::base::action; use crate::base::action;
use crate::core::component::{ComponentError, Context, Contextual}; use crate::core::component::{ComponentError, Context, Contextual};
use crate::core::theme::ThemeRef; use crate::core::theme::ThemeRef;
@ -7,8 +8,9 @@ use crate::html::{Markup, html};
/// Habilita el clonado de componentes. /// Habilita el clonado de componentes.
/// ///
/// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. El método /// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. El método
/// [`clone_box`](Self::clone_box) devuelve una copia en la *pila* del componente original, lo que /// [`clone_box`](Self::clone_box) devuelve un clon del componente original encapsulado en un
/// permite clonar componentes sin conocer su tipo concreto en tiempo de compilación. /// `Box<dyn Component>`, lo que permite clonar componentes sin conocer su tipo concreto en tiempo
/// de compilación.
pub trait ComponentClone { pub trait ComponentClone {
/// Devuelve un clon del componente encapsulado en un [`Box<dyn Component>`]. /// Devuelve un clon del componente encapsulado en un [`Box<dyn Component>`].
fn clone_box(&self) -> Box<dyn Component>; fn clone_box(&self) -> Box<dyn Component>;
@ -18,9 +20,10 @@ pub trait ComponentClone {
/// ///
/// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente /// Este *trait* se implementa automáticamente en cualquier tipo (componente) que implemente
/// [`Component`], por lo que no requiere ninguna codificación manual. /// [`Component`], por lo que no requiere ninguna codificación manual.
#[async_trait]
pub trait ComponentRender { pub trait ComponentRender {
/// Renderiza el componente usando el contexto proporcionado. /// Renderiza el componente usando el contexto proporcionado.
fn render(&mut self, cx: &mut Context) -> Markup; async fn render(&mut self, cx: &mut Context) -> Markup;
} }
/// Interfaz común que debe implementar un componente renderizable en PageTop. /// Interfaz común que debe implementar un componente renderizable en PageTop.
@ -35,9 +38,10 @@ pub trait ComponentRender {
/// ///
/// Todo tipo que implemente `Component` **debe** derivar también [`Clone`]. Aunque el compilador /// Todo tipo que implemente `Component` **debe** derivar también [`Clone`]. Aunque el compilador
/// no lo exige directamente (hacerlo rompería la seguridad de objeto de `dyn Component`), /// no lo exige directamente (hacerlo rompería la seguridad de objeto de `dyn Component`),
/// [`ComponentClone`] se implementa automáticamente mediante una *impl* blanket solo para los tipos /// [`ComponentClone`] se implementa automáticamente mediante una *impl blanket* sólo para los tipos
/// que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar [`ComponentClone`] a /// que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar [`ComponentClone`] a
/// mano, y el componente no podría registrarse en [`InRegion`](crate::core::theme::InRegion). /// mano, y el componente no podría registrarse en [`InRegion`](crate::core::theme::InRegion).
#[async_trait]
pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync { pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// Crea una nueva instancia del componente. /// Crea una nueva instancia del componente.
/// ///
@ -72,12 +76,12 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// ///
/// Por defecto, todos los componentes son renderizables (`true`). Sin embargo, este método /// Por defecto, todos los componentes son renderizables (`true`). Sin embargo, este método
/// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se /// puede sobrescribirse para decidir dinámicamente si los componentes de este tipo se
/// renderizan o no en función del contexto de renderizado. Recibe solo una referencia /// renderizan o no en función del contexto de renderizado. Recibe sólo una referencia
/// compartida al contexto porque su único propósito es consultar datos, no modificarlos. /// compartida al contexto porque su único propósito es consultar datos, no modificarlos.
/// ///
/// También puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo del /// También **puede asignarse una función [`FnIsRenderable`](super::FnIsRenderable) a un campo
/// componente para permitir que instancias concretas del mismo puedan decidir dinámicamente si /// del componente** para permitir que instancias concretas del mismo puedan decidir
/// se renderizan o no. /// dinámicamente si se renderizan o no.
#[allow(unused_variables)] #[allow(unused_variables)]
fn is_renderable(&self, cx: &Context) -> bool { fn is_renderable(&self, cx: &Context) -> bool {
true true
@ -88,8 +92,19 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar /// Segundo paso del [ciclo de renderizado](ComponentRender): se ejecuta tras comprobar
/// [`is_renderable()`](Self::is_renderable) y antes de la acción /// [`is_renderable()`](Self::is_renderable) y antes de la acción
/// [`BeforeRender`](crate::base::action::component::BeforeRender) y de /// [`BeforeRender`](crate::base::action::component::BeforeRender) y de
/// [`prepare()`](Self::prepare). Recibe solo una referencia compartida al contexto porque su /// [`prepare()`](Self::prepare). Recibe sólo una referencia compartida al contexto porque su
/// propósito es mutar el propio componente, no el contexto. Por defecto no hace nada. /// propósito es mutar el propio componente, no el contexto. Por defecto no hace nada.
///
/// Está pensado para **normalizar el estado interno** del componente antes de renderizarlo. Por
/// ejemplo, calcular clases CSS, ajustar valores de campos, derivar atributos a partir del
/// contexto, etc. Se desaconseja utilizar para operaciones de E/S o consultas a base de datos;
/// es intencionadamente síncrono.
///
/// La carga y el acceso a datos en general corresponden a [`prepare()`](Self::prepare), que es
/// `async` precisamente para ello.
///
/// La separación entre `setup()` (mutación de estado) y [`prepare()`](Self::prepare)
/// (generación de HTML) es deliberada y no debe fusionarse.
#[allow(unused_variables)] #[allow(unused_variables)]
fn setup(&mut self, cx: &Context) {} fn setup(&mut self, cx: &Context) {}
@ -97,8 +112,8 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// ///
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras /// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras
/// [`setup()`](Self::setup) y la acción /// [`setup()`](Self::setup) y la acción
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solo si ningún tema /// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solamente si ningún
/// en la cadena devuelve `Some` en /// tema en la cadena devuelve `Some` en
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component). /// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
/// ///
/// 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
@ -107,7 +122,7 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
/// 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)]
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! {}) Ok(html! {})
} }
} }
@ -143,8 +158,9 @@ impl<T: Component + Clone + 'static> ComponentClone for T {
/// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de /// para que las extensiones puedan trabajar sobre el HTML final para modificarlo antes de
/// devolverlo. /// devolverlo.
/// 7. Devuelve el [`Markup`] resultante. /// 7. Devuelve el [`Markup`] resultante.
#[async_trait]
impl<C: Component> ComponentRender for C { impl<C: Component> ComponentRender for C {
fn render(&mut self, cx: &mut Context) -> Markup { async fn render(&mut self, cx: &mut Context) -> Markup {
// Si no es renderizable, devuelve un bloque HTML vacío. // Si no es renderizable, devuelve un bloque HTML vacío.
if !self.is_renderable(cx) { if !self.is_renderable(cx) {
return html! {}; return html! {};
@ -160,12 +176,12 @@ impl<C: Component> ComponentRender for C {
let result = 'resolve: { let result = 'resolve: {
let mut t: Option<ThemeRef> = Some(cx.theme()); let mut t: Option<ThemeRef> = Some(cx.theme());
while let Some(theme) = t { while let Some(theme) = t {
if let Some(r) = theme.handle_component(self, cx) { if let Some(r) = theme.handle_component(self, cx).await {
break 'resolve r; break 'resolve r;
} }
t = theme.parent(); t = theme.parent();
} }
self.prepare(cx) self.prepare(cx).await
}; };
let prepare = match result { let prepare = match result {
Ok(markup) => markup, Ok(markup) => markup,

View file

@ -13,9 +13,10 @@ use crate::{AutoDefault, Getters};
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # #[derive(Clone)] /// # #[derive(Clone)]
/// # struct MyComponent; /// # struct MyComponent;
/// # #[async_trait]
/// # impl Component for MyComponent { /// # impl Component for MyComponent {
/// # fn new() -> Self { MyComponent } /// # fn new() -> Self { MyComponent }
/// fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> { /// async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
/// Err(ComponentError::new("Database connection failed") /// Err(ComponentError::new("Database connection failed")
/// .with_fallback(html! { p class="error" { "Content temporarily unavailable." } })) /// .with_fallback(html! { p class="error" { "Content temporarily unavailable." } }))
/// } /// }

View file

@ -15,6 +15,7 @@ use crate::web::Router;
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct MyExtension; /// pub struct MyExtension;
/// ///
/// #[async_trait]
/// impl Extension for MyExtension { /// impl Extension for MyExtension {
/// fn name(&self) -> L10n { /// fn name(&self) -> L10n {
/// L10n::n("My Extension") /// L10n::n("My Extension")
@ -54,12 +55,14 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct MyTheme; /// pub struct MyTheme;
/// ///
/// #[async_trait]
/// impl Extension for MyTheme { /// impl Extension for MyTheme {
/// fn theme(&self) -> Option<ThemeRef> { /// fn theme(&self) -> Option<ThemeRef> {
/// Some(&Self) /// Some(&Self)
/// } /// }
/// } /// }
/// ///
/// #[async_trait]
/// impl Theme for MyTheme {} /// impl Theme for MyTheme {}
/// ``` /// ```
fn theme(&self) -> Option<ThemeRef> { fn theme(&self) -> Option<ThemeRef> {
@ -81,9 +84,11 @@ pub trait Extension: AnyInfo + Send + Sync {
/// ```rust,no_run /// ```rust,no_run
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// # pub struct Database; /// # pub struct Database;
/// # #[async_trait]
/// # impl Extension for Database {} /// # impl Extension for Database {}
/// pub struct MyApp; /// pub struct MyApp;
/// ///
/// #[async_trait]
/// impl Extension for MyApp { /// impl Extension for MyApp {
/// fn dependencies(&self) -> Vec<ExtensionRef> { /// fn dependencies(&self) -> Vec<ExtensionRef> {
/// vec![&Database] /// vec![&Database]
@ -116,8 +121,6 @@ pub trait Extension: AnyInfo + Send + Sync {
/// lógica de inicialización**. PageTop lo invoca una sola vez, después de que todas las /// lógica de inicialización**. PageTop lo invoca una sola vez, después de que todas las
/// dependencias se han inicializado y antes de aceptar cualquier petición HTTP. /// dependencias se han inicializado y antes de aceptar cualquier petición HTTP.
/// ///
/// En ese caso, el bloque `impl Extension` debe llevar `#[async_trait]`:
///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
@ -130,8 +133,6 @@ pub trait Extension: AnyInfo + Send + Sync {
/// } /// }
/// } /// }
/// ``` /// ```
///
/// Las extensiones que no sobrescriben `initialize()` no necesitan `#[async_trait]`.
async fn initialize(&self) {} async fn initialize(&self) {}
/// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación. /// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación.
@ -160,6 +161,7 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # async fn create_post() -> &'static str { "" } /// # async fn create_post() -> &'static str { "" }
/// pub struct Blog; /// pub struct Blog;
/// ///
/// #[async_trait]
/// impl Extension for Blog { /// impl Extension for Blog {
/// fn configure_router(&self, router: Router) -> Router { /// fn configure_router(&self, router: Router) -> Router {
/// router /// router
@ -178,6 +180,7 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # async fn list_users() -> &'static str { "" } /// # async fn list_users() -> &'static str { "" }
/// pub struct Admin; /// pub struct Admin;
/// ///
/// #[async_trait]
/// impl Extension for Admin { /// impl Extension for Admin {
/// fn configure_router(&self, router: Router) -> Router { /// fn configure_router(&self, router: Router) -> Router {
/// let admin = Router::new() /// let admin = Router::new()
@ -204,6 +207,7 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct Api; /// pub struct Api;
/// ///
/// #[async_trait]
/// impl Extension for Api { /// impl Extension for Api {
/// fn configure_router(&self, router: Router) -> Router { /// fn configure_router(&self, router: Router) -> Router {
/// let api = Router::new() /// let api = Router::new()
@ -226,6 +230,7 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct MyExtension; /// pub struct MyExtension;
/// ///
/// #[async_trait]
/// impl Extension for MyExtension { /// impl Extension for MyExtension {
/// fn configure_router(&self, router: Router) -> Router { /// fn configure_router(&self, router: Router) -> Router {
/// serve_static_files!(router, [assets] => "/static"); /// serve_static_files!(router, [assets] => "/static");
@ -257,6 +262,7 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # ) -> axum::response::Response { next.run(req).await } /// # ) -> axum::response::Response { next.run(req).await }
/// pub struct MyAuth; /// pub struct MyAuth;
/// ///
/// #[async_trait]
/// impl Extension for MyAuth { /// impl Extension for MyAuth {
/// fn configure_middleware(&self, router: Router) -> Router { /// fn configure_middleware(&self, router: Router) -> Router {
/// router.layer(middleware::from_fn(session_middleware)) /// router.layer(middleware::from_fn(session_middleware))

View file

@ -27,6 +27,7 @@
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo, //! 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. //! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
use crate::async_trait;
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::{Markup, html}; use crate::html::{Markup, html};
use crate::locale::L10n; use crate::locale::L10n;
@ -49,7 +50,8 @@ use crate::{AutoDefault, util};
/// ///
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla /// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)). /// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
pub trait Region { #[async_trait]
pub trait Region: Send + Sync {
/// Devuelve el nombre de la región. /// Devuelve el nombre de la región.
/// ///
/// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`] /// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`]
@ -81,12 +83,9 @@ pub trait Region {
/// ///
/// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases /// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases
/// utilizadas o la semántica del marcado generado para cada región. /// utilizadas o la semántica del marcado generado para cada región.
fn render(&'static self, cx: &mut Context) -> Markup async fn render(&self, cx: &mut Context) -> Markup {
where
Self: Sized,
{
html! { html! {
@let region = cx.render_region(self); @let region = cx.render_region_named(self.name()).await;
@if !region.is_empty() { @if !region.is_empty() {
div div
class=(util::join!("region region-", self.name())) class=(util::join!("region region-", self.name()))
@ -157,7 +156,8 @@ impl Region for DefaultRegion {
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>` /// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para /// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden. /// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
pub trait Template { #[async_trait]
pub trait Template: Send + Sync {
/// Renderiza el contenido de la plantilla. /// Renderiza el contenido de la plantilla.
/// ///
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden: /// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
@ -173,11 +173,11 @@ pub trait Template {
/// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el /// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el
/// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la /// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())). /// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
fn render(&'static self, cx: &mut Context) -> Markup { async fn render(&self, cx: &mut Context) -> Markup {
html! { html! {
(DefaultRegion::Header.render(cx)) (DefaultRegion::Header.render(cx).await)
(DefaultRegion::Content.render(cx)) (DefaultRegion::Content.render(cx).await)
(DefaultRegion::Footer.render(cx)) (DefaultRegion::Footer.render(cx).await)
} }
} }
} }
@ -204,6 +204,7 @@ pub enum DefaultTemplate {
Error, Error,
} }
#[async_trait]
impl Template for DefaultTemplate {} impl Template for DefaultTemplate {}
// **< render_component! >************************************************************************** // **< render_component! >**************************************************************************

View file

@ -1,3 +1,4 @@
use crate::async_trait;
use crate::base::component::{Html, Intro, IntroOpening}; use crate::base::component::{Html, Intro, IntroOpening};
use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual}; use crate::core::component::{ChildOp, Component, ComponentError, Context, Contextual};
use crate::core::extension::Extension; use crate::core::extension::Extension;
@ -28,6 +29,7 @@ use crate::web::http::StatusCode;
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct MyTheme; /// pub struct MyTheme;
/// ///
/// #[async_trait]
/// impl Extension for MyTheme { /// impl Extension for MyTheme {
/// fn name(&self) -> L10n { /// fn name(&self) -> L10n {
/// L10n::n("My theme") /// L10n::n("My theme")
@ -42,8 +44,10 @@ use crate::web::http::StatusCode;
/// } /// }
/// } /// }
/// ///
/// #[async_trait]
/// impl Theme for MyTheme {} /// impl Theme for MyTheme {}
/// ``` /// ```
#[async_trait]
pub trait Theme: Extension + Send + Sync { pub trait Theme: Extension + Send + Sync {
/// Devuelve el tema padre del que hereda este tema, si existe. /// Devuelve el tema padre del que hereda este tema, si existe.
/// ///
@ -108,11 +112,11 @@ pub trait Theme: Extension + Send + Sync {
/// - Envolver el contenido en contenedores adicionales. /// - Envolver el contenido en contenedores adicionales.
/// - Implementar lógicas de composición alternativas. /// - Implementar lógicas de composición alternativas.
#[inline] #[inline]
fn render_page_body(&self, page: &mut Page) -> Markup { async fn render_page_body(&self, page: &mut Page) -> Markup {
if let Some(parent) = self.parent() { if let Some(parent) = self.parent() {
parent.render_page_body(page) parent.render_page_body(page).await
} else { } else {
page.template().render(page.context()) page.template().render(page.context()).await
} }
} }
@ -152,9 +156,9 @@ pub trait Theme: Extension + Send + Sync {
/// ///
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo, /// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.). /// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
fn render_page_head(&self, page: &mut Page) -> Markup { async fn render_page_head(&self, page: &mut Page) -> Markup {
if let Some(parent) = self.parent() { if let Some(parent) = self.parent() {
return parent.render_page_head(page); return parent.render_page_head(page).await;
} }
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
html! { html! {
@ -222,7 +226,7 @@ pub trait Theme: Extension + Send + Sync {
/// } /// }
/// ``` /// ```
#[allow(unused_variables)] #[allow(unused_variables)]
fn handle_component( async fn handle_component(
&self, &self,
component: &mut dyn Component, component: &mut dyn Component,
cx: &mut Context, cx: &mut Context,

View file

@ -61,39 +61,37 @@ impl ChildrenInRegions {
self self
} }
/// Construye una lista de componentes frescos para la región indicada. /// Ensambla los componentes frescos de la región indicada.
/// ///
/// El orden es: prototipos globales comunes → children propios de la página → /// Se recogen desde tres fuentes disponibles, en el siguiente orden:
/// prototipos específicos del tema activo.
/// ///
/// Los prototipos globales se clonan en cada llamada (clon profundo gracias a /// 1. Prototipos globales comunes, disponibles en cualquier tema. Se clonan en cada petición
/// [`ComponentClone`]), garantizando que `setup()` siempre parte del estado /// para que `setup()` parta siempre de un estado inicial limpio.
/// inicial. Los children propios de la página se mueven (son por petición y no necesitan /// 2. Componentes propios de la página, registrados para esta petición concreta. Se mueven en
/// clonarse). /// 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
/// [`ComponentClone`]: crate::core::component::ComponentClone /// que llegan a `setup()` con el mismo estado inicial.
pub fn children_for(&mut self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children { pub fn assemble_region(&mut self, theme_ref: ThemeRef, region_name: &str) -> Children {
let name = region_ref.name();
let common = COMMON_REGIONS.read(); let common = COMMON_REGIONS.read();
let themed = THEME_REGIONS.read(); let themed = THEME_REGIONS.read();
let mut result = Children::new(); let mut result = Children::new();
// 1. Prototipos globales comunes - clon fresco por cada página. // 1. Prototipos globales comunes.
if let Some(protos) = common.get(name) { if let Some(protos) = common.get(region_name) {
for proto in protos { for proto in protos {
result.add(proto.as_child()); result.add(proto.as_child());
} }
} }
// 2. Children propios de la página - se mueven (son por petición, no requieren clonado). // 2. Componentes propios de la página: se mueven, no se clonan.
if let Some(page_children) = self.0.remove(name) { if let Some(page_children) = self.0.remove(region_name) {
for child in page_children { for child in page_children {
result.add(child); result.add(child);
} }
} }
// 3. Prototipos del tema activo - clon fresco por cada página. // 3. Prototipos del tema activo.
if let Some(theme_map) = themed.get(&theme_ref.type_id()) { if let Some(theme_map) = themed.get(&theme_ref.type_id()) {
if let Some(protos) = theme_map.get(name) { if let Some(protos) = theme_map.get(region_name) {
for proto in protos { for proto in protos {
result.add(proto.as_child()); result.add(proto.as_child());
} }

View file

@ -12,16 +12,16 @@ use crate::locale::L10n;
/// fn render_logo(cx: &mut Context) -> Markup { /// fn render_logo(cx: &mut Context) -> Markup {
/// html! { /// html! {
/// div class="logo_color" { /// div class="logo_color" {
/// (PageTopSvg::Color.render(cx)) /// (PageTopSvg::Color.markup(cx))
/// } /// }
/// div class="line_dark" { /// div class="line_dark" {
/// (PageTopSvg::LineDark.render(cx)) /// (PageTopSvg::LineDark.markup(cx))
/// } /// }
/// div class="line_light" { /// div class="line_light" {
/// (PageTopSvg::LineLight.render(cx)) /// (PageTopSvg::LineLight.markup(cx))
/// } /// }
/// div class="line_red" { /// div class="line_red" {
/// (PageTopSvg::LineRGB(255, 0, 0).render(cx)) /// (PageTopSvg::LineRGB(255, 0, 0).markup(cx))
/// } /// }
/// } /// }
/// }; /// };
@ -41,8 +41,8 @@ pub enum PageTopSvg {
} }
impl PageTopSvg { impl PageTopSvg {
/// Renderiza el SVG del logotipo según la variante elegida. /// Devuelve el marcado SVG del logotipo según la variante elegida.
pub fn render(&self, cx: &Context) -> Markup { pub fn markup(&self, cx: &Context) -> Markup {
let path_fills = match self { let path_fills = match self {
Self::Color => self.logo_color(), Self::Color => self.logo_color(),
Self::LineDark => self.logo_line(10, 11, 9), Self::LineDark => self.logo_line(10, 11, 9),

View file

@ -162,10 +162,11 @@ impl PropsOp {
/// props: Props, /// props: Props,
/// } /// }
/// ///
/// #[async_trait]
/// impl Component for MyButton { /// impl Component for MyButton {
/// fn new() -> Self { Self::default() } /// fn new() -> Self { Self::default() }
/// ///
/// fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { /// async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
/// Ok(html! { /// Ok(html! {
/// button (self.props()) { /// button (self.props()) {
/// (self.label().using(cx)) /// (self.label().using(cx))

View file

@ -52,6 +52,7 @@ use pagetop::prelude::*;
struct HelloWorld; struct HelloWorld;
#[async_trait]
impl Extension for HelloWorld { impl Extension for HelloWorld {
fn configure_router(&self, router: Router) -> Router { fn configure_router(&self, router: Router) -> Router {
router.route("/", web::get(hello_world)) router.route("/", web::get(hello_world))
@ -61,7 +62,7 @@ impl Extension for HelloWorld {
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(|_| html! { h1 { "Hello World!" } })) .with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render() .render().await
} }
#[pagetop::main] #[pagetop::main]
@ -97,12 +98,14 @@ use std::ops::Deref;
/// ///
/// pub struct MyTheme; /// pub struct MyTheme;
/// ///
/// #[async_trait]
/// impl Extension for MyTheme { /// impl Extension for MyTheme {
/// fn theme(&self) -> Option<ThemeRef> { /// fn theme(&self) -> Option<ThemeRef> {
/// Some(&Self) /// Some(&Self)
/// } /// }
/// } /// }
/// ///
/// #[async_trait]
/// impl Theme for MyTheme { /// impl Theme for MyTheme {
/// fn before_render_page_body(&self, page: &mut Page) { /// fn before_render_page_body(&self, page: &mut Page) {
/// page /// page

View file

@ -15,6 +15,7 @@
mod error; mod error;
pub use error::ErrorPage; pub use error::ErrorPage;
pub(crate) use error::render_error_pages;
use crate::auth::CurrentUser; use crate::auth::CurrentUser;
use crate::base::action; use crate::base::action;
@ -200,7 +201,7 @@ impl Page {
/// `lang` y `dir` en la etiqueta `<html>`. /// `lang` y `dir` en la etiqueta `<html>`.
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y /// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
/// devuelve un [`Result`] con el [`Markup`] final. /// devuelve un [`Result`] con el [`Markup`] final.
pub fn render(&mut self) -> Result<Markup, ErrorPage> { pub async fn render(&mut self) -> Result<Markup, ErrorPage> {
// Acciones específicas del tema antes de renderizar el <body>. // Acciones específicas del tema antes de renderizar el <body>.
self.context.theme().before_render_page_body(self); self.context.theme().before_render_page_body(self);
@ -209,9 +210,9 @@ impl Page {
// Renderiza el <body>. // Renderiza el <body>.
let body = html! { let body = html! {
(ReservedRegion::PageTop.render(&mut self.context)) (ReservedRegion::PageTop.render(&mut self.context).await)
(self.context.theme().render_page_body(self)) (self.context.theme().render_page_body(self).await)
(ReservedRegion::PageBottom.render(&mut self.context)) (ReservedRegion::PageBottom.render(&mut self.context).await)
}; };
// Acciones específicas del tema después de renderizar el <body>. // Acciones específicas del tema después de renderizar el <body>.
@ -221,7 +222,7 @@ impl Page {
action::page::AfterRenderBody::dispatch(self); action::page::AfterRenderBody::dispatch(self);
// Renderiza el <head>. // Renderiza el <head>.
let head = self.context.theme().render_page_head(self); let head = self.context.theme().render_page_head(self).await;
// Compone la página incluyendo los atributos de idioma y dirección del texto. // Compone la página incluyendo los atributos de idioma y dirección del texto.
let lang = &self.context.langid().language; let lang = &self.context.langid().language;
@ -283,7 +284,7 @@ impl Contextual for Page {
} }
#[builder_fn] #[builder_fn]
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self { fn with_param<T: Send + Sync + 'static>(mut self, key: &'static str, value: T) -> Self {
self.context.alter_param(key, value); self.context.alter_param(key, value);
self self
} }

View file

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

View file

@ -242,6 +242,7 @@ impl tower::Service<http::Request<Body>> for ServeEmbedded {
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct MyExtension; /// pub struct MyExtension;
/// ///
/// #[async_trait]
/// impl Extension for MyExtension { /// impl Extension for MyExtension {
/// fn configure_router(&self, router: Router) -> Router { /// fn configure_router(&self, router: Router) -> Router {
/// // Forma 1) Sistema de ficheros o embebido. /// // Forma 1) Sistema de ficheros o embebido.

View file

@ -11,6 +11,7 @@ struct TestComp {
text: String, text: String,
} }
#[async_trait]
impl Component for TestComp { impl Component for TestComp {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -20,7 +21,7 @@ impl Component for TestComp {
self.props.get_id() self.props.get_id()
} }
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(html! { (self.text) }) Ok(html! { (self.text) })
} }
} }
@ -48,27 +49,30 @@ impl TestComp {
async fn child_default_is_empty() { async fn child_default_is_empty() {
let child = Child::default(); let child = Child::default();
assert!(child.id().is_none()); assert!(child.id().is_none());
assert!(child.render(&mut Context::default()).is_empty()); assert!(child.render(&mut Context::default()).await.is_empty());
} }
#[pagetop::test] #[pagetop::test]
async fn child_with_stores_component_and_renders_it() { async fn child_with_stores_component_and_renders_it() {
let child = Child::with(TestComp::text("hola")); let child = Child::with(TestComp::text("hello"));
assert_eq!(child.render(&mut Context::default()).into_string(), "hola"); assert_eq!(
child.render(&mut Context::default()).await.into_string(),
"hello"
);
} }
#[pagetop::test] #[pagetop::test]
async fn child_id_returns_component_id() { async fn child_id_returns_component_id() {
let child = Child::with(TestComp::tagged("my-id", "texto")); let child = Child::with(TestComp::tagged("my-id", "text"));
assert_eq!(child.id(), Some("my-id".to_string())); assert_eq!(child.id(), Some("my-id".to_string()));
} }
#[pagetop::test] #[pagetop::test]
async fn child_from_component_is_equivalent_to_with() { async fn child_from_component_is_equivalent_to_with() {
let child: Child = TestComp::text("desde from").into(); let child: Child = TestComp::text("from-trait").into();
assert_eq!( assert_eq!(
child.render(&mut Context::default()).into_string(), child.render(&mut Context::default()).await.into_string(),
"desde from" "from-trait"
); );
} }
@ -78,11 +82,11 @@ async fn child_clone_is_deep() {
let original = Child::with(TestComp::text("original")); let original = Child::with(TestComp::text("original"));
let clone = original.clone(); let clone = original.clone();
assert_eq!( assert_eq!(
original.render(&mut Context::default()).into_string(), original.render(&mut Context::default()).await.into_string(),
"original" "original"
); );
assert_eq!( assert_eq!(
clone.render(&mut Context::default()).into_string(), clone.render(&mut Context::default()).await.into_string(),
"original" "original"
); );
} }
@ -103,7 +107,7 @@ async fn children_add_appends_in_order() {
.with_child(TestComp::text("b")) .with_child(TestComp::text("b"))
.with_child(TestComp::text("c")); .with_child(TestComp::text("c"));
assert_eq!(c.len(), 3); assert_eq!(c.len(), 3);
assert_eq!(c.render(&mut Context::default()).into_string(), "abc"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
} }
#[pagetop::test] #[pagetop::test]
@ -111,13 +115,13 @@ async fn children_add_if_empty_only_adds_when_list_is_empty() {
let mut cx = Context::default(); let mut cx = Context::default();
// Se añade porque la lista está vacía. // Se añade porque la lista está vacía.
let c = Children::new().with_child(ChildOp::AddIfEmpty(TestComp::text("primero").into())); let c = Children::new().with_child(ChildOp::AddIfEmpty(TestComp::text("first").into()));
assert_eq!(c.len(), 1); assert_eq!(c.len(), 1);
// No se añade porque ya hay un elemento. // No se añade porque ya hay un elemento.
let c = c.with_child(ChildOp::AddIfEmpty(TestComp::text("segundo").into())); let c = c.with_child(ChildOp::AddIfEmpty(TestComp::text("second").into()));
assert_eq!(c.len(), 1); assert_eq!(c.len(), 1);
assert_eq!(c.render(&mut cx).into_string(), "primero"); assert_eq!(c.render(&mut cx).await.into_string(), "first");
} }
#[pagetop::test] #[pagetop::test]
@ -128,7 +132,7 @@ async fn children_add_many_appends_all_in_order() {
TestComp::text("z").into(), TestComp::text("z").into(),
])); ]));
assert_eq!(c.len(), 3); assert_eq!(c.len(), 3);
assert_eq!(c.render(&mut Context::default()).into_string(), "xyz"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "xyz");
} }
#[pagetop::test] #[pagetop::test]
@ -136,7 +140,7 @@ async fn children_prepend_inserts_at_start() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::text("b")) .with_child(TestComp::text("b"))
.with_child(ChildOp::Prepend(TestComp::text("a").into())); .with_child(ChildOp::Prepend(TestComp::text("a").into()));
assert_eq!(c.render(&mut Context::default()).into_string(), "ab"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
} }
#[pagetop::test] #[pagetop::test]
@ -147,7 +151,7 @@ async fn children_prepend_many_inserts_all_at_start_maintaining_order() {
TestComp::text("a").into(), TestComp::text("a").into(),
TestComp::text("b").into(), TestComp::text("b").into(),
])); ]));
assert_eq!(c.render(&mut Context::default()).into_string(), "abc"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
} }
#[pagetop::test] #[pagetop::test]
@ -156,7 +160,7 @@ async fn children_insert_after_id_inserts_after_matching_element() {
.with_child(TestComp::tagged("first", "a")) .with_child(TestComp::tagged("first", "a"))
.with_child(TestComp::text("c")) .with_child(TestComp::text("c"))
.with_child(ChildOp::InsertAfterId("first", TestComp::text("b").into())); .with_child(ChildOp::InsertAfterId("first", TestComp::text("b").into()));
assert_eq!(c.render(&mut Context::default()).into_string(), "abc"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
} }
#[pagetop::test] #[pagetop::test]
@ -164,10 +168,10 @@ async fn children_insert_after_id_appends_when_id_not_found() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::text("a")) .with_child(TestComp::text("a"))
.with_child(ChildOp::InsertAfterId( .with_child(ChildOp::InsertAfterId(
"no-existe", "not-found",
TestComp::text("b").into(), TestComp::text("b").into(),
)); ));
assert_eq!(c.render(&mut Context::default()).into_string(), "ab"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
} }
#[pagetop::test] #[pagetop::test]
@ -176,7 +180,7 @@ async fn children_insert_before_id_inserts_before_matching_element() {
.with_child(TestComp::text("a")) .with_child(TestComp::text("a"))
.with_child(TestComp::tagged("last", "c")) .with_child(TestComp::tagged("last", "c"))
.with_child(ChildOp::InsertBeforeId("last", TestComp::text("b").into())); .with_child(ChildOp::InsertBeforeId("last", TestComp::text("b").into()));
assert_eq!(c.render(&mut Context::default()).into_string(), "abc"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "abc");
} }
#[pagetop::test] #[pagetop::test]
@ -184,10 +188,10 @@ async fn children_insert_before_id_prepends_when_id_not_found() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::text("b")) .with_child(TestComp::text("b"))
.with_child(ChildOp::InsertBeforeId( .with_child(ChildOp::InsertBeforeId(
"no-existe", "not-found",
TestComp::text("a").into(), TestComp::text("a").into(),
)); ));
assert_eq!(c.render(&mut Context::default()).into_string(), "ab"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "ab");
} }
#[pagetop::test] #[pagetop::test]
@ -198,28 +202,28 @@ async fn children_remove_by_id_removes_first_matching_element() {
.with_child(TestComp::text("c")) .with_child(TestComp::text("c"))
.with_child(ChildOp::RemoveById("drop")); .with_child(ChildOp::RemoveById("drop"));
assert_eq!(c.len(), 2); assert_eq!(c.len(), 2);
assert_eq!(c.render(&mut Context::default()).into_string(), "ac"); assert_eq!(c.render(&mut Context::default()).await.into_string(), "ac");
} }
#[pagetop::test] #[pagetop::test]
async fn children_remove_by_id_does_nothing_when_id_not_found() { async fn children_remove_by_id_does_nothing_when_id_not_found() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::text("a")) .with_child(TestComp::text("a"))
.with_child(ChildOp::RemoveById("no-existe")); .with_child(ChildOp::RemoveById("not-found"));
assert_eq!(c.len(), 1); assert_eq!(c.len(), 1);
} }
#[pagetop::test] #[pagetop::test]
async fn children_replace_by_id_replaces_first_matching_element() { async fn children_replace_by_id_replaces_first_matching_element() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::tagged("target", "viejo")) .with_child(TestComp::tagged("target", "old"))
.with_child(TestComp::text("b")) .with_child(TestComp::text("b"))
.with_child(ChildOp::ReplaceById( .with_child(ChildOp::ReplaceById("target", TestComp::text("new").into()));
"target",
TestComp::text("nuevo").into(),
));
assert_eq!(c.len(), 2); assert_eq!(c.len(), 2);
assert_eq!(c.render(&mut Context::default()).into_string(), "nuevob"); assert_eq!(
c.render(&mut Context::default()).await.into_string(),
"newb"
);
} }
#[pagetop::test] #[pagetop::test]
@ -234,11 +238,11 @@ async fn children_reset_clears_all_elements() {
#[pagetop::test] #[pagetop::test]
async fn children_get_by_id_returns_first_matching_child() { async fn children_get_by_id_returns_first_matching_child() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::tagged("uno", "a")) .with_child(TestComp::tagged("one", "a"))
.with_child(TestComp::tagged("dos", "b")); .with_child(TestComp::tagged("two", "b"));
assert!(c.get_by_id("uno").is_some()); assert!(c.get_by_id("one").is_some());
assert!(c.get_by_id("dos").is_some()); assert!(c.get_by_id("two").is_some());
assert!(c.get_by_id("tres").is_none()); assert!(c.get_by_id("three").is_none());
} }
#[pagetop::test] #[pagetop::test]
@ -246,21 +250,21 @@ async fn children_iter_by_id_yields_all_matching_children() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::tagged("rep", "a")) .with_child(TestComp::tagged("rep", "a"))
.with_child(TestComp::tagged("rep", "b")) .with_child(TestComp::tagged("rep", "b"))
.with_child(TestComp::tagged("otro", "c")); .with_child(TestComp::tagged("other", "c"));
assert_eq!(c.iter_by_id("rep").count(), 2); assert_eq!(c.iter_by_id("rep").count(), 2);
assert_eq!(c.iter_by_id("otro").count(), 1); assert_eq!(c.iter_by_id("other").count(), 1);
assert_eq!(c.iter_by_id("ninguno").count(), 0); assert_eq!(c.iter_by_id("none").count(), 0);
} }
#[pagetop::test] #[pagetop::test]
async fn children_render_concatenates_all_outputs_in_order() { async fn children_render_concatenates_all_outputs_in_order() {
let c = Children::new() let c = Children::new()
.with_child(TestComp::text("uno ")) .with_child(TestComp::text("one "))
.with_child(TestComp::text("dos ")) .with_child(TestComp::text("two "))
.with_child(TestComp::text("tres")); .with_child(TestComp::text("three"));
assert_eq!( assert_eq!(
c.render(&mut Context::default()).into_string(), c.render(&mut Context::default()).await.into_string(),
"uno dos tres" "one two three"
); );
} }
@ -270,29 +274,29 @@ async fn children_render_concatenates_all_outputs_in_order() {
async fn embed_default_is_empty() { async fn embed_default_is_empty() {
let embed: Embed<TestComp> = Embed::default(); let embed: Embed<TestComp> = Embed::default();
assert!(embed.id().is_none()); assert!(embed.id().is_none());
assert!(embed.render(&mut Context::default()).is_empty()); assert!(embed.render(&mut Context::default()).await.is_empty());
assert!(embed.get().is_none()); assert!(embed.get().is_none());
} }
#[pagetop::test] #[pagetop::test]
async fn embed_with_stores_component() { async fn embed_with_stores_component() {
let embed = Embed::with(TestComp::text("contenido")); let embed = Embed::with(TestComp::text("content"));
assert!(embed.get().is_some()); assert!(embed.get().is_some());
assert_eq!( assert_eq!(
embed.render(&mut Context::default()).into_string(), embed.render(&mut Context::default()).await.into_string(),
"contenido" "content"
); );
} }
#[pagetop::test] #[pagetop::test]
async fn embed_id_returns_component_id() { async fn embed_id_returns_component_id() {
let embed = Embed::with(TestComp::tagged("embed-id", "texto")); let embed = Embed::with(TestComp::tagged("embed-id", "text"));
assert_eq!(embed.id(), Some("embed-id".to_string())); assert_eq!(embed.id(), Some("embed-id".to_string()));
} }
#[pagetop::test] #[pagetop::test]
async fn embed_get_is_some_when_component_present() { async fn embed_get_is_some_when_component_present() {
let embed = Embed::with(TestComp::tagged("abc", "hola")); let embed = Embed::with(TestComp::tagged("abc", "hello"));
// `get()` devuelve Some; la lectura del id verifica que accede al componente correctamente. // `get()` devuelve Some; la lectura del id verifica que accede al componente correctamente.
assert!(embed.get().is_some()); assert!(embed.get().is_some());
assert_eq!(embed.id(), Some("abc".to_string())); assert_eq!(embed.id(), Some("abc".to_string()));
@ -300,38 +304,36 @@ async fn embed_get_is_some_when_component_present() {
#[pagetop::test] #[pagetop::test]
async fn embed_get_allows_mutating_component() { async fn embed_get_allows_mutating_component() {
let embed = Embed::with(TestComp::tagged("orig", "texto")); let mut embed = Embed::with(TestComp::tagged("orig", "text"));
// El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`. if let Some(comp) = embed.get_mut() {
if let Some(mut comp) = embed.get() {
comp.props comp.props
.alter_prop(PropsOp::set_id("modificado".to_string())); .alter_prop(PropsOp::set_id("modified".to_string()));
}; };
assert_eq!(embed.id(), Some("modificado".to_string())); assert_eq!(embed.id(), Some("modified".to_string()));
} }
#[pagetop::test] #[pagetop::test]
async fn embed_with_component_replaces_content() { async fn embed_with_component_replaces_content() {
let embed = let embed = Embed::with(TestComp::text("first")).with_component(Some(TestComp::text("second")));
Embed::with(TestComp::text("primero")).with_component(Some(TestComp::text("segundo")));
assert_eq!( assert_eq!(
embed.render(&mut Context::default()).into_string(), embed.render(&mut Context::default()).await.into_string(),
"segundo" "second"
); );
} }
#[pagetop::test] #[pagetop::test]
async fn embed_with_component_none_empties_embed() { async fn embed_with_component_none_empties_embed() {
let embed = Embed::with(TestComp::text("algo")).with_component(None); let embed = Embed::with(TestComp::text("something")).with_component(None);
assert!(embed.get().is_none()); assert!(embed.get().is_none());
assert!(embed.render(&mut Context::default()).is_empty()); assert!(embed.render(&mut Context::default()).await.is_empty());
} }
#[pagetop::test] #[pagetop::test]
async fn embed_clone_is_deep() { async fn embed_clone_is_deep() {
let original = Embed::with(TestComp::tagged("orig", "texto")); let original = Embed::with(TestComp::tagged("orig", "text"));
let clone = original.clone(); let mut clone = original.clone();
// Mutar el clon no debe afectar al original. // Mutar el clon no debe afectar al original.
if let Some(mut comp) = clone.get() { if let Some(comp) = clone.get_mut() {
comp.props comp.props
.alter_prop(PropsOp::set_id("clone-id".to_string())); .alter_prop(PropsOp::set_id("clone-id".to_string()));
} }
@ -341,10 +343,10 @@ async fn embed_clone_is_deep() {
#[pagetop::test] #[pagetop::test]
async fn embed_converts_into_child() { async fn embed_converts_into_child() {
let embed = Embed::with(TestComp::text("desde embed")); let embed = Embed::with(TestComp::text("from embed"));
let child = Child::from(embed); let child = Child::from(embed);
assert_eq!( assert_eq!(
child.render(&mut Context::default()).into_string(), child.render(&mut Context::default()).await.into_string(),
"desde embed" "from embed"
); );
} }

View file

@ -8,8 +8,8 @@ async fn component_html_renders_static_markup() {
} }
}); });
let markup = component.render(&mut Context::default()); let markup = component.render(&mut Context::default()).await;
assert_eq!(markup.0, "<p>Test</p>"); assert_eq!(markup.into_string(), "<p>Test</p>");
} }
#[pagetop::test] #[pagetop::test]
@ -23,8 +23,8 @@ async fn component_html_renders_using_context_param() {
} }
}); });
let markup = component.render(&mut cx); let markup = component.render(&mut cx).await;
assert_eq!(markup.0, "<span>Alice</span>"); assert_eq!(markup.into_string(), "<span>Alice</span>");
} }
#[pagetop::test] #[pagetop::test]
@ -33,16 +33,16 @@ async fn component_html_allows_replacing_render_function() {
component.alter_fn(|_| html! { div { "Modified" } }); component.alter_fn(|_| html! { div { "Modified" } });
let markup = component.render(&mut Context::default()); let markup = component.render(&mut Context::default()).await;
assert_eq!(markup.0, "<div>Modified</div>"); assert_eq!(markup.into_string(), "<div>Modified</div>");
} }
#[pagetop::test] #[pagetop::test]
async fn component_html_default_renders_empty_markup() { async fn component_html_default_renders_empty_markup() {
let mut component = Html::default(); let mut component = Html::default();
let markup = component.render(&mut Context::default()); let markup = component.render(&mut Context::default()).await;
assert_eq!(markup.0, ""); assert_eq!(markup.into_string(), "");
} }
#[pagetop::test] #[pagetop::test]
@ -60,6 +60,6 @@ async fn component_html_can_access_request_path() {
html! { span { (path) } } html! { span { (path) } }
}); });
let markup = component.render(&mut cx); let markup = component.render(&mut cx).await;
assert_eq!(markup.0, "<span>/hello/world</span>"); assert_eq!(markup.into_string(), "<span>/hello/world</span>");
} }

View file

@ -13,13 +13,13 @@ async fn poweredby_default_shows_only_pagetop_recognition() {
setup().await; setup().await;
let mut p = PoweredBy::default(); let mut p = PoweredBy::default();
let html = p.render(&mut Context::default()); let html = p.render(&mut Context::default()).await.into_string();
// Debe mostrar el bloque de reconocimiento a PageTop. // Debe mostrar el bloque de reconocimiento a PageTop.
assert!(html.as_str().contains("poweredby__pagetop")); assert!(html.contains("poweredby__pagetop"));
// Y NO debe mostrar el bloque de copyright. // Y NO debe mostrar el bloque de copyright.
assert!(!html.as_str().contains("poweredby__copyright")); assert!(!html.contains("poweredby__copyright"));
} }
#[pagetop::test] #[pagetop::test]
@ -27,23 +27,20 @@ async fn poweredby_new_includes_current_year_and_app_name() {
setup().await; setup().await;
let mut p = PoweredBy::new(); let mut p = PoweredBy::new();
let html = p.render(&mut Context::default()); let html = p.render(&mut Context::default()).await.into_string();
let year = Utc::now().format("%Y").to_string(); let year = Utc::now().format("%Y").to_string();
assert!( assert!(html.contains(&year), "HTML should include the current year");
html.as_str().contains(&year),
"HTML should include the current year"
);
// El nombre de la app proviene de `global::SETTINGS.app.name`. // El nombre de la app proviene de `global::SETTINGS.app.name`.
let app_name = &global::SETTINGS.app.name; let app_name = &global::SETTINGS.app.name;
assert!( assert!(
html.as_str().contains(app_name), html.contains(app_name),
"HTML should include the application name" "HTML should include the application name"
); );
// Debe existir el span de copyright. // Debe existir el span de copyright.
assert!(html.as_str().contains("poweredby__copyright")); assert!(html.contains("poweredby__copyright"));
} }
#[pagetop::test] #[pagetop::test]
@ -52,10 +49,10 @@ async fn poweredby_with_copyright_overrides_text() {
let custom = "2001 © FooBar Inc."; let custom = "2001 © FooBar Inc.";
let mut p = PoweredBy::default().with_copyright(Some(custom)); let mut p = PoweredBy::default().with_copyright(Some(custom));
let html = p.render(&mut Context::default()); let html = p.render(&mut Context::default()).await.into_string();
assert!(html.as_str().contains(custom)); assert!(html.contains(custom));
assert!(html.as_str().contains("poweredby__copyright")); assert!(html.contains("poweredby__copyright"));
} }
#[pagetop::test] #[pagetop::test]
@ -63,11 +60,11 @@ async fn poweredby_with_copyright_none_hides_text() {
setup().await; setup().await;
let mut p = PoweredBy::new().with_copyright(None::<String>); let mut p = PoweredBy::new().with_copyright(None::<String>);
let html = p.render(&mut Context::default()); let html = p.render(&mut Context::default()).await.into_string();
assert!(!html.as_str().contains("poweredby__copyright")); assert!(!html.contains("poweredby__copyright"));
// El reconocimiento a PageTop siempre debe aparecer. // El reconocimiento a PageTop siempre debe aparecer.
assert!(html.as_str().contains("poweredby__pagetop")); assert!(html.contains("poweredby__pagetop"));
} }
#[pagetop::test] #[pagetop::test]
@ -75,10 +72,10 @@ async fn poweredby_link_points_to_crates_io() {
setup().await; setup().await;
let mut p = PoweredBy::default(); let mut p = PoweredBy::default();
let html = p.render(&mut Context::default()); let html = p.render(&mut Context::default()).await.into_string();
assert!( assert!(
html.as_str().contains("https://pagetop.cillero.es"), html.contains("https://pagetop.cillero.es"),
"Link should point to pagetop.cillero.es" "Link should point to pagetop.cillero.es"
); );
} }

View file

@ -8,6 +8,7 @@ struct TestMarkupComponent {
markup: Markup, markup: Markup,
} }
#[async_trait]
impl Component for TestMarkupComponent { impl Component for TestMarkupComponent {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
@ -17,7 +18,7 @@ impl Component for TestMarkupComponent {
cx.param_or::<bool>("renderable", true) cx.param_or::<bool>("renderable", true)
} }
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
Ok(self.markup.clone()) Ok(self.markup.clone())
} }
} }
@ -74,7 +75,7 @@ async fn non_renderable_component_produces_empty_markup() {
let mut comp = TestMarkupComponent { let mut comp = TestMarkupComponent {
markup: html! { p { "Should never be rendered" } }, markup: html! { p { "Should never be rendered" } },
}; };
assert_eq!(comp.render(&mut cx).into_string(), ""); assert_eq!(comp.render(&mut cx).await.into_string(), "");
} }
#[pagetop::test] #[pagetop::test]
@ -93,7 +94,7 @@ async fn markup_from_component_equals_markup_reinjected_in_html_macro() {
let mut comp = TestMarkupComponent { let mut comp = TestMarkupComponent {
markup: markup.clone(), markup: markup.clone(),
}; };
comp.render(&mut cx).into_string() comp.render(&mut cx).await.into_string()
}; };
// Vía 2: reinyectamos el Markup en `html!` directamente. // Vía 2: reinyectamos el Markup en `html!` directamente.