From c1afe0e70c7405d23c50b361df20f93ef0b27441 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 31 May 2026 23:38:43 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migra=20API=20p=C3=BAblica?= =?UTF-8?q?=20de=20actix-web=20a=20Axum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `configure_service` como `configure_router(Router) -> Router`. - Macro `static_files_service!` como `serve_static_files!`. - `ResultPage` eliminado; handlers devuelven `Result`. - `ErrorPage` implementa `IntoResponse` en lugar de `ResponseError`. - Registro con `OnceLock`; eliminados `drop_extensions` y `app.welcome`. - `Redirect` devuelve `Response`; docs y ejemplos actualizados. --- README.md | 2 +- examples/form-controls.rs | 6 +- examples/hello-name.rs | 9 +- examples/hello-world.rs | 6 +- examples/intro-colors.rs | 6 +- examples/navbar-menus.rs | 6 +- extensions/pagetop-aliner/README.md | 2 +- extensions/pagetop-aliner/src/lib.rs | 7 +- extensions/pagetop-bootsier/README.md | 2 +- extensions/pagetop-bootsier/src/lib.rs | 9 +- .../src/theme/image/component.rs | 2 +- helpers/pagetop-build/README.md | 4 +- helpers/pagetop-build/src/lib.rs | 15 +- src/app.rs | 4 +- src/base/extension/welcome.rs | 6 +- src/core/extension/all.rs | 117 +++------ src/core/extension/definition.rs | 111 +++++++-- src/global.rs | 34 ++- src/html/assets/favicon.rs | 16 +- src/html/assets/javascript.rs | 2 +- src/html/assets/stylesheet.rs | 2 +- src/html/route.rs | 2 +- src/lib.rs | 2 +- src/locale/request.rs | 2 +- src/prelude.rs | 6 +- src/response/json.rs | 10 +- src/response/page.rs | 12 +- src/response/page/error.rs | 61 ++--- src/response/redirect.rs | 60 +++-- src/web.rs | 225 +++++++++--------- 30 files changed, 393 insertions(+), 355 deletions(-) diff --git a/README.md b/README.md index 604c4b3c..89fade06 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ impl Extension for HelloWorld { } } -async fn hello_world(request: HttpRequest) -> ResultPage { +async fn hello_world(request: HttpRequest) -> Result { Page::new(request) .add_child(Html::with(|_| html! { h1 { "Hello World!" } })) .render() diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 4a6fc6c0..1c7f066e 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -11,12 +11,12 @@ impl Extension for FormControls { vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(form_controls)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(form_controls)) } } -async fn form_controls(request: HttpRequest) -> ResultPage { +async fn form_controls(request: HttpRequest) -> Result { Page::new(request) .with_child( Intro::default() diff --git a/examples/hello-name.rs b/examples/hello-name.rs index e2904c6f..71439c7d 100644 --- a/examples/hello-name.rs +++ b/examples/hello-name.rs @@ -3,16 +3,15 @@ use pagetop::prelude::*; struct HelloName; impl Extension for HelloName { - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/hello/{name}", service::web::get().to(hello_name)); + fn configure_router(&self, router: Router) -> Router { + router.route("/hello/{name}", web::get(hello_name)) } } async fn hello_name( request: HttpRequest, - path: service::web::Path, -) -> ResultPage { - let name = path.into_inner(); + web::Path(name): web::Path, +) -> Result { Page::new(request) .with_child(Html::with(move |_| { html! { diff --git a/examples/hello-world.rs b/examples/hello-world.rs index e6127af9..f1c40d23 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -3,12 +3,12 @@ use pagetop::prelude::*; struct HelloWorld; impl Extension for HelloWorld { - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(hello_world)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(hello_world)) } } -async fn hello_world(request: HttpRequest) -> ResultPage { +async fn hello_world(request: HttpRequest) -> Result { Page::new(request) .with_child(Html::with(|_| { html! { diff --git a/examples/intro-colors.rs b/examples/intro-colors.rs index 57ddeed4..b219c5be 100644 --- a/examples/intro-colors.rs +++ b/examples/intro-colors.rs @@ -5,12 +5,12 @@ include_locales!(LOC from "examples/locale"); struct IntroColors; impl Extension for IntroColors { - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(intro_colors)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(intro_colors)) } } -async fn intro_colors(request: HttpRequest) -> ResultPage { +async fn intro_colors(request: HttpRequest) -> Result { Page::new(request) .with_child( Intro::default() diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 38918aed..7f8ccdda 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -8,7 +8,11 @@ struct SuperMenu; impl Extension for SuperMenu { fn dependencies(&self) -> Vec { - vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] + vec![ + &pagetop_aliner::Aliner, + &pagetop_bootsier::Bootsier, + &pagetop::base::extension::Welcome, + ] } fn initialize(&self) { diff --git a/extensions/pagetop-aliner/README.md b/extensions/pagetop-aliner/README.md index 7b772591..bf515d66 100644 --- a/extensions/pagetop-aliner/README.md +++ b/extensions/pagetop-aliner/README.md @@ -64,7 +64,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_aliner::Aliner; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Aliner) .add_child( diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 42a89e48..dedf4e19 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_aliner::Aliner; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Aliner) .with_child( @@ -109,8 +109,9 @@ impl Extension for Aliner { Some(&Self) } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, [aliner] => "/aliner"); + fn configure_router(&self, router: Router) -> Router { + serve_static_files!(router, [aliner] => "/aliner"); + router } } diff --git a/extensions/pagetop-bootsier/README.md b/extensions/pagetop-bootsier/README.md index edb0be75..f71f221e 100644 --- a/extensions/pagetop-bootsier/README.md +++ b/extensions/pagetop-bootsier/README.md @@ -64,7 +64,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_bootsier::Bootsier; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Bootsier) .add_child( diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index ca2a80c8..8c0ec847 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_bootsier::Bootsier; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Bootsier) .with_child( @@ -140,9 +140,10 @@ impl Extension for Bootsier { Some(&Self) } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs"); - static_files_service!(scfg, [bootsier_js] => "/bootsier/js"); + fn configure_router(&self, router: Router) -> Router { + serve_static_files!(router, [bootsier_bs] => "/bootsier/bs"); + serve_static_files!(router, [bootsier_js] => "/bootsier/js"); + router } } diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index 678ccdb3..df2c28a7 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -55,7 +55,7 @@ impl Component for Image { { (logo.render(cx)) } - }) + }); } image::Source::Responsive(source) => Some(source), image::Source::Thumbnail(source) => Some(source), diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md index c5d9c5bd..bb7d3bfa 100644 --- a/helpers/pagetop-build/README.md +++ b/helpers/pagetop-build/README.md @@ -94,7 +94,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro usen nombres diferentes. Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos -en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html) +en [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: ```rust,ignore @@ -105,7 +105,7 @@ pub struct MyExtension; impl Extension for MyExtension { // Servicio web que publica los recursos de `guides` en `/ruta/a/guides`. fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, guides => "/ruta/a/guides"); + serve_static_files!(scfg, guides => "/ruta/a/guides"); } } ``` diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index 774a4af7..f8390ee6 100644 --- a/helpers/pagetop-build/src/lib.rs +++ b/helpers/pagetop-build/src/lib.rs @@ -95,7 +95,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro usen nombres diferentes. Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos -en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html) +en [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: ```rust,ignore @@ -104,9 +104,10 @@ use pagetop::prelude::*; pub struct MyExtension; impl Extension for MyExtension { - /// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`. - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, guides => "/ruta/a/guides"); + /// Registra los recursos de `guides` en el router bajo `/ruta/a/guides`. + fn configure_router(&self, mut router: Router) -> Router { + serve_static_files!(router, [guides] => "/ruta/a/guides"); + router } } ``` @@ -116,10 +117,10 @@ impl Extension for MyExtension { html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" )] -use grass::{from_path, Options, OutputStyle}; -use pagetop_statics::{resource_dir, ResourceDir}; +use grass::{Options, OutputStyle, from_path}; +use pagetop_statics::{ResourceDir, resource_dir}; -use std::fs::{create_dir_all, remove_dir_all, File}; +use std::fs::{File, create_dir_all, remove_dir_all}; use std::io::Write; use std::path::Path; diff --git a/src/app.rs b/src/app.rs index d24a03d1..4a009fb7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ mod figfont; use crate::core::{extension, extension::ExtensionRef}; use crate::html::Markup; use crate::locale::Locale; -use crate::response::page::{ErrorPage, ResultPage}; +use crate::response::page::ErrorPage; use crate::web::{HttpRequest, Router}; use crate::{PAGETOP_VERSION, global, trace}; @@ -159,6 +159,6 @@ impl Application { } } -async fn route_not_found(request: HttpRequest) -> ResultPage { +async fn route_not_found(request: HttpRequest) -> Result { Err(ErrorPage::NotFound(request)) } diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs index b8739a40..b6d09dbe 100644 --- a/src/base/extension/welcome.rs +++ b/src/base/extension/welcome.rs @@ -20,12 +20,12 @@ impl Extension for Welcome { L10n::l("welcome_extension_description") } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(home)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(home)) } } -async fn home(request: HttpRequest) -> ResultPage { +async fn home(request: HttpRequest) -> Result { let app = &global::SETTINGS.app.name; Page::new(request) diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index b787c9a8..f9081d83 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -1,60 +1,43 @@ use crate::core::action::add_action; use crate::core::extension::ExtensionRef; use crate::core::theme::all::THEMES; -use crate::{global, service, static_files_service, trace}; +use crate::web::Router; +use crate::{global, serve_static_files, trace, web}; -use parking_lot::RwLock; +use std::sync::OnceLock; -use std::sync::LazyLock; - -// **< EXTENSIONES >******************************************************************************** - -static ENABLED_EXTENSIONS: LazyLock>> = - LazyLock::new(|| RwLock::new(Vec::new())); - -static DROPPED_EXTENSIONS: LazyLock>> = - LazyLock::new(|| RwLock::new(Vec::new())); +static EXTENSIONS: OnceLock> = OnceLock::new(); // **< REGISTRO DE LAS EXTENSIONES >**************************************************************** pub fn register_extensions(root_extension: Option) { - // Prepara la lista de extensiones habilitadas. - let mut enabled_list: Vec = Vec::new(); + // Garantiza que ocurre sólo una vez cuando los tests se ejecutan en paralelo. + EXTENSIONS.get_or_init(|| { + let mut list: Vec = Vec::new(); - // Primero añade el tema básico a la lista de extensiones habilitadas. - add_to_enabled(&mut enabled_list, &crate::base::theme::Basic); + // Primero añade el tema básico a la lista de extensiones habilitadas. + add_to_enabled(&mut list, &crate::base::theme::Basic); - // Si se proporciona una extensión raíz inicial, se añade a la lista de extensiones habilitadas. - if let Some(extension) = root_extension { - add_to_enabled(&mut enabled_list, extension); - } + // Si se proporciona la extensión raíz inicial, se añade a las extensiones habilitadas. + if let Some(extension) = root_extension { + add_to_enabled(&mut list, extension); + } - // Añade la página de bienvenida predefinida si se habilita en la configuración. - if global::SETTINGS.app.welcome { - add_to_enabled(&mut enabled_list, &crate::base::extension::Welcome); - } + // Añade la página de bienvenida si no hay extensión raíz. + if root_extension.is_none() { + add_to_enabled(&mut list, &crate::base::extension::Welcome); + } - // Guarda la lista final de extensiones habilitadas. - ENABLED_EXTENSIONS.write().append(&mut enabled_list); - - // Prepara una lista de extensiones deshabilitadas. - let mut dropped_list: Vec = Vec::new(); - - // Si se proporciona una extensión raíz, analiza su lista de dependencias. - if let Some(extension) = root_extension { - add_to_dropped(&mut dropped_list, extension); - } - - // Guarda la lista final de extensiones deshabilitadas. - DROPPED_EXTENSIONS.write().append(&mut dropped_list); + list + }); } fn add_to_enabled(list: &mut Vec, extension: ExtensionRef) { // Verifica que la extensión no esté en la lista para evitar duplicados. if !list.iter().any(|e| e.type_id() == extension.type_id()) { // Añade primero (en orden inverso) las dependencias de la extensión. - for d in extension.dependencies().iter().rev() { - add_to_enabled(list, *d); + for d in extension.dependencies().into_iter().rev() { + add_to_enabled(list, d); } // Añade la propia extensión a la lista. @@ -77,40 +60,11 @@ fn add_to_enabled(list: &mut Vec, extension: ExtensionRef) { } } -fn add_to_dropped(list: &mut Vec, extension: ExtensionRef) { - // Recorre las extensiones que la actual recomienda deshabilitar. - for d in &extension.drop_extensions() { - // Verifica que la extensión no esté ya en la lista. - if !list.iter().any(|e| e.type_id() == d.type_id()) { - // Comprueba si la extensión está habilitada. Si es así, registra una advertencia. - if ENABLED_EXTENSIONS - .read() - .iter() - .any(|e| e.type_id() == extension.type_id()) - { - trace::warn!( - "Trying to drop \"{}\" extension which is enabled", - extension.short_name() - ); - } else { - // Si la extensión no está habilitada, se añade a la lista y registra la acción. - list.push(*d); - trace::debug!("Extension \"{}\" dropped", d.short_name()); - // Añade recursivamente las dependencias de la extensión eliminada. - // De este modo, todas las dependencias se tienen en cuenta para ser deshabilitadas. - for dependency in &extension.dependencies() { - add_to_dropped(list, *dependency); - } - } - } - } -} - // **< REGISTRO DE LAS ACCIONES >******************************************************************* pub fn register_actions() { - for extension in ENABLED_EXTENSIONS.read().iter() { - for a in extension.actions().into_iter() { + for extension in EXTENSIONS.get().into_iter().flatten() { + for a in extension.actions() { add_action(a); } } @@ -120,25 +74,28 @@ pub fn register_actions() { pub fn initialize_extensions() { trace::info!("Calling application bootstrap"); - for extension in ENABLED_EXTENSIONS.read().iter() { - extension.initialize(); + for e in EXTENSIONS.get().into_iter().flatten() { + e.initialize(); } } -// **< CONFIGURA LOS SERVICIOS >******************************************************************** +// **< CONFIGURA LAS RUTAS >************************************************************************ -pub fn configure_services(scfg: &mut service::web::ServiceConfig) { +pub fn configure_routes(router: Router) -> Router { // Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos. #[cfg(debug_assertions)] - scfg.route( - // Ruta automática lanzada por Chrome DevTools. + let router = router.route( "/.well-known/appspecific/com.chrome.devtools.json", - service::web::get().to(|| async { service::HttpResponse::NotFound().finish() }), + web::get(|| async { web::http::StatusCode::NOT_FOUND }), ); - for extension in ENABLED_EXTENSIONS.read().iter() { - extension.configure_service(scfg); - } + let router = EXTENSIONS + .get() + .into_iter() + .flatten() + .fold(router, |r, e| e.configure_router(r)); - static_files_service!(scfg, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/"); + serve_static_files!(router, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/pagetop"); + + router } diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index a5d2b723..984a5cc1 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -1,8 +1,9 @@ +use crate::actions; +use crate::core::AnyInfo; use crate::core::action::ActionBox; use crate::core::theme::ThemeRef; -use crate::core::AnyInfo; use crate::locale::L10n; -use crate::{actions, service}; +use crate::web::Router; /// Interfaz común que debe implementar cualquier extensión de PageTop. /// @@ -11,15 +12,15 @@ use crate::{actions, service}; /// /// ```rust /// # use pagetop::prelude::*; -/// pub struct Blog; +/// pub struct MyExtension; /// -/// impl Extension for Blog { +/// impl Extension for MyExtension { /// fn name(&self) -> L10n { -/// L10n::n("Blog") +/// L10n::n("My Extension") /// } /// /// fn description(&self) -> L10n { -/// L10n::n("Blog system") +/// L10n::n("Does something useful") /// } /// } /// ``` @@ -86,31 +87,95 @@ pub trait Extension: AnyInfo + Send + Sync { /// aceptar cualquier petición HTTP. fn initialize(&self) {} - /// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros - /// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig). + /// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación. /// - /// # Ejemplo + /// Recibe las rutas acumuladas hasta ese momento, añade lo que la extensión necesite y retorna + /// las rutas con las nuevas modificaciones. La implementación por defecto devuelve las rutas + /// sin cambios. /// - /// ```rust,ignore + /// # 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")` | + /// | Capa de *middleware* | `.layer(some_layer)` | + /// | Estado compartido entre *handlers* | `.with_state(my_state)` | + /// + /// # Ejemplos + /// + /// ## Rutas HTTP básicas + /// + /// ```rust /// # use pagetop::prelude::*; - /// pub struct ExtensionSample; + /// # async fn list_posts() -> &'static str { "" } + /// # async fn view_post() -> &'static str { "" } + /// # async fn create_post() -> &'static str { "" } + /// pub struct Blog; /// - /// impl Extension for ExtensionSample { - /// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - /// scfg.route("/sample", web::get().to(route_sample)); + /// impl Extension for Blog { + /// fn configure_router(&self, router: Router) -> Router { + /// router + /// .route("/posts", web::get(list_posts)) + /// .route("/posts/{id}", web::get(view_post)) + /// .route("/posts/new", web::post(create_post)) /// } /// } /// ``` - #[allow(unused_variables)] - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {} - - /// Permite declarar extensiones destinadas a deshabilitar o desinstalar recursos de otras - /// extensiones asociadas a versiones anteriores de la aplicación. /// - /// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras - /// implementaciones. - fn drop_extensions(&self) -> Vec { - vec![] + /// ## Rutas agrupadas bajo un prefijo + /// + /// ```rust + /// # use pagetop::prelude::*; + /// # async fn dashboard() -> &'static str { "" } + /// # async fn list_users() -> &'static str { "" } + /// pub struct Admin; + /// + /// impl Extension for Admin { + /// fn configure_router(&self, router: Router) -> Router { + /// let admin = Router::new() + /// .route("/dashboard", web::get(dashboard)) + /// .route("/users", web::get(list_users)); + /// + /// router.nest("/admin", admin) + /// } + /// } + /// ``` + /// + /// ## Rutas con capa de *middleware* + /// + /// ```rust,ignore + /// # use pagetop::prelude::*; + /// pub struct Api; + /// + /// impl Extension for Api { + /// fn configure_router(&self, router: Router) -> Router { + /// router + /// .route("/api/data", web::get(get_data)) + /// .layer(auth_layer()) + /// } + /// } + /// ``` + /// + /// ## Archivos estáticos + /// + /// La macro [`serve_static_files!`](crate::serve_static_files) sombrea `router` internamente, + /// por lo que el parámetro no necesita `mut`. Sí es necesario devolverlo al final. + /// + /// ```rust,ignore + /// # use pagetop::prelude::*; + /// pub struct MyExtension; + /// + /// impl Extension for MyExtension { + /// fn configure_router(&self, router: Router) -> Router { + /// serve_static_files!(router, [assets] => "/static"); + /// router + /// } + /// } + /// ``` + fn configure_router(&self, router: Router) -> Router { + router } } diff --git a/src/global.rs b/src/global.rs index 953dfb6d..d6bdbc47 100644 --- a/src/global.rs +++ b/src/global.rs @@ -20,27 +20,26 @@ pub use log_format::LogFormat; include_config!(SETTINGS: Settings => [ // [app] - "app.name" => "PageTop App", - "app.description" => "Developed with the amazing PageTop framework.", - "app.theme" => "Basic", - "app.lang_negotiation" => "Full", - "app.startup_banner" => "Slant", - "app.welcome" => true, + "app.name" => "PageTop App", + "app.description" => "Developed with the amazing PageTop framework.", + "app.theme" => "Basic", + "app.lang_negotiation" => "Full", + "app.startup_banner" => "Slant", // [dev] - "dev.pagetop_static_dir" => "", + "dev.pagetop_static_dir" => "", // [log] - "log.enabled" => true, - "log.tracing" => "Info", - "log.rolling" => "Stdout", - "log.path" => "log", - "log.prefix" => "tracing.log", - "log.format" => "Full", + "log.enabled" => true, + "log.tracing" => "Info", + "log.rolling" => "Stdout", + "log.path" => "log", + "log.prefix" => "tracing.log", + "log.format" => "Full", // [server] - "server.bind_address" => "localhost", - "server.bind_port" => 8080, + "server.bind_address" => "localhost", + "server.bind_port" => 8080, ]); // **< Settings >*********************************************************************************** @@ -84,11 +83,6 @@ pub struct App { /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// *"Starwars"*. pub startup_banner: StartupBanner, - /// Activa la página de bienvenida de PageTop. - /// - /// Si está activada, se instala la extensión [`Welcome`](crate::base::extension::Welcome), que - /// ofrece una página de bienvenida predefinida en `"/"`. - pub welcome: bool, /// Modo de ejecución, dado por la variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no /// está definido. pub run_mode: String, diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 2a4f26ed..9d0fb688 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -13,7 +13,7 @@ use crate::{AutoDefault, CowStr}; /// /// > **Nota** /// > Los archivos de los iconos deben estar disponibles en el servidor web de la aplicación. Pueden -/// > servirse usando [`static_files_service!`](crate::static_files_service). +/// > servirse usando [`serve_static_files!`](crate::serve_static_files). /// /// # Ejemplo /// @@ -165,14 +165,12 @@ impl Favicon { } } - /// Centraliza la creación de los elementos ``. - /// - /// - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.). - /// - `href`: URL del recurso. - /// - `sizes`: tamaños opcionales. - /// - `color`: color opcional (solo relevante para `mask-icon`). - /// - /// También infiere automáticamente el tipo MIME (`type`) según la extensión del archivo. + // Crea un elemento para el favicon. Infiere el tipo MIME según la extensión. + // + // - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.). + // - `href`: URL del recurso. + // - `sizes`: tamaños opcionales. + // - `color`: color opcional (solo relevante para `mask-icon`). fn add_icon_item( mut self, icon_rel: &'static str, diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index e348bebc..6af0fd55 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -39,7 +39,7 @@ enum Source { /// /// > **Nota** /// > Los archivos de los scripts deben estar disponibles en el servidor web de la aplicación. -/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service). +/// > Pueden servirse usando [`serve_static_files!`](crate::serve_static_files). /// /// # Ejemplo /// diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index 5941db9d..fb71fd44 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -56,7 +56,7 @@ impl TargetMedia { /// /// > **Nota** /// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden -/// > servirse usando [`static_files_service!`](crate::static_files_service). +/// > servirse usando [`serve_static_files!`](crate::serve_static_files). /// /// # Ejemplo /// diff --git a/src/html/route.rs b/src/html/route.rs index a1efb0d8..ae694857 100644 --- a/src/html/route.rs +++ b/src/html/route.rs @@ -1,4 +1,4 @@ -use crate::{builder_fn, AutoDefault, CowStr}; +use crate::{AutoDefault, CowStr, builder_fn}; use std::fmt; diff --git a/src/lib.rs b/src/lib.rs index 918ecd02..d4712a0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ impl Extension for HelloWorld { } } -async fn hello_world(request: HttpRequest) -> ResultPage { +async fn hello_world(request: HttpRequest) -> Result { Page::new(request) .with_child(Html::with(|_| html! { h1 { "Hello World!" } })) .render() diff --git a/src/locale/request.rs b/src/locale/request.rs index 6f3af13d..53e4e032 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -1,5 +1,5 @@ use crate::global; -use crate::service::HttpRequest; +use crate::web::HttpRequest; use super::{LangId, LanguageIdentifier, Locale}; diff --git a/src/prelude.rs b/src/prelude.rs index 32ce68b7..5e6f7ec1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -15,7 +15,7 @@ pub use crate::include_config; // crate::locale pub use crate::include_locales; // crate::web -pub use crate::static_files_service; +pub use crate::serve_static_files; // crate::core::action pub use crate::actions; // crate::core::theme @@ -36,7 +36,7 @@ pub use crate::locale::*; pub use crate::datetime::*; pub use crate::web; -pub use crate::web::{HttpRequest, IntoResponse, Json, Path, Query, Router}; +pub use crate::web::{HttpRequest, Router}; pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; @@ -45,7 +45,7 @@ pub use crate::core::component::*; pub use crate::core::extension::*; pub use crate::core::theme::*; -pub use crate::response::{page::*, redirect::*}; +pub use crate::response::{json::*, page::*, redirect::*}; pub use crate::base::action; pub use crate::base::component::*; diff --git a/src/response/json.rs b/src/response/json.rs index 23b8ab2c..7ef4b402 100644 --- a/src/response/json.rs +++ b/src/response/json.rs @@ -1,4 +1,4 @@ -//! Extractor y generador de respuestas JSON (reexporta [`actix_web::web::Json`]). +//! Extractor y generador de respuestas JSON (reexporta [`axum::Json`]). //! //! # Uso como extractor JSON //! @@ -11,10 +11,10 @@ //! struct NuevoUsuario { nombre: String, email: String } //! //! /// Manejador configurado para la ruta POST "/usuarios". -//! async fn crear_usuario(payload: Json) -> HttpResponse { +//! async fn crear_usuario(payload: Json) -> web::http::StatusCode { //! // `payload` ya es `NuevoUsuario`; si la deserialización falla, -//! // devolverá automáticamente 400 Bad Request con un cuerpo JSON que describe el error. -//! HttpResponse::Ok().finish() +//! // devolverá automáticamente 400 Bad Request. +//! web::http::StatusCode::OK //! } //! ``` //! @@ -36,4 +36,4 @@ //! `Json` funciona con cualquier tipo que implemente `serde::Serialize` (para respuestas) y/o //! `serde::Deserialize` (para peticiones). -pub use actix_web::web::Json; +pub use axum::Json; diff --git a/src/response/page.rs b/src/response/page.rs index d8bd4b16..2376fd39 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -16,18 +16,16 @@ mod error; pub use error::ErrorPage; -pub use actix_web::Result as ResultPage; - use crate::base::action; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; -use crate::html::{html, Markup, DOCTYPE}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Attr, AttrId}; use crate::html::{Classes, ClassesOp}; +use crate::html::{DOCTYPE, Markup, html}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; -use crate::service::HttpRequest; -use crate::{builder_fn, AutoDefault}; +use crate::web::HttpRequest; +use crate::{AutoDefault, builder_fn}; // **< ReservedRegion >***************************************************************************** @@ -227,8 +225,8 @@ impl Page { /// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos /// `lang` y `dir` en la etiqueta ``. /// 8. Compone el documento HTML completo (``, ``, ``, ``) y - /// devuelve un [`ResultPage`] con el [`Markup`] final. - pub fn render(&mut self) -> ResultPage { + /// devuelve un [`Result`] con el [`Markup`] final. + pub fn render(&mut self) -> Result { // Acciones específicas del tema antes de renderizar el . self.context.theme().before_render_page_body(self); diff --git a/src/response/page/error.rs b/src/response/page/error.rs index fd9959c2..2c92d02e 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,9 +1,7 @@ use crate::core::component::Contextual; use crate::locale::L10n; -use crate::response::ResponseError; -use crate::service::http::{header::ContentType, StatusCode}; -use crate::service::{HttpRequest, HttpResponse}; use crate::util; +use crate::web::{HttpRequest, IntoResponse, Response, http}; use super::Page; @@ -31,13 +29,9 @@ pub enum ErrorPage { } impl ErrorPage { - /// Función auxiliar para renderizar una página de error genérica usando el tema activo. - /// - /// Construye una [`Page`] a partir de la petición y un prefijo de clave basado en el código de - /// estado (`error`), del que se derivan los textos localizados `error_title`, - /// `error_alert` y `error_help`. - /// - /// Si el renderizado falla, escribe en su lugar el texto plano asociado al código de estado. + // Renderiza una página de error genérica usando el tema activo. Deriva las claves de + // localización del código de estado (`error_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(); @@ -51,7 +45,19 @@ impl ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(&code.to_string()) + f.write_str(code.as_str()) + } + } + + /// Devuelve el código de estado HTTP asociado a la variante de error. + pub fn status_code(&self) -> http::StatusCode { + match self { + ErrorPage::BadRequest(_) => http::StatusCode::BAD_REQUEST, + ErrorPage::AccessDenied(_) => http::StatusCode::FORBIDDEN, + ErrorPage::NotFound(_) => http::StatusCode::NOT_FOUND, + ErrorPage::InternalError(_) => http::StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::ServiceUnavailable(_) => http::StatusCode::SERVICE_UNAVAILABLE, + ErrorPage::GatewayTimeout(_) => http::StatusCode::GATEWAY_TIMEOUT, } } } @@ -69,7 +75,7 @@ impl fmt::Display for ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(&self.status_code().to_string()) + f.write_str(self.status_code().as_str()) } } @@ -80,7 +86,7 @@ impl fmt::Display for ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(&self.status_code().to_string()) + f.write_str(self.status_code().as_str()) } } @@ -96,22 +102,17 @@ impl fmt::Display for ErrorPage { } } -impl ResponseError for ErrorPage { - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()) - .insert_header(ContentType::html()) - .body(self.to_string()) - } - - #[rustfmt::skip] - fn status_code(&self) -> StatusCode { - match self { - ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST, - ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN, - ErrorPage::NotFound(_) => StatusCode::NOT_FOUND, - ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, - ErrorPage::GatewayTimeout(_) => StatusCode::GATEWAY_TIMEOUT, - } +/// Convierte un [`ErrorPage`] en una respuesta HTTP con el código de estado adecuado y el cuerpo +/// HTML generado por el tema activo. +impl IntoResponse for ErrorPage { + fn into_response(self) -> Response { + let status = self.status_code(); + let body = self.to_string(); + ( + status, + [(http::header::CONTENT_TYPE, "text/html; charset=utf-8")], + body, + ) + .into_response() } } diff --git a/src/response/redirect.rs b/src/response/redirect.rs index a3bec0cd..ebe470f8 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -18,12 +18,12 @@ //! //! - **Respuestas especiales**. -use crate::service::HttpResponse; +use crate::web::{IntoResponse, Response, http}; /// Funciones predefinidas para generar respuestas HTTP de redirección. /// -/// Ofrece atajos para construir respuestas con el código de estado apropiado, añade la cabecera -/// `Location` y la cierra con `.finish()`, evitando repetir la misma secuencia en cada controlador. +/// Ofrece atajos para construir respuestas con el código de estado apropiado y la cabecera +/// `Location`, evitando repetir la misma secuencia en cada controlador. pub struct Redirect; impl Redirect { @@ -34,10 +34,12 @@ impl Redirect { /// Emplear cuando un recurso se ha movido de forma definitiva y la URL antigua debe dejar de /// usarse. #[must_use] - pub fn moved(redirect_to_url: &str) -> HttpResponse { - HttpResponse::MovedPermanently() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn moved(redirect_to_url: &str) -> Response { + ( + http::StatusCode::MOVED_PERMANENTLY, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **permanente**. Código de estado **308**. Mantiene método y cuerpo sin cambios. @@ -45,10 +47,12 @@ impl Redirect { /// Indicada para reorganizaciones de un sitio o aplicación web en las que también existen /// métodos distintos de GET (POST, PUT, ...) que no deben degradarse a GET. #[must_use] - pub fn permanent(redirect_to_url: &str) -> HttpResponse { - HttpResponse::PermanentRedirect() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn permanent(redirect_to_url: &str) -> Response { + ( + http::StatusCode::PERMANENT_REDIRECT, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **temporal**. Código de estado **302**. El método GET (y normalmente HEAD) se @@ -57,10 +61,12 @@ impl Redirect { /// Útil cuando un recurso está fuera de servicio de forma imprevista (mantenimiento breve, /// sobrecarga, ...). #[must_use] - pub fn found(redirect_to_url: &str) -> HttpResponse { - HttpResponse::Found() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn found(redirect_to_url: &str) -> Response { + ( + http::StatusCode::FOUND, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **temporal**. Código de estado **303**. Método GET se mantiene tal cual. Los @@ -69,10 +75,12 @@ impl Redirect { /// Se usa típicamente tras un POST o PUT para aplicar el patrón *Post/Redirect/Get*, permite /// recargar la página de resultados sin volver a ejecutar la operación. #[must_use] - pub fn see_other(redirect_to_url: &str) -> HttpResponse { - HttpResponse::SeeOther() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn see_other(redirect_to_url: &str) -> Response { + ( + http::StatusCode::SEE_OTHER, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **temporal**. Código de estado **307**. Conserva método y cuerpo íntegros. @@ -80,10 +88,12 @@ impl Redirect { /// Preferible a [`found`](Self::found) cuando el sitio expone operaciones diferentes de GET que /// deben respetarse durante la redirección. #[must_use] - pub fn temporary(redirect_to_url: &str) -> HttpResponse { - HttpResponse::TemporaryRedirect() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn temporary(redirect_to_url: &str) -> Response { + ( + http::StatusCode::TEMPORARY_REDIRECT, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Respuesta **especial**. Código de estado **304**. Se envía tras una petición condicional, @@ -92,7 +102,7 @@ impl Redirect { /// /// No es una redirección, el cliente debe reutilizar su copia local. #[must_use] - pub fn not_modified() -> HttpResponse { - HttpResponse::NotModified().finish() + pub fn not_modified() -> Response { + http::StatusCode::NOT_MODIFIED.into_response() } } diff --git a/src/web.rs b/src/web.rs index fb8a4883..63916cd3 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,10 +1,9 @@ //! Servidor web y rutas de la aplicación (basado en [Axum](https://docs.rs/axum)). //! //! Define rutas y manejadores: el [`Router`], las operaciones HTTP ([`get`], [`post`], [`put`], -//! [`delete`], [`patch`]), los extractores ([`Path`], [`Query`]), [`Json`] e [`IntoResponse`], y -//! re-exporta el módulo `http` para tipos de bajo nivel como `StatusCode`, `HeaderName` o `Method`. -//! También incluye servicios para gestionar archivos estáticos como [`ServeDir`] y -//! [`ServeEmbedded`]. +//! [`delete`], [`patch`]), los extractores ([`Path`], [`Query`]) e [`IntoResponse`], y re-exporta +//! el módulo `http` para tipos de bajo nivel como `StatusCode`, `HeaderName` o `Method`. También +//! ofrece utilidades para servir archivos estáticos, [`ServeDir`] y [`ServeEmbedded`]. use std::collections::HashMap; use std::convert::Infallible; @@ -12,8 +11,6 @@ use std::task::{Context, Poll}; use axum::body::Body; use axum::extract::FromRequestParts; -use axum::http::request::Parts; -use axum::http::{HeaderMap, Request, Response, StatusCode, Uri}; // Infraestructura del router. pub use axum::Router; @@ -22,11 +19,10 @@ pub use axum::http; // Extractores de petición. pub use axum::extract::{Path, Query}; -// Tipos de respuesta. -pub use axum::Json; -pub use axum::response::IntoResponse; +// Para implementar respuestas. +pub use axum::response::{IntoResponse, Response}; -// Verbos HTTP para registrar rutas. +// Operaciones HTTP para registrar rutas. pub use axum::routing::{delete, get, patch, post, put}; // Servicios para archivos estáticos (disco y embebidos). @@ -45,12 +41,12 @@ pub use tower_http::services::ServeDir; /// [`ErrorPage`](crate::response::page::ErrorPage): /// /// ```rust,ignore -/// async fn my_handler(request: HttpRequest) -> ResultPage { ... } +/// async fn my_handler(request: HttpRequest) -> Result { ... } /// ``` #[derive(Clone, Debug)] pub struct HttpRequest { - uri: Uri, - headers: HeaderMap, + uri: http::Uri, + headers: http::HeaderMap, } impl HttpRequest { @@ -75,7 +71,7 @@ impl HttpRequest { } /// Devuelve las cabeceras HTTP de la petición. - pub fn headers(&self) -> &HeaderMap { + pub fn headers(&self) -> &http::HeaderMap { &self.headers } } @@ -84,7 +80,10 @@ impl FromRequestParts for HttpRequest { type Rejection = Infallible; // Implementa el extractor de Axum para poder declarar `HttpRequest` como parámetro. - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + async fn from_request_parts( + parts: &mut http::request::Parts, + _state: &S, + ) -> Result { Ok(HttpRequest { uri: parts.uri.clone(), headers: parts.headers.clone(), @@ -94,13 +93,14 @@ impl FromRequestParts for HttpRequest { // **< ServeEmbedded >****************************************************************************** -/// Permite servir archivos estáticos embebidos en el binario. +/// Servicio para archivos estáticos embebidos en el binario. /// -/// Creado por la macro [`crate::static_files_service!`] cuando se pide servir recursos embebidos. -/// Los recursos se indexan por ruta relativa sin la barra inicial (p. ej. `"css/style.css"`). Si se -/// solicita la raíz o un directorio, devuelve `index.html` si existe. +/// Creado por la macro [`serve_static_files!`](crate::serve_static_files) en los modos que incluyen +/// recursos embebidos. Estos recursos se identifican por su ruta relativa sin la barra inicial +/// (p. ej. `"css/style.css"`). Si se solicita la raíz o una ruta que termina en `/`, el servicio +/// devuelve el `index.html` raíz si existe; no busca por subdirectorio. /// -/// Es [`Clone`] para clonar el servicio por petición, pero internamente comparte el mapa de +/// Implementa [`Clone`] para clonar el servicio por petición, pero internamente comparte el mapa de /// recursos con un [`Arc`](std::sync::Arc) para evitar copias innecesarias. #[derive(Clone)] pub struct ServeEmbedded { @@ -116,8 +116,8 @@ impl ServeEmbedded { } } -impl tower::Service> for ServeEmbedded { - type Response = Response; +impl tower::Service> for ServeEmbedded { + type Response = http::Response; type Error = Infallible; type Future = std::future::Ready>; @@ -125,7 +125,7 @@ impl tower::Service> for ServeEmbedded { Poll::Ready(Ok(())) } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: http::Request) -> Self::Future { use axum::http::header; // Axum elimina el prefijo de montaje: la ruta restante puede o no comenzar con '/'. @@ -141,12 +141,12 @@ impl tower::Service> for ServeEmbedded { }); let response = match resource { - Some(r) => Response::builder() + Some(r) => http::Response::builder() .header(header::CONTENT_TYPE, r.mime_type) .body(Body::from(r.data)) .unwrap(), - None => Response::builder() - .status(StatusCode::NOT_FOUND) + None => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) .body(Body::empty()) .unwrap(), }; @@ -155,23 +155,26 @@ impl tower::Service> for ServeEmbedded { } } -// **< static_files_service! >********************************************************************** +// **< serve_static_files! >************************************************************************ -/// Configura un servicio web para publicar archivos estáticos. +/// Configura el servidor web para publicar archivos estáticos. /// -/// La macro añade rutas al [`Router`] de Axum pasado como primer argumento y ofrece tres modos: +/// La macro añade rutas al [`Router`] del primer argumento usando uno de los tres modos posibles: /// -/// - **Sistema de ficheros o embebido** (`[$path, $bundle]`): intenta servir desde `$path`; si es -/// vacío, no existe o no es un directorio, usa el conjunto de recursos `$bundle` embebido. -/// - **Sólo embebido** (`[$bundle]`): sirve siempre desde el conjunto de recursos embebido. -/// - **Sólo sistema de ficheros** (`$path`): sin corchetes, sirve únicamente desde disco si existe. +/// - **Sistema de ficheros o embebido** (`[$dir, $bundle]`): intenta servir los archivos desde el +/// directorio `$dir`; si está vacío, no existe o no es un directorio, usa el conjunto de recursos +/// `$bundle` embebido. +/// - **Sólo embebido** (`[$bundle]`): sirve siempre desde el conjunto de recursos embebido en el +/// binario. +/// - **Sólo sistema de ficheros** (`$dir`): sin corchetes, sirve únicamente desde el directorio si +/// existe. /// /// # Argumentos /// -/// * `$router` — Variable mutable de tipo [`Router`] donde registrar el servicio. -/// * `$path` — Ruta al directorio local con los archivos estáticos. -/// * `$bundle` — Nombre del conjunto de recursos embebidos generado por `build.rs`. -/// * `$route` — Ruta URL base desde la que se servirán los archivos. +/// * `$router` - Variable de tipo [`Router`] donde registrar las rutas. +/// * `$dir` - Ruta al directorio local con los archivos estáticos. +/// * `$bundle` - Nombre del conjunto de recursos embebidos generado por `build.rs`. +/// * `$path` - Prefijo URL bajo el que se publicarán los archivos. /// /// # Ejemplos /// @@ -180,92 +183,99 @@ impl tower::Service> for ServeEmbedded { /// pub struct MyExtension; /// /// impl Extension for MyExtension { -/// fn configure_router(&self, mut router: Router) -> Router { +/// fn configure_router(&self, router: Router) -> Router { /// // Forma 1) Sistema de ficheros o embebido. -/// static_files_service!(router, ["/var/www/static", assets] => "/public"); +/// serve_static_files!(router, ["/var/www/static", assets] => "/public"); /// /// // Forma 2) Siempre embebido. -/// static_files_service!(router, [assets] => "/public"); +/// serve_static_files!(router, [assets] => "/public"); /// /// // Forma 3) Sólo sistema de ficheros (no requiere `assets`). -/// static_files_service!(router, "/var/www/static" => "/public"); +/// serve_static_files!(router, "/var/www/static" => "/public"); /// /// router /// } /// } /// ``` #[macro_export] -macro_rules! static_files_service { +macro_rules! serve_static_files { // Forma 1: primero intenta servir desde el sistema de ficheros; si falla, sirve embebido. - ( $router:ident, [$path:expr, $bundle:ident] => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "static_files_service", - mode = "filesystem_or_embedded", - route = $route, - ); - let _guard = span.enter(); - let mut served_from_fs = false; - if !::std::path::Path::new(&$path).as_os_str().is_empty() { - if let Ok(absolute) = $crate::util::resolve_absolute_dir($path) { - $router = $router.nest_service($route, $crate::web::ServeDir::new(absolute)); - served_from_fs = true; + ( $router:ident, [$dir:expr, $bundle:ident] => $path:expr $(,)? ) => { + let $router = { + let _span = $crate::trace::debug_span!( + "serve_static_files", + mode = "filesystem_or_embedded", + route = $path, + ) + .entered(); + let mut __r = $router; + let mut served_from_fs = false; + if !::std::path::Path::new(&$dir).as_os_str().is_empty() { + if let Ok(absolute) = $crate::util::resolve_absolute_dir($dir) { + __r = __r.nest_service($path, $crate::web::ServeDir::new(absolute)); + served_from_fs = true; + } } - } - if !served_from_fs { + if !served_from_fs { + $crate::util::paste! { + mod [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + __r = __r.nest_service( + $path, + $crate::web::ServeEmbedded::new( + []::$bundle(), + ), + ); + } + } + __r + }; + }; + // Forma 2: sirve siempre embebido. + ( $router:ident, [$bundle:ident] => $path:expr $(,)? ) => { + let $router = { + let _span = $crate::trace::debug_span!( + "serve_static_files", + mode = "embedded_only", + route = $path, + ) + .entered(); $crate::util::paste! { mod [] { include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); } - $router = $router.nest_service( - $route, + $router.nest_service( + $path, $crate::web::ServeEmbedded::new( []::$bundle(), ), - ); + ) } - } - }}; - // Forma 2: sirve siempre embebido. - ( $router:ident, [$bundle:ident] => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "static_files_service", - mode = "embedded_only", - route = $route, - ); - let _guard = span.enter(); - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - $router = $router.nest_service( - $route, - $crate::web::ServeEmbedded::new( - []::$bundle(), - ), - ); - } - }}; + }; + }; // Forma 3: intenta servir desde el sistema de ficheros. - ( $router:ident, $path:expr => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "static_files_service", - mode = "filesystem_only", - route = $route, - ); - let _guard = span.enter(); - match $crate::util::resolve_absolute_dir($path) { - Ok(absolute) => { - $router = $router.nest_service($route, $crate::web::ServeDir::new(absolute)); + ( $router:ident, $dir:expr => $path:expr $(,)? ) => { + let $router = { + let _span = $crate::trace::debug_span!( + "serve_static_files", + mode = "filesystem_only", + route = $path, + ) + .entered(); + match $crate::util::resolve_absolute_dir($dir) { + Ok(absolute) => $router.nest_service($path, $crate::web::ServeDir::new(absolute)), + Err(e) => { + $crate::trace::warn!( + "Static dir not found or invalid for route `{}`: {} ({e})", + $path, + $dir, + ); + $router + } } - Err(e) => { - $crate::trace::warn!( - "Static dir not found or invalid for route `{}`: {:?} ({e})", - $route, - $path, - ); - } - } - }}; + }; + }; } // **< Utilidades de test >************************************************************************* @@ -275,8 +285,7 @@ macro_rules! static_files_service { pub mod test { use axum::Router; use axum::body::Body; - use axum::http::{Method, Request}; - use axum::response::Response; + use axum::http; use tower::ServiceExt; /// Devuelve el router tal como se recibe, listo para usarse en pruebas de integración. @@ -286,7 +295,7 @@ pub mod test { /// Constructor de peticiones HTTP para pruebas. pub struct TestRequest { - method: Method, + method: http::Method, uri: String, } @@ -294,7 +303,7 @@ pub mod test { /// Crea una petición GET. pub fn get() -> Self { Self { - method: Method::GET, + method: http::Method::GET, uri: "/".to_owned(), } } @@ -302,7 +311,7 @@ pub mod test { /// Crea una petición POST. pub fn post() -> Self { Self { - method: Method::POST, + method: http::Method::POST, uri: "/".to_owned(), } } @@ -314,8 +323,8 @@ pub mod test { } /// Construye la petición HTTP de Axum (para enviar al router en tests de integración). - pub fn to_request(self) -> Request { - Request::builder() + pub fn to_request(self) -> http::Request { + http::Request::builder() .method(self.method) .uri(self.uri) .body(Body::empty()) @@ -334,7 +343,7 @@ pub mod test { } /// Envía una petición al router y devuelve la respuesta. - pub async fn send_request(router: &Router, req: Request) -> Response { + pub async fn send_request(router: &Router, req: http::Request) -> http::Response { router.clone().oneshot(req).await.unwrap() } }