♻️ 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:
parent
54d5b3e53d
commit
8aa4372bdf
16 changed files with 159 additions and 107 deletions
30
README.md
30
README.md
|
|
@ -11,9 +11,8 @@
|
||||||
[](https://crates.io/crates/pagetop)
|
[](https://crates.io/crates/pagetop)
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
[](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**:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
src/app.rs
55
src/app.rs
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
@ -95,12 +142,12 @@ 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
|
||||||
///
|
///
|
||||||
|
|
|
||||||
39
src/lib.rs
39
src/lib.rs
|
|
@ -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 >****************************************************************************************
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue