diff --git a/Cargo.toml b/Cargo.toml index 48bc600c..269f752f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,63 +1,3 @@ -[package] -name = "pagetop" -version = "0.5.0" -edition = "2021" - -description = """ - Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables. -""" -categories = ["web-programming::http-server"] -keywords = ["pagetop", "web", "framework", "frontend", "ssr"] - -repository.workspace = true -homepage.workspace = true -license.workspace = true -authors.workspace = true - -[dependencies] -chrono = "0.4" -colored = "3.1" -config = { version = "0.15", default-features = false, features = ["toml"] } -figlet-rs = "1.0" -getter-methods = "2.0" -itoa = "1.0" -indexmap = "2.14" -parking_lot = "0.12" -substring = "1.4" -terminal_size = "0.4" - -tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } -tracing-actix-web = "0.7" - -fluent-templates = "0.14" -unic-langid = { version = "0.9", features = ["macros"] } - -actix-web = { workspace = true, default-features = true } -actix-session = { version = "0.11", features = ["cookie-session"] } -actix-web-files = { package = "actix-files", version = "0.6" } - -serde.workspace = true - -pagetop-macros.workspace = true -pagetop-minimal.workspace = true -pagetop-statics.workspace = true - -[features] -default = [] -testing = [] - -[dev-dependencies] -tempfile = "3.27" -serde_json = "1.0" -pagetop-aliner.workspace = true -pagetop-bootsier.workspace = true - -[build-dependencies] -pagetop-build.workspace = true - - [workspace] resolver = "2" members = [ @@ -75,12 +15,45 @@ members = [ [workspace.package] repository = "https://git.cillero.es/manuelcillero/pagetop" homepage = "https://pagetop.cillero.es" +edition = "2024" license = "MIT OR Apache-2.0" authors = ["Manuel Cillero "] [workspace.dependencies] -actix-web = { version = "4.13", default-features = false } +async-trait = "0.1" +axum = { version = "0.8" } +change-detection = "1.2" +chrono = "0.4" +colored = "3.1" +concat-string = "1.0" +config = { version = "0.15", default-features = false, features = ["toml"] } +figlet-rs = "1.0" +fluent-templates = "0.14" +getter-methods = "2.0" +grass = "0.13" +indexmap = "2.14" +indoc = "2.0" +itoa = "1.0" +mime_guess = "2.0" +parking_lot = "0.12" +pastey = "0.2" +path-slash = "0.2" +proc-macro2 = "1.0" +proc-macro2-diagnostics = { version = "0.10", default-features = false } +quote = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +tempfile = "3.27" +terminal_size = "0.4" +tokio = { version = "1", features = ["full"] } +tower = { version = "0.5", features = ["util"] } +tower-http = { version = "0.6", features = ["fs"] } +tracing = "0.1" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } +unic-langid = { version = "0.9", features = ["macros"] } +url = "2.5" # Helpers pagetop-build = { version = "0.3", path = "helpers/pagetop-build" } pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" } @@ -92,3 +65,65 @@ pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" } pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" } # PageTop pagetop = { version = "0.5", path = "." } + +[workspace.dependencies.sea-orm] +version = "1.1" +features = ["debug-print", "macros", "runtime-tokio-native-tls"] +default-features = false + +[workspace.dependencies.sea-schema] +version = "0.16" + + +[package] +name = "pagetop" +version = "0.5.0" + +description = """ + Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables. +""" +categories = ["web-programming::http-server"] +keywords = ["pagetop", "web", "framework", "frontend", "ssr"] + +repository.workspace = true +homepage.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[features] +default = [] +testing = [] + +[dependencies] +axum.workspace = true +chrono.workspace = true +colored.workspace = true +config.workspace = true +figlet-rs.workspace = true +fluent-templates.workspace = true +getter-methods.workspace = true +indexmap.workspace = true +itoa.workspace = true +parking_lot.workspace = true +pagetop-macros.workspace = true +pagetop-minimal.workspace = true +pagetop-statics.workspace = true +serde.workspace = true +terminal_size.workspace = true +tokio.workspace = true +tower.workspace = true +tower-http.workspace = true +tracing.workspace = true +tracing-appender.workspace = true +tracing-subscriber.workspace = true +unic-langid.workspace = true + +[dev-dependencies] +pagetop-aliner.workspace = true +pagetop-bootsier.workspace = true +serde_json.workspace = true +tempfile.workspace = true + +[build-dependencies] +pagetop-build.workspace = true 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/Cargo.toml b/extensions/pagetop-aliner/Cargo.toml index 00deda3e..d2828b3f 100644 --- a/extensions/pagetop-aliner/Cargo.toml +++ b/extensions/pagetop-aliner/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-aliner" version = "0.1.0" -edition = "2021" description = """ Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML @@ -11,11 +10,15 @@ keywords = ["pagetop", "theme", "css"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true [dependencies] pagetop.workspace = true +[dev-dependencies] +tokio.workspace = true + [build-dependencies] pagetop-build.workspace = true 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 e88a9142..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,20 +109,21 @@ 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 } } impl Theme for Aliner { fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/normalize.css") + StyleSheet::from("/pagetop/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/basic.css") + StyleSheet::from("/pagetop/css/basic.css") .with_version(PAGETOP_VERSION) .with_weight(-99), )) diff --git a/extensions/pagetop-bootsier/Cargo.toml b/extensions/pagetop-bootsier/Cargo.toml index 6e6fc66b..44b6d248 100644 --- a/extensions/pagetop-bootsier/Cargo.toml +++ b/extensions/pagetop-bootsier/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-bootsier" version = "0.1.1" -edition = "2021" description = """ Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles. @@ -11,6 +10,7 @@ keywords = ["pagetop", "theme", "bootstrap", "css", "js"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -18,5 +18,8 @@ authors.workspace = true pagetop.workspace = true serde.workspace = true +[dev-dependencies] +tokio.workspace = true + [build-dependencies] pagetop-build.workspace = true 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/classes/layout.rs b/extensions/pagetop-bootsier/src/theme/classes/layout.rs index ee403a88..1438b210 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/layout.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::attrs::{ScaleSize, Side}; use crate::theme::BreakPoint; +use crate::theme::attrs::{ScaleSize, Side}; // **< Margin >************************************************************************************* diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index ca15c635..b70fed65 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; /// Componente para crear un **menú desplegable** ([`dropdown`]). /// diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs index 60d7120d..18ab908f 100644 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; /// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*). /// diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index 997b7c45..68cce931 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -2,8 +2,8 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; use std::fmt; diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 92736586..7d51e9c9 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -2,8 +2,8 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; // **< Item >*************************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index 781e1d09..81b32783 100644 --- a/extensions/pagetop-bootsier/src/theme/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; /// Componente para crear un **área de texto** de formulario. /// 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/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index ef5a6fe9..43386baf 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; // **< ItemKind >*********************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index ccd97e90..096ec87a 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_OFFCANVAS: &str = "offcanvas"; diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs index 764627e4..a2c014b8 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; /// Componente para crear un **panel lateral deslizante** ([`offcanvas`]). /// diff --git a/extensions/pagetop-seaorm/Cargo.toml b/extensions/pagetop-seaorm/Cargo.toml index 66034137..6e2b6fc7 100644 --- a/extensions/pagetop-seaorm/Cargo.toml +++ b/extensions/pagetop-seaorm/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-seaorm" version = "0.0.4" -edition = "2021" description = """ Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales. @@ -11,6 +10,7 @@ keywords = ["pagetop", "database", "sql", "orm", "ssr"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -20,17 +20,10 @@ postgres = ["sea-orm/sqlx-postgres"] sqlite = ["sea-orm/sqlx-sqlite"] [dependencies] +async-trait.workspace = true pagetop.workspace = true +sea-orm.workspace = true +sea-schema.workspace = true serde.workspace = true - -async-trait = "0.1" -futures = "0.3" -url = "2.5" - -[dependencies.sea-orm] -version = "1.1" -features = ["debug-print", "macros", "runtime-async-std-native-tls"] -default-features = false - -[dependencies.sea-schema] -version = "0.16" +tokio.workspace = true +url.workspace = true diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml index aa37e1af..a06bc9ca 100644 --- a/helpers/pagetop-build/Cargo.toml +++ b/helpers/pagetop-build/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-build" version = "0.3.2" -edition = "2021" description = """ Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el @@ -12,9 +11,10 @@ keywords = ["pagetop", "build", "assets", "resources", "static"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -grass = "0.13" +grass.workspace = true pagetop-statics.workspace = true 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/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index b34d2ec1..13b5d387 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-macros" version = "0.3.0" -edition = "2021" description = """ Una colección de macros que mejoran la experiencia de desarrollo con PageTop. @@ -11,6 +10,7 @@ keywords = ["pagetop", "macros", "proc-macros", "codegen"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -18,7 +18,7 @@ authors.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1.0" -proc-macro2-diagnostics = { version = "0.10", default-features = false } -quote = "1.0" -syn = { version = "2.0", features = ["full", "extra-traits"] } +proc-macro2.workspace = true +proc-macro2-diagnostics.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/helpers/pagetop-minimal/Cargo.toml b/helpers/pagetop-minimal/Cargo.toml index 39b7d10d..dfb37a9d 100644 --- a/helpers/pagetop-minimal/Cargo.toml +++ b/helpers/pagetop-minimal/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-minimal" version = "0.1.0" -edition = "2021" description = """ Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones @@ -12,10 +11,11 @@ keywords = ["pagetop", "build", "assets", "resources", "static"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -concat-string = "1.0" -indoc = "2.0" -pastey = "0.2" +concat-string.workspace = true +indoc.workspace = true +pastey.workspace = true diff --git a/helpers/pagetop-statics/Cargo.toml b/helpers/pagetop-statics/Cargo.toml index da967c31..503511eb 100644 --- a/helpers/pagetop-statics/Cargo.toml +++ b/helpers/pagetop-statics/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-statics" version = "0.1.3" -edition = "2021" description = """ Librería para automatizar la recopilación de recursos estáticos en PageTop. @@ -11,6 +10,7 @@ keywords = ["pagetop", "build", "static", "resources", "file"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -19,15 +19,11 @@ default = ["change-detection"] sort = [] [dependencies] -change-detection = { version = "1.2", optional = true } -mime_guess = "2.0" -path-slash = "0.2" - -actix-web.workspace = true -derive_more = "0.99.17" -futures-util = { version = "0.3", default-features = false, features = ["std"] } +change-detection = { workspace = true, optional = true } +mime_guess.workspace = true +path-slash.workspace = true [build-dependencies] -change-detection = { version = "1.2", optional = true } -mime_guess = "2.0" -path-slash = "0.2" +change-detection = { workspace = true, optional = true } +mime_guess.workspace = true +path-slash.workspace = true diff --git a/helpers/pagetop-statics/build.rs b/helpers/pagetop-statics/build.rs index fcd009c9..3cbd6706 100644 --- a/helpers/pagetop-statics/build.rs +++ b/helpers/pagetop-statics/build.rs @@ -13,7 +13,7 @@ use resource_dir::resource_dir; mod sets { include!("src/sets.rs"); } -use sets::{generate_resources_sets, SplitByCount}; +use sets::{SplitByCount, generate_resources_sets}; use std::{env, path::Path}; diff --git a/helpers/pagetop-statics/src/resource_dir.rs b/helpers/pagetop-statics/src/resource_dir.rs index 805e1ed4..41c29829 100644 --- a/helpers/pagetop-statics/src/resource_dir.rs +++ b/helpers/pagetop-statics/src/resource_dir.rs @@ -1,4 +1,4 @@ -use super::sets::{generate_resources_sets, SplitByCount}; +use super::sets::{SplitByCount, generate_resources_sets}; use std::{ env, io, path::{Path, PathBuf}, diff --git a/helpers/pagetop-statics/src/sets.rs b/helpers/pagetop-statics/src/sets.rs index 1d9299df..5e09f1ff 100644 --- a/helpers/pagetop-statics/src/sets.rs +++ b/helpers/pagetop-statics/src/sets.rs @@ -5,8 +5,8 @@ use std::{ }; use super::resource::{ - collect_resources, generate_function_end, generate_function_header, generate_resource_insert, - generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME, + DEFAULT_VARIABLE_NAME, collect_resources, generate_function_end, generate_function_header, + generate_resource_insert, generate_uses, generate_variable_header, generate_variable_return, }; /// Defines the split strategie. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..5d6e629c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +edition = "2024" +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Auto" + +# Heurísticas por defecto: evitar reformateo agresivo +use_small_heuristics = "Default" diff --git a/src/app.rs b/src/app.rs index 6a266edc..4a009fb7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,25 +5,21 @@ mod figfont; use crate::core::{extension, extension::ExtensionRef}; use crate::html::Markup; use crate::locale::Locale; -use crate::response::page::{ErrorPage, ResultPage}; -use crate::service::HttpRequest; -use crate::{global, service, trace, PAGETOP_VERSION}; - -use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; -use actix_session::storage::CookieSessionStore; -use actix_session::SessionMiddleware; - -use substring::Substring; +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 configuración y puesta en marcha. 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). +/// 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). pub struct Application; impl Default for Application { @@ -33,24 +29,24 @@ impl Default for Application { } impl Application { - /// Crea una instancia de la aplicación. + /// 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) } /// Prepara una instancia de la aplicación a partir de una extensión raíz. /// - /// Esa extensión suele declarar: - /// - /// - Sus propias dependencias (que se habilitarán automáticamente). - /// - Una lista de extensiones que deben deshabilitarse si estuvieran activadas. - /// - /// Esto simplifica el arranque en escenarios complejos. + /// 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)) } - /// Método interno para preparar la aplicación, opcionalmente con una extensión. + // Secuencia de arranque común a new() y prepare(). fn internal_prepare(root_extension: Option) -> Self { // Al arrancar muestra una cabecera para la aplicación. Self::show_banner(); @@ -73,10 +69,10 @@ impl Application { Self } - /// Muestra una cabecera para la aplicación basada en la configuración. + // Muestra la cabecera de arranque si está habilitada en la configuración. fn show_banner() { use colored::Colorize; - use terminal_size::{terminal_size, Width}; + use terminal_size::{Width, terminal_size}; if global::SETTINGS.app.startup_banner != global::StartupBanner::Off { // Nombre de la aplicación, ajustado al ancho del terminal si es necesario. @@ -85,8 +81,8 @@ impl Application { if let Some((Width(term_width), _)) = terminal_size() { if term_width >= 80 { let maxlen: usize = ((term_width / 10) - 2).into(); - let mut app = app_name.substring(0, maxlen).to_string(); - if app_name.len() > maxlen { + let mut app: String = app_name.chars().take(maxlen).collect(); + if app_name.chars().count() > maxlen { app = format!("{app}..."); } if let Some(ff) = figfont::FIGFONT.convert(&app) { @@ -103,7 +99,7 @@ impl Application { // Descripción de la aplicación. if !global::SETTINGS.app.description.is_empty() { println!("{}", global::SETTINGS.app.description.cyan()); - }; + } // Versión de PageTop. println!( @@ -114,72 +110,55 @@ impl Application { } } + // Construye el router con las rutas de todas las extensiones habilitadas. + fn build_router() -> Router { + let router = extension::all::configure_routes(Router::new()); + router.fallback(route_not_found) + } + /// Arranca el servidor web de la aplicación. /// - /// Devuelve [`std::io::Error`] si el *socket* no puede enlazarse (por puerto en uso, permisos, - /// etc.). - pub fn run(self) -> Result { - // Genera clave secreta para firmar y verificar cookies. - let secret_key = service::cookie::Key::generate(); - - // Prepara el servidor web. - Ok(service::HttpServer::new(move || { - Self::service_app() - .wrap(tracing_actix_web::TracingLogger::default()) - .wrap( - SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) - .session_lifecycle(match global::SETTINGS.server.session_lifetime { - 0 => SessionLifecycle::BrowserSession(BrowserSession::default()), - _ => SessionLifecycle::PersistentSession( - PersistentSession::default().session_ttl( - service::cookie::time::Duration::seconds( - global::SETTINGS.server.session_lifetime, - ), - ), - ), - }) - .build(), - ) - }) - .bind(format!( + /// 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: + /// + /// ```rust,no_run + /// use pagetop::prelude::*; + /// + /// struct MyApp; + /// + /// impl Extension for MyApp {} + /// + /// #[pagetop::main] + /// async fn main() -> std::io::Result<()> { + /// Application::prepare(&MyApp).run()?.await + /// } + /// ``` + pub fn run(self) -> Result>, Error> { + let addr = format!( "{}:{}", - &global::SETTINGS.server.bind_address, - &global::SETTINGS.server.bind_port - ))? - .run()) + global::SETTINGS.server.bind_address, + global::SETTINGS.server.bind_port + ); + + // Enlaza el puerto de forma síncrona para detectar errores antes del *await*. + 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 + }) } - /// Prepara el servidor web de la aplicación para pruebas. - pub fn test( - self, - ) -> service::App< - impl service::Factory< - service::Request, - Config = (), - Response = service::Response, - Error = service::Error, - InitError = (), - >, - > { - Self::service_app() - } - - /// Configura el servicio web de la aplicación. - fn service_app() -> service::App< - impl service::Factory< - service::Request, - Config = (), - Response = service::Response, - Error = service::Error, - InitError = (), - >, - > { - service::App::new() - .configure(extension::all::configure_services) - .default_service(service::web::route().to(service_not_found)) + /// Devuelve el servidor web configurado para usarlo en pruebas de integración. + pub fn test(self) -> Router { + Self::build_router() } } -async fn service_not_found(request: HttpRequest) -> ResultPage { +async fn route_not_found(request: HttpRequest) -> Result { Err(ErrorPage::NotFound(request)) } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index a7ccb2c4..63902f10 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -114,7 +114,7 @@ impl Component for Intro { fn prepare(&self, cx: &mut Context) -> Result { cx.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION), + StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION), )); if *self.opening() == IntroOpening::PageTop { cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx| 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/base/theme/basic.rs b/src/base/theme/basic.rs index 34f9088b..3e6e99e9 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -13,12 +13,12 @@ impl Extension for Basic { impl Theme for Basic { fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/normalize.css") + StyleSheet::from("/pagetop/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/basic.css") + StyleSheet::from("/pagetop/css/basic.css") .with_version(PAGETOP_VERSION) .with_weight(-99), )) diff --git a/src/config.rs b/src/config.rs index 9b7b43d2..9c687a0f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,11 +7,11 @@ //! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada //! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente. //! -//! //! # Orden de carga //! //! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del -//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación. +//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación. Puedes +//! cambiar esta ubicación mediante la variable de entorno `CONFIG_DIR`. //! //! PageTop carga en este orden, y siempre de forma opcional, los siguientes archivos TOML: //! @@ -42,7 +42,6 @@ //! Los archivos se combinan en el orden anterior, cada archivo sobrescribe a los anteriores en caso //! de conflicto. //! -//! //! # Cómo añadir opciones de configuración a tu código //! //! Añade [*serde*](https://docs.rs/serde) en tu archivo *Cargo.toml* con la *feature* `derive`: @@ -91,7 +90,6 @@ //! //! Las estructuras de configuración son de **sólo lectura** durante la ejecución. //! -//! //! # Usando tus opciones de configuración //! //! ```rust,ignore @@ -131,9 +129,14 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into()); let config_dir = util::resolve_absolute_dir(&dir).unwrap_or_else(|_| PathBuf::from(&dir)); - // Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Si no está definida, se usa - // por defecto DEFAULT_RUN_MODE (p. ej. PAGETOP_RUN_MODE=production). - let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into()); + // Modo de ejecución. Con la *feature* `testing` activa (usada por `cargo ts` y `cargo tw`), se + // fija en "test" en tiempo de compilación, sin manipular el entorno. En caso contrario se lee + // de PAGETOP_RUN_MODE, o se usa DEFAULT_RUN_MODE si la variable no está definida. + let rm = if cfg!(feature = "testing") { + "test".to_string() + } else { + env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into()) + }; Config::builder() // 1. Configuración común para todos los entornos (common.toml). @@ -158,7 +161,7 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// Hay que añadir en nuestra librería el siguiente código: /// /// ```rust,ignore -/// include_config!(SETTINGS: Settings => [ +/// include_config!(SETTINGS_NAME: SettingsType => [ /// "ruta.clave" => valor, /// // ... /// ]); @@ -168,8 +171,8 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// /// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los /// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio. -/// * **`Settings_Type`** es la referencia a la estructura que define los tipos para deserializar la -/// configuración. Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`). +/// * **`SettingsType`** es la estructura que define los tipos para deserializar la configuración. +/// Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`). /// * **Lista de pares** con las claves TOML que requieran valores por defecto. Siguen la notación /// `"seccion.subclave"` para coincidir con el árbol TOML. /// @@ -211,7 +214,7 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p. ej. `[blog]`) para /// evitar colisiones con otras librerías. /// -/// * **Solo lectura**. La variable generada es inmutable durante toda la vida del programa. Para +/// * **Sólo lectura**. La variable generada es inmutable durante toda la vida del programa. Para /// configurar distintos entornos (*dev*, *staging*, *prod*) usa los archivos TOML descritos en la /// documentación de [`config`](crate::config). /// @@ -220,8 +223,8 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// /// # Requisitos /// -/// * Dependencia `serde` con la *feature* `derive`. -/// * Las claves deben coincidir con los campos (*snake case*) de tu estructura `Settings_Type`. +/// * Las claves deben coincidir con los campos (*snake case*) de la estructura de ajustes. +/// * Añade `serde` con la *feature* `derive` en *Cargo.toml*: /// /// ```toml /// [dependencies] @@ -229,10 +232,10 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// ``` #[macro_export] macro_rules! include_config { - ( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { + ( $SETTINGS_NAME:ident : $settings_type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { #[doc = concat!( "Ajustes de configuración y **valores por defecto** para ", - "[`", stringify!($Settings_Type), "`]." + "[`", stringify!($settings_type), "`]." )] #[doc = ""] #[doc = "Valores predeterminados que se aplican en ausencia de configuración:"] @@ -241,17 +244,18 @@ macro_rules! include_config { #[doc = concat!($k, " = ", stringify!($v))] )* #[doc = "```"] - pub static $SETTINGS_NAME: std::sync::LazyLock<$Settings_Type> = + pub static $SETTINGS_NAME: std::sync::LazyLock<$settings_type> = std::sync::LazyLock::new(|| { let mut settings = $crate::config::CONFIG_VALUES.clone(); $( - settings = settings.set_default($k, $v).unwrap(); + settings = settings.set_default($k, $v) + .expect(concat!("Failed to set default for key ", $k)); )* settings .build() - .expect(concat!("Failed to build config for ", stringify!($Settings_Type))) - .try_deserialize::<$Settings_Type>() - .expect(concat!("Error parsing settings for ", stringify!($Settings_Type))) + .expect(concat!("Failed to build config for ", stringify!($settings_type))) + .try_deserialize::<$settings_type>() + .expect(concat!("Error parsing settings for ", stringify!($settings_type))) }); }; } diff --git a/src/core.rs b/src/core.rs index 8a47848e..03e32a94 100644 --- a/src/core.rs +++ b/src/core.rs @@ -30,28 +30,22 @@ impl TypeInfo { } } - /// Extrae un rango de segmentos de `type_name` (tokens separados por `::`). - /// - /// Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta: - /// - /// * Los índices positivos cuentan **desde la izquierda**, empezando en `0`. - /// * Los índices negativos cuentan **desde la derecha**, `-1` es el último. - /// * Si `end` es `None`, el corte llega hasta el último segmento. - /// * Si la selección resulta vacía por índices desordenados o segmento inexistente, se devuelve - /// la cadena vacía. - /// - /// Ejemplos (con `type_name = "alloc::vec::Vec"`): - /// - /// | Llamada | Resultado | - /// |------------------------------|--------------------------| - /// | `partial(..., 0, None)` | `"alloc::vec::Vec"` | - /// | `partial(..., 1, None)` | `"vec::Vec"` | - /// | `partial(..., -1, None)` | `"Vec"` | - /// | `partial(..., 0, Some(-2))` | `"alloc::vec"` | - /// | `partial(..., -5, None)` | `"alloc::vec::Vec"` | - /// - /// La porción devuelta vive tanto como `'static` porque `type_name` es `'static` y sólo se - /// presta. + // Extrae un rango de segmentos de `type_name` (tokens separados por `::`). + // + // Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta: + // + // * Los índices positivos cuentan desde la izquierda, empezando en 0. + // * Los índices negativos cuentan desde la derecha; -1 es el último. + // * Si `end` es `None`, el corte llega hasta el último segmento. + // * Si la selección resulta vacía por índices desordenados o segmento inexistente, devuelve "". + // + // Ejemplos con type_name = "alloc::vec::Vec": + // + // partial(..., 0, None) => "alloc::vec::Vec" + // partial(..., 1, None) => "vec::Vec" + // partial(..., -1, None) => "Vec" + // partial(..., 0, Some(-2)) => "alloc::vec" + // partial(..., -5, None) => "alloc::vec::Vec" fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { let maxlen = type_name.len(); @@ -59,7 +53,7 @@ impl TypeInfo { let mut segments = Vec::new(); let mut segment_start = 0; // Posición inicial del segmento actual. let mut angle_brackets = 0; // Profundidad dentro de '<...>'. - let mut previous_char = '\0'; // Se inicializa a carácter nulo, no hay aún carácter previo. + let mut previous_char = '\0'; // Control, ningún carácter previo aún. for (idx, c) in type_name.char_indices() { match c { diff --git a/src/core/action/list.rs b/src/core/action/list.rs index d60129c1..f47d3d6e 100644 --- a/src/core/action/list.rs +++ b/src/core/action/list.rs @@ -1,7 +1,7 @@ -use crate::core::action::{ActionBox, ActionDispatcher}; -use crate::core::AnyCast; -use crate::trace; use crate::AutoDefault; +use crate::core::AnyCast; +use crate::core::action::{ActionBox, ActionDispatcher}; +use crate::trace; use parking_lot::RwLock; diff --git a/src/core/component/children.rs b/src/core/component/children.rs index 617a783d..bfedec14 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -1,6 +1,6 @@ use crate::core::component::{Component, Context}; -use crate::html::{html, Markup}; -use crate::{builder_fn, AutoDefault, UniqueId}; +use crate::html::{Markup, html}; +use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::Mutex; diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 63b2daad..9ff7251c 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,13 +1,13 @@ +use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef}; -use crate::core::TypeInfo; -use crate::html::{html, Markup, RoutePath}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::html::{Markup, RoutePath, html}; use crate::locale::L10n; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; -use crate::service::HttpRequest; -use crate::{builder_fn, util, CowStr}; +use crate::web::HttpRequest; +use crate::{CowStr, builder_fn, util}; use std::any::Any; use std::cell::Cell; diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index 718e3b37..b7ceaa9a 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -2,7 +2,7 @@ use crate::base::action; use crate::core::component::{ComponentError, Context, Contextual}; use crate::core::theme::ThemeRef; use crate::core::{AnyInfo, TypeInfo}; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; /// Permite clonar un componente. /// diff --git a/src/core/component/error.rs b/src/core/component/error.rs index beb7c8f2..86f9e4aa 100644 --- a/src/core/component/error.rs +++ b/src/core/component/error.rs @@ -1,4 +1,4 @@ -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::{AutoDefault, Getters}; /// Error producido durante el renderizado de un componente. 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/core/theme.rs b/src/core/theme.rs index a8c1f3a4..43649db1 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -28,9 +28,9 @@ //! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas. use crate::core::component::Context; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::locale::L10n; -use crate::{util, AutoDefault}; +use crate::{AutoDefault, util}; // **< Region >************************************************************************************* diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 17f3b391..0b036dd4 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -3,10 +3,10 @@ use crate::core::component::{ChildOp, Component, ComponentError, Context, Contex use crate::core::extension::Extension; use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef}; use crate::global; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::locale::L10n; use crate::response::page::Page; -use crate::service::http::StatusCode; +use crate::web::http::StatusCode; /// Interfaz común que debe implementar cualquier tema de PageTop. /// diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index a2b71ff2..a10e3ecc 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,6 +1,6 @@ use crate::core::component::{Child, ChildOp, Children, Component}; use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef}; -use crate::{builder_fn, AutoDefault, UniqueId}; +use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::RwLock; diff --git a/src/global.rs b/src/global.rs index 8bf753e3..d6bdbc47 100644 --- a/src/global.rs +++ b/src/global.rs @@ -20,28 +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.session_lifetime" => 604_800, + "server.bind_address" => "localhost", + "server.bind_port" => 8080, ]); // **< Settings >*********************************************************************************** @@ -85,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, @@ -116,7 +109,7 @@ pub struct Log { pub enabled: bool, /// Opciones, o combinación de opciones separadas por comas, para filtrar las trazas: *"Error"*, /// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*. - /// Ejemplo: *"Error,actix_server::builder=Info,tracing_actix_web=Debug"*. + /// Ejemplo: *"Error,tower_http=Debug,axum::rejection=trace"*. pub tracing: String, /// Muestra los mensajes de traza en el terminal (*"Stdout"*) o los vuelca en archivos con /// rotación: *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*. @@ -136,8 +129,4 @@ pub struct Server { pub bind_address: String, /// Puerto de escucha del servidor web. pub bind_port: u16, - /// Duración de la cookie de sesión en segundos (p. ej., `604_800` para una semana). - /// - /// El valor `0` indica que la cookie permanecerá activa hasta que se cierre el navegador. - pub session_lifetime: i64, } diff --git a/src/html.rs b/src/html.rs index 21809c08..6020627d 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,7 +1,7 @@ //! HTML en código. mod maud; -pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE}; +pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, display, html, html_private}; mod route; pub use route::RoutePath; diff --git a/src/html/assets.rs b/src/html/assets.rs index fe5f5b7c..80cb3b26 100644 --- a/src/html/assets.rs +++ b/src/html/assets.rs @@ -3,7 +3,7 @@ pub mod javascript; pub mod stylesheet; use crate::core::component::Context; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::{AutoDefault, Weight}; /// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 1a4174bf..9d0fb688 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -1,5 +1,5 @@ use crate::core::component::Context; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::{AutoDefault, CowStr}; /// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. @@ -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 62126895..6af0fd55 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -1,7 +1,7 @@ use crate::core::component::Context; use crate::html::assets::Asset; -use crate::html::{html, Markup, PreEscaped}; -use crate::{util, AutoDefault, CowStr, Weight}; +use crate::html::{Markup, PreEscaped, html}; +use crate::{AutoDefault, CowStr, Weight, util}; /// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador. /// @@ -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 5a6d98c5..fb71fd44 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -1,7 +1,7 @@ use crate::core::component::Context; use crate::html::assets::Asset; -use crate::html::{html, Markup, PreEscaped}; -use crate::{util, AutoDefault, CowStr, Weight}; +use crate::html::{Markup, PreEscaped, html}; +use crate::{AutoDefault, CowStr, Weight, util}; /// Define el origen del recurso CSS y cómo se incluye en el documento. /// @@ -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/attr.rs b/src/html/attr.rs index 61f7252c..8f25a5eb 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -1,5 +1,5 @@ use crate::locale::{L10n, LangId}; -use crate::{builder_fn, AutoDefault}; +use crate::{AutoDefault, builder_fn}; /// Valor opcional para atributos HTML. /// diff --git a/src/html/classes.rs b/src/html/classes.rs index 3465d6b3..2f665c19 100644 --- a/src/html/classes.rs +++ b/src/html/classes.rs @@ -1,4 +1,4 @@ -use crate::{builder_fn, util, AutoDefault, CowStr}; +use crate::{AutoDefault, CowStr, builder_fn, util}; use std::collections::HashSet; diff --git a/src/html/logo.rs b/src/html/logo.rs index d5dcaa0b..7746da7a 100644 --- a/src/html/logo.rs +++ b/src/html/logo.rs @@ -1,7 +1,7 @@ -use crate::core::component::Context; -use crate::html::{html, Markup}; -use crate::locale::L10n; use crate::AutoDefault; +use crate::core::component::Context; +use crate::html::{Markup, html}; +use crate::locale::L10n; /// Representación SVG del **logotipo de PageTop** para incrustar en HTML. /// 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 0213e61e..d4712a0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,12 +53,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! { h1 { "Hello World!" } })) .render() @@ -117,10 +117,10 @@ use std::ops::Deref; /// fn before_render_page_body(&self, page: &mut Page) { /// page /// .alter_assets(AssetsOp::AddStyleSheet( -/// StyleSheet::from("/css/normalize.css").with_version("8.0.1"), +/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1"), /// )) /// .alter_assets(AssetsOp::AddStyleSheet( -/// StyleSheet::from("/css/basic.css").with_version(PAGETOP_VERSION), +/// StyleSheet::from("/pagetop/css/basic.css").with_version(PAGETOP_VERSION), /// )) /// .alter_assets(AssetsOp::AddStyleSheet( /// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")), @@ -132,9 +132,9 @@ use std::ops::Deref; /// referencia a la versión del *crate* que lo usa. pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault}; +pub use pagetop_macros::{AutoDefault, builder_fn, html, main, test}; -pub use pagetop_statics::{resource, StaticResource}; +pub use pagetop_statics::{StaticResource, resource}; pub use getter_methods::Getters; @@ -198,8 +198,8 @@ pub mod datetime; pub mod core; // Respuestas a peticiones web en sus diferentes formatos. pub mod response; -// Gestión del servidor y servicios web. -pub mod service; +// Gestión del servidor y rutas web. +pub mod web; // Reúne acciones, componentes, extensiones y temas predefinidos. pub mod base; // Prepara y ejecuta la aplicación. diff --git a/src/locale/definition.rs b/src/locale/definition.rs index 06a07c49..bffc805c 100644 --- a/src/locale/definition.rs +++ b/src/locale/definition.rs @@ -1,7 +1,7 @@ use crate::{global, trace}; use super::languages::LANGUAGES; -use super::{langid, LanguageIdentifier}; +use super::{LanguageIdentifier, langid}; use std::sync::LazyLock; diff --git a/src/locale/l10n.rs b/src/locale/l10n.rs index af5e9535..e75e103d 100644 --- a/src/locale/l10n.rs +++ b/src/locale/l10n.rs @@ -1,5 +1,5 @@ use crate::html::{Markup, PreEscaped}; -use crate::{include_locales, AutoDefault, CowStr}; +use crate::{AutoDefault, CowStr, include_locales}; use super::{LangId, Locale}; diff --git a/src/locale/languages.rs b/src/locale/languages.rs index f1962a14..cda4483d 100644 --- a/src/locale/languages.rs +++ b/src/locale/languages.rs @@ -1,6 +1,6 @@ use crate::util; -use super::{langid, LanguageIdentifier}; +use super::{LanguageIdentifier, langid}; use std::collections::HashMap; use std::sync::LazyLock; 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 818bfc91..5e6f7ec1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -14,8 +14,8 @@ pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight} pub use crate::include_config; // crate::locale pub use crate::include_locales; -// crate::service -pub use crate::static_files_service; +// crate::web +pub use crate::serve_static_files; // crate::core::action pub use crate::actions; // crate::core::theme @@ -35,8 +35,8 @@ pub use crate::locale::*; pub use crate::datetime::*; -pub use crate::service; -pub use crate::service::{HttpMessage, HttpRequest, HttpResponse}; +pub use crate::web; +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::{json::*, page::*, redirect::*, ResponseError}; +pub use crate::response::{json::*, page::*, redirect::*}; pub use crate::base::action; pub use crate::base::component::*; diff --git a/src/response.rs b/src/response.rs index 4078d420..55150b71 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,7 +1,5 @@ //! Respuestas a las peticiones web en sus diferentes formatos. -pub use actix_web::ResponseError; - pub mod page; pub mod json; 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/service.rs b/src/service.rs deleted file mode 100644 index 10665413..00000000 --- a/src/service.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Gestión del servidor y servicios web (con [Actix Web](https://docs.rs/actix-web)). - -pub use actix_session::Session; -pub use actix_web::body::BoxBody; -pub use actix_web::dev::Server; -pub use actix_web::dev::ServiceFactory as Factory; -pub use actix_web::dev::ServiceRequest as Request; -pub use actix_web::dev::ServiceResponse as Response; -pub use actix_web::{cookie, http, rt, web}; -pub use actix_web::{App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer}; -pub use actix_web_files::Files as ActixFiles; - -pub use pagetop_statics::ResourceFiles; - -#[doc(hidden)] -pub use actix_web::test; - -// **< static_files_service! >********************************************************************** - -/// Configura un servicio web para publicar archivos estáticos. -/// -/// La macro ofrece tres modos para configurar el servicio: -/// -/// - **Sistema de ficheros o embebido** (`[$path, $bundle]`): trata de servir los archivos desde -/// `$path`; y si es una cadena vacía, no existe o no es un directorio, entonces usará el conjunto -/// de recursos `$bundle` integrado en el binario. -/// - **Sólo embebido** (`[$bundle]`): sirve siempre desde el conjunto de recursos `$bundle` -/// integrado en el binario. -/// - **Sólo sistema de ficheros** (`$path`): sin usar corchetes, sirve únicamente desde el sistema -/// de ficheros si existe; en otro caso no registra el servicio. -/// -/// # Argumentos -/// -/// * `$scfg` - Instancia de [`ServiceConfig`](crate::service::web::ServiceConfig) donde aplicar la -/// configuración. -/// * `$path` - Ruta al directorio local con los archivos estáticos. -/// * `$bundle` - Nombre del conjunto de recursos que esta macro integra en el binario. -/// * `$route` - Ruta URL base desde la que se servirán los archivos. -/// -/// # Ejemplos -/// -/// ```rust,ignore -/// # use pagetop::prelude::*; -/// pub struct MyExtension; -/// -/// impl Extension for MyExtension { -/// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { -/// // Forma 1) Sistema de ficheros o embebido. -/// static_files_service!(scfg, ["/var/www/static", assets] => "/public"); -/// -/// // Forma 2) Siempre embebido. -/// static_files_service!(scfg, [assets] => "/public"); -/// -/// // Forma 3) Sólo sistema de ficheros (no requiere `assets`). -/// static_files_service!(scfg, "/var/www/static" => "/public"); -/// } -/// } -/// ``` -#[macro_export] -macro_rules! static_files_service { - // Forma 1: primero intenta servir desde el sistema de ficheros; si falla, sirve embebido. - ( $scfg:ident, [$path:expr, $bundle:ident] => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "Configuring static files (file system or embedded)", - mode = "fs_or_embedded", - route = $route, - ); - let _ = span.in_scope(|| { - let mut serve_embedded: bool = true; - if !::std::path::Path::new(&$path).as_os_str().is_empty() { - if let Ok(absolute) = $crate::util::resolve_absolute_dir($path) { - $scfg.service($crate::service::ActixFiles::new($route, absolute)); - serve_embedded = false; - } - } - if serve_embedded { - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - $scfg.service($crate::service::ResourceFiles::new( - $route, - []::$bundle(), - )); - } - } - }); - }}; - // Forma 2: sirve siempre embebido. - ( $scfg:ident, [$bundle:ident] => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "Configuring static files (using embedded only)", - mode = "embedded", - route = $route, - ); - let _ = span.in_scope(|| { - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - $scfg.service($crate::service::ResourceFiles::new( - $route, - []::$bundle(), - )); - } - }); - }}; - // Forma 3: intenta servir desde el sistema de ficheros. - ( $scfg:ident, $path:expr => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "Configuring static files (file system only)", - mode = "fs", - route = $route, - ); - let _ = span.in_scope(|| match $crate::util::resolve_absolute_dir($path) { - Ok(absolute) => { - $scfg.service($crate::service::ActixFiles::new($route, absolute)); - } - Err(e) => { - $crate::trace::warn!( - "Static dir not found or invalid for route `{}`: {:?} ({e})", - $route, - $path, - ); - } - }); - }}; -} diff --git a/src/util.rs b/src/util.rs index 6ea4b70d..5c1fff49 100644 --- a/src/util.rs +++ b/src/util.rs @@ -58,14 +58,15 @@ pub enum NormalizeAsciiError { /// # use pagetop::util; /// assert_eq!(util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), "foo bar cli"); /// ``` -pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsciiError> { +pub fn normalize_ascii(input: &str) -> Result, NormalizeAsciiError> { let bytes = input.as_bytes(); if bytes.is_empty() { return Err(NormalizeAsciiError::IsEmpty); } - let mut start = 0usize; - let mut end = 0usize; + // Primera pasada, determina si se necesita asignación y calcula los límites del contenido. + let mut start = 0; + let mut end = 0; let mut needs_alloc = false; let mut needs_alloc_ws = false; @@ -110,6 +111,7 @@ pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsci return Ok(Cow::Borrowed(slice)); } + // Segunda pasada, construye la cadena normalizada. let mut output = String::with_capacity(slice.len()); let mut prev_sep = true; @@ -132,8 +134,8 @@ pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsci /// /// - Devuelve `Some(Cow)` si la entrada es válida ASCII (normalizada a minúsculas). /// - Devuelve `Some(Cow::Borrowed(""))` si la entrada es `""` o queda vacía tras recortar. -/// - Devuelve `None` si la entrada contiene bytes non-ASCII; y emite un `trace::debug!` con el -/// campo `target`. +/// - Devuelve `None` si la entrada contiene bytes no ASCII; y emite un `trace::debug!` con el campo +/// `target`. #[inline] pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option> { match normalize_ascii(input) { @@ -171,15 +173,25 @@ pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Opt /// println!("{:#?}", util::resolve_absolute_dir("/var/www")); /// ``` pub fn resolve_absolute_dir>(path: P) -> io::Result { + resolve_absolute_dir_with_base(path, env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from)) +} + +/// Auxiliar de [`resolve_absolute_dir`] expuesta para tests. +/// +/// Permite probar la lógica de resolución inyectando el directorio base explícitamente, sin +/// modificar variables de entorno globales. No forma parte de la API pública. +#[doc(hidden)] +pub fn resolve_absolute_dir_with_base>( + path: P, + base: Option, +) -> io::Result { let path = path.as_ref(); let candidate = if path.is_absolute() { path.to_path_buf() } else { - // Directorio base CARGO_MANIFEST_DIR si está disponible; o current_dir() en su defecto. - env::var_os("CARGO_MANIFEST_DIR") - .map(PathBuf::from) - .or_else(|| env::current_dir().ok()) + // Directorio base proporcionado, o current_dir() en su defecto. + base.or_else(|| env::current_dir().ok()) .unwrap_or_else(|| PathBuf::from(".")) .join(path) }; @@ -191,10 +203,8 @@ pub fn resolve_absolute_dir>(path: P) -> io::Result { if absolute_dir.is_dir() { Ok(absolute_dir) } else { - Err({ - let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); - trace::warn!(msg); - io::Error::new(io::ErrorKind::InvalidInput, msg) - }) + let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); + trace::warn!(msg); + Err(io::Error::new(io::ErrorKind::InvalidInput, msg)) } } diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 00000000..63916cd3 --- /dev/null +++ b/src/web.rs @@ -0,0 +1,349 @@ +//! 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`]) 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; +use std::task::{Context, Poll}; + +use axum::body::Body; +use axum::extract::FromRequestParts; + +// Infraestructura del router. +pub use axum::Router; +pub use axum::http; + +// Extractores de petición. +pub use axum::extract::{Path, Query}; + +// Para implementar respuestas. +pub use axum::response::{IntoResponse, Response}; + +// Operaciones HTTP para registrar rutas. +pub use axum::routing::{delete, get, patch, post, put}; + +// Servicios para archivos estáticos (disco y embebidos). +pub use pagetop_statics::StaticResource; +pub use tower_http::services::ServeDir; + +// **< HttpRequest >******************************************************************************** + +/// Representa una petición HTTP. +/// +/// Almacena los datos necesarios para negociar el idioma y renderizar las páginas de error, +/// incluyendo la URI completa y las cabeceras de la petición original. +/// +/// Puede declararse directamente como parámetro en un *handler* para pasarlo al +/// [`Context`](crate::core::component::Context) de renderizado y a las variantes de +/// [`ErrorPage`](crate::response::page::ErrorPage): +/// +/// ```rust,ignore +/// async fn my_handler(request: HttpRequest) -> Result { ... } +/// ``` +#[derive(Clone, Debug)] +pub struct HttpRequest { + uri: http::Uri, + headers: http::HeaderMap, +} + +impl HttpRequest { + /// Devuelve la URI completa de la petición, incluyendo la *query string* si la hay. + pub fn uri(&self) -> &str { + self.uri + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/") + } + + /// Devuelve la ruta (*path*) de la petición, sin la *query string*. + pub fn path(&self) -> &str { + self.uri.path() + } + + /// Devuelve la cadena de consulta (*query string*) de la petición, sin el carácter `?`. + /// + /// Devuelve una cadena vacía si la petición no tiene *query string*. + pub fn query_string(&self) -> &str { + self.uri.query().unwrap_or("") + } + + /// Devuelve las cabeceras HTTP de la petición. + pub fn headers(&self) -> &http::HeaderMap { + &self.headers + } +} + +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 http::request::Parts, + _state: &S, + ) -> Result { + Ok(HttpRequest { + uri: parts.uri.clone(), + headers: parts.headers.clone(), + }) + } +} + +// **< ServeEmbedded >****************************************************************************** + +/// Servicio para archivos estáticos embebidos en el binario. +/// +/// 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. +/// +/// 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 { + files: std::sync::Arc>, +} + +impl ServeEmbedded { + /// Crea un nuevo servicio a partir del mapa de recursos embebidos generado por `build.rs`. + pub fn new(files: HashMap<&'static str, StaticResource>) -> Self { + Self { + files: std::sync::Arc::new(files), + } + } +} + +impl tower::Service> for ServeEmbedded { + type Response = http::Response; + type Error = Infallible; + type Future = std::future::Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + 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 '/'. + let path = req.uri().path().trim_start_matches('/'); + + // Busca la ruta exacta; si es raíz o directorio, intenta index.html. + let resource = self.files.get(path).or_else(|| { + if path.is_empty() || path.ends_with('/') { + self.files.get("index.html") + } else { + None + } + }); + + let response = match resource { + Some(r) => http::Response::builder() + .header(header::CONTENT_TYPE, r.mime_type) + .body(Body::from(r.data)) + .unwrap(), + None => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap(), + }; + + std::future::ready(Ok(response)) + } +} + +// **< serve_static_files! >************************************************************************ + +/// Configura el servidor web para publicar archivos estáticos. +/// +/// La macro añade rutas al [`Router`] del primer argumento usando uno de los tres modos posibles: +/// +/// - **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 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 +/// +/// ```rust,ignore +/// # use pagetop::prelude::*; +/// pub struct MyExtension; +/// +/// impl Extension for MyExtension { +/// fn configure_router(&self, router: Router) -> Router { +/// // Forma 1) Sistema de ficheros o embebido. +/// serve_static_files!(router, ["/var/www/static", assets] => "/public"); +/// +/// // Forma 2) Siempre embebido. +/// serve_static_files!(router, [assets] => "/public"); +/// +/// // Forma 3) Sólo sistema de ficheros (no requiere `assets`). +/// serve_static_files!(router, "/var/www/static" => "/public"); +/// +/// router +/// } +/// } +/// ``` +#[macro_export] +macro_rules! serve_static_files { + // Forma 1: primero intenta servir desde el sistema de ficheros; si falla, sirve embebido. + ( $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 { + $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.nest_service( + $path, + $crate::web::ServeEmbedded::new( + []::$bundle(), + ), + ) + } + }; + }; + // Forma 3: intenta servir desde el sistema de ficheros. + ( $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 + } + } + }; + }; +} + +// **< Utilidades de test >************************************************************************* + +/// Utilidades para escribir pruebas de integración con PageTop sobre Axum. +#[doc(hidden)] +pub mod test { + use axum::Router; + use axum::body::Body; + use axum::http; + use tower::ServiceExt; + + /// Devuelve el router tal como se recibe, listo para usarse en pruebas de integración. + pub fn init_router(router: Router) -> Router { + router + } + + /// Constructor de peticiones HTTP para pruebas. + pub struct TestRequest { + method: http::Method, + uri: String, + } + + impl TestRequest { + /// Crea una petición GET. + pub fn get() -> Self { + Self { + method: http::Method::GET, + uri: "/".to_owned(), + } + } + + /// Crea una petición POST. + pub fn post() -> Self { + Self { + method: http::Method::POST, + uri: "/".to_owned(), + } + } + + /// Establece la URI de la petición. + pub fn uri(mut self, uri: impl Into) -> Self { + self.uri = uri.into(); + self + } + + /// Construye la petición HTTP de Axum (para enviar al router en tests de integración). + pub fn to_request(self) -> http::Request { + http::Request::builder() + .method(self.method) + .uri(self.uri) + .body(Body::empty()) + .unwrap() + } + + /// Construye un [`HttpRequest`](super::HttpRequest) listo para pasarlo a + /// [`Context::new`](crate::core::component::Context::new) en tests unitarios de componentes. + pub fn to_http_request(self) -> super::HttpRequest { + let uri = self.uri.parse().unwrap(); + super::HttpRequest { + uri, + headers: axum::http::HeaderMap::new(), + } + } + } + + /// Envía una petición al router y devuelve la respuesta. + pub async fn send_request(router: &Router, req: http::Request) -> http::Response { + router.clone().oneshot(req).await.unwrap() + } +} diff --git a/static/css/intro.css b/static/css/intro.css index 1cc03ffc..00fe0d21 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -1,8 +1,8 @@ :root { - --intro-bg-img: url('/img/intro-header.jpg'); - --intro-bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg')); - --intro-bg-img-sm: url('/img/intro-header-sm.jpg'); - --intro-bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg')); + --intro-bg-img: url('/pagetop/img/intro-header.jpg'); + --intro-bg-img-set: image-set(url('/pagetop/img/intro-header.avif') type('image/avif'), url('/pagetop/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg')); + --intro-bg-img-sm: url('/pagetop/img/intro-header-sm.jpg'); + --intro-bg-img-sm-set: image-set(url('/pagetop/img/intro-header-sm.avif') type('image/avif'), url('/pagetop/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg')); --intro-bg-color: #7a430e; --intro-bg-block-1: #ffb84b; --intro-bg-block-2: #ffc66f;