♻️ Convierte initialize() y Application en async

`async_trait` se re-exporta desde `pagetop` y se añade al preludio; ya
no es necesario declararlo como dependencia en las extensiones.
`initialize()` es ahora verdaderamente `async` (con `.await`), y
`Application::new()` y `prepare()` pasan también a ser funciones
`async`.
This commit is contained in:
Manuel Cillero 2026-07-04 22:56:19 +02:00
parent 54d5b3e53d
commit 8aa4372bdf
16 changed files with 159 additions and 107 deletions

View file

@ -11,9 +11,8 @@
[![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop) [![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop#licencia) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop#licencia)
</div>
<br> <br>
</div>
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript. creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
@ -29,8 +28,7 @@ según las necesidades de cada proyecto, incluyendo:
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y * **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
componentes. componentes.
## Guía rápida
## ⚡️ Guía rápida
La aplicación más sencilla de PageTop se ve así: La aplicación más sencilla de PageTop se ve así:
@ -39,14 +37,14 @@ use pagetop::prelude::*;
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::new().run()?.await Application::new().await.run().await
} }
``` ```
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`. bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera: Para personalizar el servicio, puedes crear una extensión de PageTop de la siguiente manera:
```rust,no_run ```rust,no_run
use pagetop::prelude::*; use pagetop::prelude::*;
@ -54,28 +52,28 @@ use pagetop::prelude::*;
struct HelloWorld; struct HelloWorld;
impl Extension for HelloWorld { impl Extension for HelloWorld {
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { fn configure_router(&self, router: Router) -> Router {
scfg.route("/", service::web::get().to(hello_world)); router.route("/", web::get(hello_world))
} }
} }
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.add_child(Html::with(|_| html! { h1 { "Hello World!" } })) .with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render() .render()
} }
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await Application::prepare(&HelloWorld).await.run().await
} }
``` ```
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:
@ -114,7 +112,7 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
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
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto: `.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
@ -135,14 +133,14 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
> `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:
@ -156,7 +154,7 @@ Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el es
el ecosistema Rust. el ecosistema Rust.
## Contribuir ## Contribuir
PageTop mantiene **un único repositorio oficial**: PageTop mantiene **un único repositorio oficial**:

View file

@ -2,5 +2,5 @@ use pagetop::prelude::*;
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::new().run()?.await Application::new().await.run().await
} }

View file

@ -437,5 +437,5 @@ fn form_lists() -> Form {
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&FormControls).run()?.await Application::prepare(&FormControls).await.run().await
} }

View file

@ -23,5 +23,5 @@ async fn hello_name(
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&HelloName).run()?.await Application::prepare(&HelloName).await.run().await
} }

View file

@ -20,5 +20,5 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await Application::prepare(&HelloWorld).await.run().await
} }

View file

@ -78,5 +78,5 @@ async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&IntroColors).run()?.await Application::prepare(&IntroColors).await.run().await
} }

View file

@ -5,6 +5,7 @@ include_locales!(LOC from "examples/locale");
struct SuperMenu; struct SuperMenu;
#[async_trait]
impl Extension for SuperMenu { impl Extension for SuperMenu {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![
@ -14,7 +15,7 @@ impl Extension for SuperMenu {
] ]
} }
fn initialize(&self) { async fn initialize(&self) {
let navbar_menu = bs::Navbar::brand_left(bs::navbar::Brand::new()) let navbar_menu = bs::Navbar::brand_left(bs::navbar::Brand::new())
.with_expand(token::BreakPoint::LG) .with_expand(token::BreakPoint::LG)
.with_item(bs::navbar::Item::nav( .with_item(bs::navbar::Item::nav(
@ -114,5 +115,5 @@ impl Extension for SuperMenu {
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&SuperMenu).run()?.await Application::prepare(&SuperMenu).await.run().await
} }

View file

@ -9,45 +9,40 @@ use crate::response::page::ErrorPage;
use crate::web::{HttpRequest, Router}; use crate::web::{HttpRequest, Router};
use crate::{PAGETOP_VERSION, global, trace}; use crate::{PAGETOP_VERSION, global, trace};
use std::future::Future;
use std::io::Error; use std::io::Error;
use std::sync::LazyLock; use std::sync::LazyLock;
/// Punto de entrada de una aplicación PageTop. /// Punto de entrada de una aplicación PageTop.
/// ///
/// No almacena datos, **encapsula** el inicio completo de la configuración y puesta en marcha de la /// Orquesta el arranque de la aplicación. Primero se instancia con [`new()`](Application::new) o
/// aplicación. Para instanciarla se puede usar [`new()`](Application::new) o /// [`prepare()`](Application::prepare), y después se ejecuta usando [`run()`](Application::run) (o
/// [`prepare()`](Application::prepare). Después sólo hay que llamar a [`run()`](Application::run) /// usando [`test()`](Application::test) si se está preparando un entorno de pruebas).
/// para ejecutar la aplicación (o a [`test()`](Application::test) si se está preparando un entorno
/// de pruebas).
pub struct Application; pub struct Application;
impl Default for Application {
fn default() -> Self {
Self::new()
}
}
impl Application { impl Application {
/// Crea una instancia mínima de la aplicación, sin extensión raíz. /// Crea una instancia mínima de la aplicación, sin extensión raíz.
/// ///
/// Útil para verificar que el servidor arranca correctamente. Para una aplicación real, usa /// Útil para verificar que el servidor arranca correctamente. Para una aplicación real, usa
/// [`prepare()`](Application::prepare) con una extensión raíz. /// [`prepare()`](Application::prepare) con una extensión raíz.
pub fn new() -> Self { pub async fn new() -> Self {
Self::internal_prepare(None) Self::internal_prepare(None).await
} }
/// Prepara una instancia de la aplicación a partir de una extensión raíz. /// Prepara una instancia de la aplicación a partir de una extensión raíz.
/// ///
/// Las dependencias se habilitan en orden: primero las que no dependen de ninguna otra, luego /// Inicializa la aplicación habilitando las extensiones en orden de dependencia: primero las
/// las que dependen de extensiones ya habilitadas, y así sucesivamente hasta dejar habilitada /// que no dependen de ninguna otra, luego las que dependen de extensiones ya habilitadas, y así
/// la extensión raíz. /// hasta habilitar la extensión raíz.
pub fn prepare(root_extension: ExtensionRef) -> Self { ///
Self::internal_prepare(Some(root_extension)) /// Es *async* porque cada extensión puede realizar operaciones asíncronas en su
/// [`initialize()`](crate::core::extension::Extension::initialize) (conexión a base de datos,
/// migraciones, semillas de datos...).
pub async fn prepare(root_extension: ExtensionRef) -> Self {
Self::internal_prepare(Some(root_extension)).await
} }
// Secuencia de arranque común a new() y prepare(). // Secuencia de arranque común a new() y prepare().
fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self { async fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self {
// Al arrancar muestra una cabecera para la aplicación. // Al arrancar muestra una cabecera para la aplicación.
Self::show_banner(); Self::show_banner();
@ -64,7 +59,7 @@ impl Application {
extension::all::register_actions(); extension::all::register_actions();
// Inicializa las extensiones. // Inicializa las extensiones.
extension::all::initialize_extensions(); extension::all::initialize_extensions().await;
Self Self
} }
@ -119,9 +114,9 @@ impl Application {
/// Arranca el servidor web de la aplicación. /// Arranca el servidor web de la aplicación.
/// ///
/// Enlaza el puerto del servidor web de forma síncrona (puede fallar con [`std::io::Error`] si /// Enlaza el puerto del servidor web (puede fallar con [`std::io::Error`] si el puerto ya está
/// el puerto ya está en uso o el proceso carece de permisos) y devuelve un [`Future`] que /// en uso o el proceso carece de permisos) y ejecuta el bucle de atención de peticiones. El
/// ejecuta el bucle de atención de peticiones. El patrón habitual es: /// patrón habitual es:
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
@ -132,26 +127,24 @@ impl Application {
/// ///
/// #[pagetop::main] /// #[pagetop::main]
/// async fn main() -> std::io::Result<()> { /// async fn main() -> std::io::Result<()> {
/// Application::prepare(&MyApp).run()?.await /// Application::prepare(&MyApp).await.run().await
/// } /// }
/// ``` /// ```
pub fn run(self) -> Result<impl Future<Output = Result<(), Error>>, Error> { pub async fn run(self) -> Result<(), Error> {
let addr = format!( let addr = format!(
"{}:{}", "{}:{}",
global::SETTINGS.server.bind_address, global::SETTINGS.server.bind_address,
global::SETTINGS.server.bind_port global::SETTINGS.server.bind_port
); );
// Enlaza el puerto de forma síncrona para detectar errores antes del *await*. // Enlaza el puerto de forma síncrona para detectar errores.
let std_listener = std::net::TcpListener::bind(&addr)?; let std_listener = std::net::TcpListener::bind(&addr)?;
std_listener.set_nonblocking(true)?; std_listener.set_nonblocking(true)?;
let router = Self::build_router(); let router = Self::build_router();
Ok(async move {
let listener = tokio::net::TcpListener::from_std(std_listener)?; let listener = tokio::net::TcpListener::from_std(std_listener)?;
axum::serve(listener, router).await axum::serve(listener, router).await
})
} }
/// Devuelve el servidor web configurado para usarlo en pruebas de integración. /// Devuelve el servidor web configurado para usarlo en pruebas de integración.

View file

@ -72,10 +72,10 @@ pub fn register_actions() {
// **< INICIALIZA LAS EXTENSIONES >***************************************************************** // **< INICIALIZA LAS EXTENSIONES >*****************************************************************
pub fn initialize_extensions() { pub async fn initialize_extensions() {
trace::info!("Calling application bootstrap"); trace::info!("Calling application bootstrap");
for e in EXTENSIONS.get().into_iter().flatten() { for e in EXTENSIONS.get().into_iter().flatten() {
e.initialize(); e.initialize().await;
} }
} }

View file

@ -1,4 +1,5 @@
use crate::actions; use crate::actions;
use crate::async_trait;
use crate::core::AnyInfo; use crate::core::AnyInfo;
use crate::core::action::ActionBox; use crate::core::action::ActionBox;
use crate::core::theme::ThemeRef; use crate::core::theme::ThemeRef;
@ -24,6 +25,7 @@ use crate::web::Router;
/// } /// }
/// } /// }
/// ``` /// ```
#[async_trait]
pub trait Extension: AnyInfo + Send + Sync { pub trait Extension: AnyInfo + Send + Sync {
/// Nombre de la extensión como *texto localizado* legible para el usuario. /// Nombre de la extensión como *texto localizado* legible para el usuario.
/// ///
@ -64,10 +66,30 @@ pub trait Extension: AnyInfo + Send + Sync {
None None
} }
/// Otras extensiones que deben habilitarse **antes** de esta. /// Extensiones que deben inicializarse **antes** de esta.
/// ///
/// PageTop resolverá automáticamente estas dependencias respetando el orden durante el arranque /// PageTop usa este método para construir un grafo de dependencias en tiempo de ejecución entre
/// de la aplicación. /// extensiones. Garantiza que todas las extensiones requeridas están presentes y se inicializan
/// en el orden apropiado, es decir, las dependencias siempre antes que la extensión que las
/// declara.
///
/// Esta declaración es independiente de las dependencias en `Cargo.toml`, donde Rust gestiona
/// las dependencias de los *crates* en tiempo de compilación.
///
/// # Ejemplo
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # pub struct Database;
/// # impl Extension for Database {}
/// pub struct MyApp;
///
/// impl Extension for MyApp {
/// fn dependencies(&self) -> Vec<ExtensionRef> {
/// vec![&Database]
/// }
/// }
/// ```
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![] vec![]
} }
@ -83,9 +105,34 @@ pub trait Extension: AnyInfo + Send + Sync {
/// Inicializa la extensión durante la fase de arranque de la aplicación. /// Inicializa la extensión durante la fase de arranque de la aplicación.
/// ///
/// Se llama una sola vez, después de que todas las dependencias se han inicializado y antes de /// La firma extendida que se muestra es el resultado de aplicar la macro
/// aceptar cualquier petición HTTP. /// [`pagetop::async_trait`](pagetop::async_trait) a la declaración del método:
fn initialize(&self) {} ///
/// ```text
/// async fn initialize(&self) {}
/// ```
///
/// Solo es necesario sobrescribir este método **cuando la extensión necesita ejecutar alguna
/// 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.
///
/// En ese caso, el bloque `impl Extension` debe llevar `#[async_trait]`:
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// pub struct MyExtension;
///
/// #[async_trait]
/// impl Extension for MyExtension {
/// async fn initialize(&self) {
/// // lógica de inicialización...
/// }
/// }
/// ```
///
/// Las extensiones que no sobrescriben `initialize()` no necesitan `#[async_trait]`.
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.
/// ///
@ -96,10 +143,10 @@ pub trait Extension: AnyInfo + Send + Sync {
/// # Operaciones disponibles /// # Operaciones disponibles
/// ///
/// | Operación | Llamada sobre `router` | /// | Operación | Llamada sobre `router` |
/// |------------------------------------|-------------------------------------------------| /// |------------------------------------|----------------------------------------------------|
/// | Ruta HTTP | `.route("/path", web::get(handler))` | /// | Ruta HTTP | `.route("/path", web::get(handler))` |
/// | Rutas bajo prefijo común | `.nest("/prefix", sub_router)` | /// | Rutas bajo prefijo común | `.nest("/prefix", sub_router)` |
/// | Archivos estáticos | `serve_static_files!(router, [...] => "/path")` | /// | Archivos estáticos | `serve_static_files!(router, [assets] => "/path")` |
/// | Estado compartido entre *handlers* | `.with_state(my_state)` | /// | Estado compartido entre *handlers* | `.with_state(my_state)` |
/// ///
/// # Ejemplos /// # Ejemplos

View file

@ -29,7 +29,7 @@ según las necesidades de cada proyecto, incluyendo:
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y * **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
componentes. componentes.
# Guía rápida ## Guía rápida
La aplicación más sencilla de PageTop se ve así: La aplicación más sencilla de PageTop se ve así:
@ -38,14 +38,14 @@ use pagetop::prelude::*;
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::new().run()?.await Application::new().await.run().await
} }
``` ```
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`. bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera: Para personalizar el servicio, puedes crear una extensión de PageTop de la siguiente manera:
```rust,no_run ```rust,no_run
use pagetop::prelude::*; use pagetop::prelude::*;
@ -66,22 +66,12 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
#[pagetop::main] #[pagetop::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await Application::prepare(&HelloWorld).await.run().await
} }
``` ```
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>`.
# 🧩 Gestión de Dependencias
Los proyectos que utilizan PageTop gestionan las dependencias con `cargo`, como cualquier otro
proyecto en Rust.
Sin embargo, es fundamental que cada extensión declare explícitamente sus
[dependencias](core::extension::Extension::dependencies), si las tiene, para que PageTop pueda
estructurar e inicializar la aplicación de forma modular.
*/ */
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
@ -132,6 +122,21 @@ use std::ops::Deref;
/// referencia a la versión del *crate* que lo usa. /// referencia a la versión del *crate* que lo usa.
pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Re-exporta el atributo [`async_trait`](https://docs.rs/async-trait) para implementar *traits*
/// con métodos `async`.
///
/// Si en el ámbito se declara `use pagetop::prelude::*` o `use pagetop::async_trait;` basta con
/// usar el nombre corto `#[async_trait]`. Otra opción es usar su forma cualificada
/// `#[pagetop::async_trait]`.
///
/// Al estar incluido en PageTop, no hace falta añadir la dependencia `async-trait` en el
/// `Cargo.toml` del proyecto.
///
/// En proyectos que crean extensiones de PageTop, su uso es necesario cuando se sobrescribe
/// [`initialize()`](crate::core::extension::Extension::initialize) en un bloque
/// [`impl Extension`](crate::core::extension::Extension).
pub use async_trait::async_trait;
pub use pagetop_macros::{AutoDefault, builder_fn, html, main, test}; pub use pagetop_macros::{AutoDefault, builder_fn, html, main, test};
pub use pagetop_statics::{StaticFile, resource}; pub use pagetop_statics::{StaticFile, resource};
@ -174,8 +179,8 @@ pub type UniqueId = std::any::TypeId;
/// Representa el peso lógico de una instancia en una colección ordenada por pesos. /// Representa el peso lógico de una instancia en una colección ordenada por pesos.
/// ///
/// Las instancias con pesos **más bajos**, incluyendo valores negativos (`-128..127`), se situarán /// Las instancias con pesos **más bajos**, incluyendo valores negativos, se situarán antes en la
/// antes en la ordenación. /// ordenación. Acepta valores `i8` de -128 a 127.
pub type Weight = i8; pub type Weight = i8;
// **< API >**************************************************************************************** // **< API >****************************************************************************************

View file

@ -4,7 +4,7 @@
pub use crate::PAGETOP_VERSION; pub use crate::PAGETOP_VERSION;
pub use crate::{builder_fn, html, main, test}; pub use crate::{async_trait, builder_fn, html, main, test};
pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight}; pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight};

