♻️ 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

@ -9,45 +9,40 @@ use crate::response::page::ErrorPage;
use crate::web::{HttpRequest, Router};
use crate::{PAGETOP_VERSION, global, trace};
use std::future::Future;
use std::io::Error;
use std::sync::LazyLock;
/// 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
/// aplicación. Para instanciarla se puede usar [`new()`](Application::new) o
/// [`prepare()`](Application::prepare). Después sólo hay que llamar a [`run()`](Application::run)
/// para ejecutar la aplicación (o a [`test()`](Application::test) si se está preparando un entorno
/// de pruebas).
/// Orquesta el arranque de la aplicación. Primero se instancia con [`new()`](Application::new) o
/// [`prepare()`](Application::prepare), y después se ejecuta usando [`run()`](Application::run) (o
/// usando [`test()`](Application::test) si se está preparando un entorno de pruebas).
pub struct Application;
impl Default for Application {
fn default() -> Self {
Self::new()
}
}
impl Application {
/// 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
/// [`prepare()`](Application::prepare) con una extensión raíz.
pub fn new() -> Self {
Self::internal_prepare(None)
pub async fn new() -> Self {
Self::internal_prepare(None).await
}
/// 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
/// las que dependen de extensiones ya habilitadas, y así sucesivamente hasta dejar habilitada
/// la extensión raíz.
pub fn prepare(root_extension: ExtensionRef) -> Self {
Self::internal_prepare(Some(root_extension))
/// Inicializa la aplicación habilitando las extensiones en orden de dependencia: primero las
/// que no dependen de ninguna otra, luego las que dependen de extensiones ya habilitadas, y así
/// hasta habilitar la extensión raíz.
///
/// 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().
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.
Self::show_banner();
@ -64,7 +59,7 @@ impl Application {
extension::all::register_actions();
// Inicializa las extensiones.
extension::all::initialize_extensions();
extension::all::initialize_extensions().await;
Self
}
@ -119,9 +114,9 @@ impl Application {
/// 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
/// el puerto ya está en uso o el proceso carece de permisos) y devuelve un [`Future`] que
/// ejecuta el bucle de atención de peticiones. El patrón habitual es:
/// Enlaza el puerto del servidor web (puede fallar con [`std::io::Error`] si el puerto ya está
/// en uso o el proceso carece de permisos) y ejecuta el bucle de atención de peticiones. El
/// patrón habitual es:
///
/// ```rust,no_run
/// use pagetop::prelude::*;
@ -132,26 +127,24 @@ impl Application {
///
/// #[pagetop::main]
/// 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!(
"{}:{}",
global::SETTINGS.server.bind_address,
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)?;
std_listener.set_nonblocking(true)?;
let router = Self::build_router();
Ok(async move {
let listener = tokio::net::TcpListener::from_std(std_listener)?;
axum::serve(listener, router).await
})
let listener = tokio::net::TcpListener::from_std(std_listener)?;
axum::serve(listener, router).await
}
/// 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 >*****************************************************************
pub fn initialize_extensions() {
pub async fn initialize_extensions() {
trace::info!("Calling application bootstrap");
for e in EXTENSIONS.get().into_iter().flatten() {
e.initialize();
e.initialize().await;
}
}

View file

@ -1,4 +1,5 @@
use crate::actions;
use crate::async_trait;
use crate::core::AnyInfo;
use crate::core::action::ActionBox;
use crate::core::theme::ThemeRef;
@ -24,6 +25,7 @@ use crate::web::Router;
/// }
/// }
/// ```
#[async_trait]
pub trait Extension: AnyInfo + Send + Sync {
/// Nombre de la extensión como *texto localizado* legible para el usuario.
///
@ -64,10 +66,30 @@ pub trait Extension: AnyInfo + Send + Sync {
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
/// de la aplicación.
/// PageTop usa este método para construir un grafo de dependencias en tiempo de ejecución entre
/// 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> {
vec![]
}
@ -83,9 +105,34 @@ pub trait Extension: AnyInfo + Send + Sync {
/// 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
/// aceptar cualquier petición HTTP.
fn initialize(&self) {}
/// La firma extendida que se muestra es el resultado de aplicar la macro
/// [`pagetop::async_trait`](pagetop::async_trait) a la declaración del método:
///
/// ```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.
///
@ -95,12 +142,12 @@ pub trait Extension: AnyInfo + Send + Sync {
///
/// # Operaciones disponibles
///
/// | Operación | Llamada sobre `router` |
/// |------------------------------------|-------------------------------------------------|
/// | Ruta HTTP | `.route("/path", web::get(handler))` |
/// | Rutas bajo prefijo común | `.nest("/prefix", sub_router)` |
/// | Archivos estáticos | `serve_static_files!(router, [...] => "/path")` |
/// | Estado compartido entre *handlers* | `.with_state(my_state)` |
/// | Operación | Llamada sobre `router` |
/// |------------------------------------|----------------------------------------------------|
/// | Ruta HTTP | `.route("/path", web::get(handler))` |
/// | Rutas bajo prefijo común | `.nest("/prefix", sub_router)` |
/// | Archivos estáticos | `serve_static_files!(router, [assets] => "/path")` |
/// | Estado compartido entre *handlers* | `.with_state(my_state)` |
///
/// # 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
componentes.
# Guía rápida
## Guía rápida
La aplicación más sencilla de PageTop se ve así:
@ -38,14 +38,14 @@ use pagetop::prelude::*;
#[pagetop::main]
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
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
use pagetop::prelude::*;
@ -66,22 +66,12 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
#[pagetop::main]
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
(`/`) 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.
(`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`.
*/
#![cfg_attr(docsrs, feature(doc_cfg))]
@ -132,6 +122,21 @@ use std::ops::Deref;
/// referencia a la versión del *crate* que lo usa.
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_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.
///
/// Las instancias con pesos **más bajos**, incluyendo valores negativos (`-128..127`), se situarán
/// antes en la ordenación.
/// Las instancias con pesos **más bajos**, incluyendo valores negativos, se situarán antes en la
/// ordenación. Acepta valores `i8` de -128 a 127.
pub type Weight = i8;
// **< API >****************************************************************************************

View file

@ -4,7 +4,7 @@
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};