View file

@ -4,13 +4,13 @@ use pagetop::prelude::*;
/// ///
/// Los tests de este módulo renderizan componentes directamente con `Context::default()`, por lo /// Los tests de este módulo renderizan componentes directamente con `Context::default()`, por lo
/// que sólo necesitan el subsistema de localización y las extensiones registradas, no un router. /// que sólo necesitan el subsistema de localización y las extensiones registradas, no un router.
fn setup() { async fn setup() {
let _ = Application::new(); Application::new().await;
} }
#[pagetop::test] #[pagetop::test]
async fn poweredby_default_shows_only_pagetop_recognition() { async fn poweredby_default_shows_only_pagetop_recognition() {
setup(); 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());
@ -24,7 +24,7 @@ async fn poweredby_default_shows_only_pagetop_recognition() {
#[pagetop::test] #[pagetop::test]
async fn poweredby_new_includes_current_year_and_app_name() { async fn poweredby_new_includes_current_year_and_app_name() {
setup(); 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());
@ -48,7 +48,7 @@ async fn poweredby_new_includes_current_year_and_app_name() {
#[pagetop::test] #[pagetop::test]
async fn poweredby_with_copyright_overrides_text() { async fn poweredby_with_copyright_overrides_text() {
setup(); setup().await;
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));
@ -60,7 +60,7 @@ async fn poweredby_with_copyright_overrides_text() {
#[pagetop::test] #[pagetop::test]
async fn poweredby_with_copyright_none_hides_text() { async fn poweredby_with_copyright_none_hides_text() {
setup(); 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());
@ -72,7 +72,7 @@ async fn poweredby_with_copyright_none_hides_text() {
#[pagetop::test] #[pagetop::test]
async fn poweredby_link_points_to_crates_io() { async fn poweredby_link_points_to_crates_io() {
setup(); 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());
@ -85,7 +85,7 @@ async fn poweredby_link_points_to_crates_io() {
#[pagetop::test] #[pagetop::test]
async fn poweredby_getter_reflects_internal_state() { async fn poweredby_getter_reflects_internal_state() {
setup(); setup().await;
// Por defecto no hay copyright. // Por defecto no hay copyright.
let p0 = PoweredBy::default(); let p0 = PoweredBy::default();

View file

@ -1,8 +1,12 @@
use pagetop::prelude::*; use pagetop::prelude::*;
async fn setup() {
Application::new().await;
}
#[pagetop::test] #[pagetop::test]
async fn literal_text() { async fn literal_text() {
let _app = web::test::init_router(Application::new().test()); setup().await;
let l10n = L10n::n("© 2025 PageTop"); let l10n = L10n::n("© 2025 PageTop");
assert_eq!(l10n.get(), Some("© 2025 PageTop".to_string())); assert_eq!(l10n.get(), Some("© 2025 PageTop".to_string()));
@ -10,7 +14,7 @@ async fn literal_text() {
#[pagetop::test] #[pagetop::test]
async fn translation_without_args() { async fn translation_without_args() {
let _app = web::test::init_router(Application::new().test()); setup().await;
let l10n = L10n::l("test_hello_world"); let l10n = L10n::l("test_hello_world");
let translation = l10n.lookup(&Locale::resolve("es-ES")); let translation = l10n.lookup(&Locale::resolve("es-ES"));
@ -19,7 +23,7 @@ async fn translation_without_args() {
#[pagetop::test] #[pagetop::test]
async fn translation_with_args() { async fn translation_with_args() {
let _app = web::test::init_router(Application::new().test()); setup().await;
let l10n = L10n::l("test_hello_user").with_arg("userName", "Manuel"); let l10n = L10n::l("test_hello_user").with_arg("userName", "Manuel");
let translation = l10n.lookup(&Locale::resolve("es-ES")); let translation = l10n.lookup(&Locale::resolve("es-ES"));
@ -28,7 +32,7 @@ async fn translation_with_args() {
#[pagetop::test] #[pagetop::test]
async fn translation_with_plural_and_select() { async fn translation_with_plural_and_select() {
let _app = web::test::init_router(Application::new().test()); setup().await;
let l10n = L10n::l("test_shared_photos").with_args(vec![ let l10n = L10n::l("test_shared_photos").with_args(vec![
("userName", "Roberto"), ("userName", "Roberto"),
@ -41,7 +45,7 @@ async fn translation_with_plural_and_select() {
#[pagetop::test] #[pagetop::test]
async fn check_fallback_language() { async fn check_fallback_language() {
let _app = web::test::init_router(Application::new().test()); setup().await;
let l10n = L10n::l("test_hello_world"); let l10n = L10n::l("test_hello_world");
let translation = l10n.lookup(&Locale::resolve("xx-YY")); // Retrocede a "en-US". let translation = l10n.lookup(&Locale::resolve("xx-YY")); // Retrocede a "en-US".
@ -50,7 +54,7 @@ async fn check_fallback_language() {
#[pagetop::test] #[pagetop::test]
async fn check_unknown_key() { async fn check_unknown_key() {
let _app = web::test::init_router(Application::new().test()); setup().await;
let l10n = L10n::l("non-existent-key"); let l10n = L10n::l("non-existent-key");
let translation = l10n.lookup(&Locale::resolve("en-US")); let translation = l10n.lookup(&Locale::resolve("en-US"));

View file

@ -2,7 +2,7 @@ use pagetop::prelude::*;
#[pagetop::test] #[pagetop::test]
async fn homepage_returns_404() { async fn homepage_returns_404() {
let app = web::test::init_router(Application::new().test()); let app = web::test::init_router(Application::new().await.test());
let req = web::test::TestRequest::get().uri("/").to_request(); let req = web::test::TestRequest::get().uri("/").to_request();
let resp = web::test::send_request(&app, req).await; let resp = web::test::send_request(&app, req).await;

View file

@ -3,6 +3,10 @@ use pagetop::prelude::*;
use std::{borrow::Cow, fs, io}; use std::{borrow::Cow, fs, io};
use tempfile::TempDir; use tempfile::TempDir;
async fn setup() {
Application::new().await;
}
// **< Testing normalize_ascii() >****************************************************************** // **< Testing normalize_ascii() >******************************************************************
fn assert_err(input: &str, expected: util::NormalizeAsciiError) { fn assert_err(input: &str, expected: util::NormalizeAsciiError) {
@ -265,7 +269,7 @@ mod unix {
#[pagetop::test] #[pagetop::test]
async fn ok_absolute_dir() -> io::Result<()> { async fn ok_absolute_dir() -> io::Result<()> {
let _app = web::test::init_router(Application::new().test()); setup().await;
// /tmp/<rand>/sub // /tmp/<rand>/sub
let td = TempDir::new()?; let td = TempDir::new()?;
@ -279,7 +283,7 @@ mod unix {
#[pagetop::test] #[pagetop::test]
async fn ok_relative_dir_with_manifest() -> io::Result<()> { async fn ok_relative_dir_with_manifest() -> io::Result<()> {
let _app = web::test::init_router(Application::new().test()); setup().await;
let td = TempDir::new()?; let td = TempDir::new()?;
let sub = td.path().join("sub"); let sub = td.path().join("sub");
@ -293,7 +297,7 @@ mod unix {
#[pagetop::test] #[pagetop::test]
async fn error_not_a_directory() -> io::Result<()> { async fn error_not_a_directory() -> io::Result<()> {
let _app = web::test::init_router(Application::new().test()); setup().await;
let td = TempDir::new()?; let td = TempDir::new()?;
let file = td.path().join("foo.txt"); let file = td.path().join("foo.txt");
@ -311,7 +315,7 @@ mod windows {
#[pagetop::test] #[pagetop::test]
async fn ok_absolute_dir() -> io::Result<()> { async fn ok_absolute_dir() -> io::Result<()> {
let _app = web::test::init_router(Application::new().test()); setup().await;
// C:\Users\...\Temp\... // C:\Users\...\Temp\...
let td = TempDir::new()?; let td = TempDir::new()?;
@ -325,7 +329,7 @@ mod windows {
#[pagetop::test] #[pagetop::test]
async fn ok_relative_dir_with_manifest() -> io::Result<()> { async fn ok_relative_dir_with_manifest() -> io::Result<()> {
let _app = web::test::init_router(Application::new().test()); setup().await;
let td = TempDir::new()?; let td = TempDir::new()?;
let sub = td.path().join("sub"); let sub = td.path().join("sub");
@ -339,7 +343,7 @@ mod windows {
#[pagetop::test] #[pagetop::test]
async fn error_not_a_directory() -> io::Result<()> { async fn error_not_a_directory() -> io::Result<()> {
let _app = web::test::init_router(Application::new().test()); setup().await;
let td = TempDir::new()?; let td = TempDir::new()?;
let file = td.path().join("foo.txt"); let file = td.path().join("foo.txt");