diff --git a/Cargo.toml b/Cargo.toml index 269f752f..48bc600c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,63 @@ +[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 = [ @@ -15,45 +75,12 @@ 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] -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" +actix-web = { version = "4.13", default-features = false } 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" } @@ -65,65 +92,3 @@ 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 89fade06..604c4b3c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ impl Extension for HelloWorld { } } -async fn hello_world(request: HttpRequest) -> Result { +async fn hello_world(request: HttpRequest) -> ResultPage { 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 1c7f066e..4a6fc6c0 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_router(&self, router: Router) -> Router { - router.route("/", web::get(form_controls)) + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(form_controls)); } } -async fn form_controls(request: HttpRequest) -> Result { +async fn form_controls(request: HttpRequest) -> ResultPage { Page::new(request) .with_child( Intro::default() diff --git a/examples/hello-name.rs b/examples/hello-name.rs index 71439c7d..e2904c6f 100644 --- a/examples/hello-name.rs +++ b/examples/hello-name.rs @@ -3,15 +3,16 @@ use pagetop::prelude::*; struct HelloName; impl Extension for HelloName { - fn configure_router(&self, router: Router) -> Router { - router.route("/hello/{name}", web::get(hello_name)) + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/hello/{name}", service::web::get().to(hello_name)); } } async fn hello_name( request: HttpRequest, - web::Path(name): web::Path, -) -> Result { + path: service::web::Path, +) -> ResultPage { + let name = path.into_inner(); Page::new(request) .with_child(Html::with(move |_| { html! { diff --git a/examples/hello-world.rs b/examples/hello-world.rs index f1c40d23..e6127af9 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_router(&self, router: Router) -> Router { - router.route("/", web::get(hello_world)) + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(hello_world)); } } -async fn hello_world(request: HttpRequest) -> Result { +async fn hello_world(request: HttpRequest) -> ResultPage { Page::new(request) .with_child(Html::with(|_| { html! { diff --git a/examples/intro-colors.rs b/examples/intro-colors.rs index b219c5be..57ddeed4 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_router(&self, router: Router) -> Router { - router.route("/", web::get(intro_colors)) + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(intro_colors)); } } -async fn intro_colors(request: HttpRequest) -> Result { +async fn intro_colors(request: HttpRequest) -> ResultPage { Page::new(request) .with_child( Intro::default() diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 7f8ccdda..38918aed 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -8,11 +8,7 @@ struct SuperMenu; impl Extension for SuperMenu { fn dependencies(&self) -> Vec { - vec![ - &pagetop_aliner::Aliner, - &pagetop_bootsier::Bootsier, - &pagetop::base::extension::Welcome, - ] + vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] } fn initialize(&self) { diff --git a/extensions/pagetop-aliner/Cargo.toml b/extensions/pagetop-aliner/Cargo.toml index d2828b3f..00deda3e 100644 --- a/extensions/pagetop-aliner/Cargo.toml +++ b/extensions/pagetop-aliner/Cargo.toml @@ -1,6 +1,7 @@ [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 @@ -10,15 +11,11 @@ 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 bf515d66..7b772591 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) -> Result { +async fn homepage(request: HttpRequest) -> ResultPage { 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 dedf4e19..e88a9142 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) -> Result { +async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) .with_theme(&Aliner) .with_child( @@ -109,21 +109,20 @@ impl Extension for Aliner { Some(&Self) } - fn configure_router(&self, router: Router) -> Router { - serve_static_files!(router, [aliner] => "/aliner"); - router + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + static_files_service!(scfg, [aliner] => "/aliner"); } } impl Theme for Aliner { fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/pagetop/css/normalize.css") + StyleSheet::from("/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/pagetop/css/basic.css") + StyleSheet::from("/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 44b6d248..6e6fc66b 100644 --- a/extensions/pagetop-bootsier/Cargo.toml +++ b/extensions/pagetop-bootsier/Cargo.toml @@ -1,6 +1,7 @@ [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. @@ -10,7 +11,6 @@ keywords = ["pagetop", "theme", "bootstrap", "css", "js"] repository.workspace = true homepage.workspace = true -edition.workspace = true license.workspace = true authors.workspace = true @@ -18,8 +18,5 @@ 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 f71f221e..edb0be75 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) -> Result { +async fn homepage(request: HttpRequest) -> ResultPage { 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 8c0ec847..ca2a80c8 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) -> Result { +async fn homepage(request: HttpRequest) -> ResultPage { Page::new(request) .with_theme(&Bootsier) .with_child( @@ -140,10 +140,9 @@ impl Extension for Bootsier { Some(&Self) } - fn configure_router(&self, router: Router) -> Router { - serve_static_files!(router, [bootsier_bs] => "/bootsier/bs"); - serve_static_files!(router, [bootsier_js] => "/bootsier/js"); - router + 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"); } } diff --git a/extensions/pagetop-bootsier/src/theme/classes/layout.rs b/extensions/pagetop-bootsier/src/theme/classes/layout.rs index 1438b210..ee403a88 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::BreakPoint; use crate::theme::attrs::{ScaleSize, Side}; +use crate::theme::BreakPoint; // **< Margin >************************************************************************************* diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index b70fed65..ca15c635 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::LOCALES_BOOTSIER; use crate::theme::*; +use crate::LOCALES_BOOTSIER; /// 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 18ab908f..60d7120d 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::LOCALES_BOOTSIER; use crate::theme::form; +use crate::LOCALES_BOOTSIER; /// 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 68cce931..997b7c45 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::LOCALES_BOOTSIER; use crate::theme::form; +use crate::LOCALES_BOOTSIER; use std::fmt; diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 7d51e9c9..92736586 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::LOCALES_BOOTSIER; use crate::theme::form; +use crate::LOCALES_BOOTSIER; // **< Item >*************************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index 81b32783..781e1d09 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::LOCALES_BOOTSIER; use crate::theme::form; +use crate::LOCALES_BOOTSIER; /// 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 df2c28a7..678ccdb3 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 43386baf..ef5a6fe9 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::LOCALES_BOOTSIER; use crate::theme::*; +use crate::LOCALES_BOOTSIER; // **< ItemKind >*********************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index 096ec87a..ccd97e90 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::LOCALES_BOOTSIER; use crate::theme::*; +use crate::LOCALES_BOOTSIER; 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 a2c014b8..764627e4 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::LOCALES_BOOTSIER; use crate::theme::*; +use crate::LOCALES_BOOTSIER; /// Componente para crear un **panel lateral deslizante** ([`offcanvas`]). /// diff --git a/extensions/pagetop-seaorm/Cargo.toml b/extensions/pagetop-seaorm/Cargo.toml index 6e2b6fc7..66034137 100644 --- a/extensions/pagetop-seaorm/Cargo.toml +++ b/extensions/pagetop-seaorm/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "pagetop-seaorm" version = "0.0.4" +edition = "2021" description = """ Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales. @@ -10,7 +11,6 @@ keywords = ["pagetop", "database", "sql", "orm", "ssr"] repository.workspace = true homepage.workspace = true -edition.workspace = true license.workspace = true authors.workspace = true @@ -20,10 +20,17 @@ 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 -tokio.workspace = true -url.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" diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml index a06bc9ca..aa37e1af 100644 --- a/helpers/pagetop-build/Cargo.toml +++ b/helpers/pagetop-build/Cargo.toml @@ -1,6 +1,7 @@ [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 @@ -11,10 +12,9 @@ keywords = ["pagetop", "build", "assets", "resources", "static"] repository.workspace = true homepage.workspace = true -edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -grass.workspace = true +grass = "0.13" pagetop-statics.workspace = true diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md index bb7d3bfa..c5d9c5bd 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 [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) +en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.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) { - serve_static_files!(scfg, guides => "/ruta/a/guides"); + static_files_service!(scfg, guides => "/ruta/a/guides"); } } ``` diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index f8390ee6..774a4af7 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 [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) +en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html) para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: ```rust,ignore @@ -104,10 +104,9 @@ use pagetop::prelude::*; pub struct MyExtension; impl Extension for MyExtension { - /// 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 + /// 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"); } } ``` @@ -117,10 +116,10 @@ impl Extension for MyExtension { html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" )] -use grass::{Options, OutputStyle, from_path}; -use pagetop_statics::{ResourceDir, resource_dir}; +use grass::{from_path, Options, OutputStyle}; +use pagetop_statics::{resource_dir, ResourceDir}; -use std::fs::{File, create_dir_all, remove_dir_all}; +use std::fs::{create_dir_all, remove_dir_all, File}; use std::io::Write; use std::path::Path; diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index 13b5d387..b34d2ec1 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "pagetop-macros" version = "0.3.0" +edition = "2021" description = """ Una colección de macros que mejoran la experiencia de desarrollo con PageTop. @@ -10,7 +11,6 @@ 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.workspace = true -proc-macro2-diagnostics.workspace = true -quote.workspace = true -syn.workspace = true +proc-macro2 = "1.0" +proc-macro2-diagnostics = { version = "0.10", default-features = false } +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } diff --git a/helpers/pagetop-minimal/Cargo.toml b/helpers/pagetop-minimal/Cargo.toml index dfb37a9d..39b7d10d 100644 --- a/helpers/pagetop-minimal/Cargo.toml +++ b/helpers/pagetop-minimal/Cargo.toml @@ -1,6 +1,7 @@ [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 @@ -11,11 +12,10 @@ keywords = ["pagetop", "build", "assets", "resources", "static"] repository.workspace = true homepage.workspace = true -edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -concat-string.workspace = true -indoc.workspace = true -pastey.workspace = true +concat-string = "1.0" +indoc = "2.0" +pastey = "0.2" diff --git a/helpers/pagetop-statics/Cargo.toml b/helpers/pagetop-statics/Cargo.toml index 503511eb..da967c31 100644 --- a/helpers/pagetop-statics/Cargo.toml +++ b/helpers/pagetop-statics/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "pagetop-statics" version = "0.1.3" +edition = "2021" description = """ Librería para automatizar la recopilación de recursos estáticos en PageTop. @@ -10,7 +11,6 @@ keywords = ["pagetop", "build", "static", "resources", "file"] repository.workspace = true homepage.workspace = true -edition.workspace = true license.workspace = true authors.workspace = true @@ -19,11 +19,15 @@ default = ["change-detection"] sort = [] [dependencies] -change-detection = { workspace = true, optional = true } -mime_guess.workspace = true -path-slash.workspace = true +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"] } [build-dependencies] -change-detection = { workspace = true, optional = true } -mime_guess.workspace = true -path-slash.workspace = true +change-detection = { version = "1.2", optional = true } +mime_guess = "2.0" +path-slash = "0.2" diff --git a/helpers/pagetop-statics/build.rs b/helpers/pagetop-statics/build.rs index 3cbd6706..fcd009c9 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::{SplitByCount, generate_resources_sets}; +use sets::{generate_resources_sets, SplitByCount}; use std::{env, path::Path}; diff --git a/helpers/pagetop-statics/src/resource_dir.rs b/helpers/pagetop-statics/src/resource_dir.rs index 41c29829..805e1ed4 100644 --- a/helpers/pagetop-statics/src/resource_dir.rs +++ b/helpers/pagetop-statics/src/resource_dir.rs @@ -1,4 +1,4 @@ -use super::sets::{SplitByCount, generate_resources_sets}; +use super::sets::{generate_resources_sets, SplitByCount}; use std::{ env, io, path::{Path, PathBuf}, diff --git a/helpers/pagetop-statics/src/sets.rs b/helpers/pagetop-statics/src/sets.rs index 5e09f1ff..1d9299df 100644 --- a/helpers/pagetop-statics/src/sets.rs +++ b/helpers/pagetop-statics/src/sets.rs @@ -5,8 +5,8 @@ use std::{ }; use super::resource::{ - DEFAULT_VARIABLE_NAME, collect_resources, generate_function_end, generate_function_header, - generate_resource_insert, generate_uses, generate_variable_header, generate_variable_return, + collect_resources, generate_function_end, generate_function_header, generate_resource_insert, + generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME, }; /// Defines the split strategie. diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 5d6e629c..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1,8 +0,0 @@ -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 4a009fb7..6a266edc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,21 +5,25 @@ mod figfont; use crate::core::{extension, extension::ExtensionRef}; use crate::html::Markup; use crate::locale::Locale; -use crate::response::page::ErrorPage; -use crate::web::{HttpRequest, Router}; -use crate::{PAGETOP_VERSION, global, trace}; +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 std::future::Future; use std::io::Error; use std::sync::LazyLock; /// Punto de entrada de una aplicación PageTop. /// -/// No almacena datos, **encapsula** el inicio completo de la configuración y puesta en marcha de la -/// aplicación. Para instanciarla se puede usar [`new()`](Application::new) o -/// [`prepare()`](Application::prepare). Después sólo hay que llamar a [`run()`](Application::run) -/// para ejecutar la aplicación (o a [`test()`](Application::test) si se está preparando un entorno -/// de pruebas). +/// 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). pub struct Application; impl Default for Application { @@ -29,24 +33,24 @@ impl Default for Application { } impl Application { - /// Crea una instancia mínima de la aplicación, sin extensión raíz. - /// - /// Útil para verificar que el servidor arranca correctamente. Para una aplicación real, usa - /// [`prepare()`](Application::prepare) con una extensión raíz. + /// Crea una instancia de la aplicación. pub fn new() -> Self { Self::internal_prepare(None) } /// Prepara una instancia de la aplicación a partir de una extensión raíz. /// - /// Las dependencias se habilitan en orden: primero las que no dependen de ninguna otra, luego - /// las que dependen de extensiones ya habilitadas, y así sucesivamente hasta dejar habilitada - /// la extensión raíz. + /// 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. pub fn prepare(root_extension: ExtensionRef) -> Self { Self::internal_prepare(Some(root_extension)) } - // Secuencia de arranque común a new() y prepare(). + /// Método interno para preparar la aplicación, opcionalmente con una extensión. fn internal_prepare(root_extension: Option) -> Self { // Al arrancar muestra una cabecera para la aplicación. Self::show_banner(); @@ -69,10 +73,10 @@ impl Application { Self } - // Muestra la cabecera de arranque si está habilitada en la configuración. + /// Muestra una cabecera para la aplicación basada en la configuración. fn show_banner() { use colored::Colorize; - use terminal_size::{Width, terminal_size}; + use terminal_size::{terminal_size, Width}; if global::SETTINGS.app.startup_banner != global::StartupBanner::Off { // Nombre de la aplicación, ajustado al ancho del terminal si es necesario. @@ -81,8 +85,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: String = app_name.chars().take(maxlen).collect(); - if app_name.chars().count() > maxlen { + let mut app = app_name.substring(0, maxlen).to_string(); + if app_name.len() > maxlen { app = format!("{app}..."); } if let Some(ff) = figfont::FIGFONT.convert(&app) { @@ -99,7 +103,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!( @@ -110,55 +114,72 @@ 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. /// - /// 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 - ); + /// 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(); - // 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. + 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!( + "{}:{}", + &global::SETTINGS.server.bind_address, + &global::SETTINGS.server.bind_port + ))? + .run()) } - /// Devuelve el servidor web configurado para usarlo en pruebas de integración. - pub fn test(self) -> Router { - Self::build_router() + /// 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)) } } -async fn route_not_found(request: HttpRequest) -> Result { +async fn service_not_found(request: HttpRequest) -> ResultPage { Err(ErrorPage::NotFound(request)) } diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index 63902f10..a7ccb2c4 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("/pagetop/css/intro.css").with_version(PAGETOP_VERSION), + StyleSheet::from("/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 b6d09dbe..b8739a40 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_router(&self, router: Router) -> Router { - router.route("/", web::get(home)) + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(home)); } } -async fn home(request: HttpRequest) -> Result { +async fn home(request: HttpRequest) -> ResultPage { let app = &global::SETTINGS.app.name; Page::new(request) diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 3e6e99e9..34f9088b 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("/pagetop/css/normalize.css") + StyleSheet::from("/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/pagetop/css/basic.css") + StyleSheet::from("/css/basic.css") .with_version(PAGETOP_VERSION) .with_weight(-99), )) diff --git a/src/config.rs b/src/config.rs index 9c687a0f..9b7b43d2 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. Puedes -//! cambiar esta ubicación mediante la variable de entorno `CONFIG_DIR`. +//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación. //! //! PageTop carga en este orden, y siempre de forma opcional, los siguientes archivos TOML: //! @@ -42,6 +42,7 @@ //! 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`: @@ -90,6 +91,7 @@ //! //! Las estructuras de configuración son de **sólo lectura** durante la ejecución. //! +//! //! # Usando tus opciones de configuración //! //! ```rust,ignore @@ -129,14 +131,9 @@ 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. 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()) - }; + // 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()); Config::builder() // 1. Configuración común para todos los entornos (common.toml). @@ -161,7 +158,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_NAME: SettingsType => [ +/// include_config!(SETTINGS: Settings => [ /// "ruta.clave" => valor, /// // ... /// ]); @@ -171,8 +168,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. -/// * **`SettingsType`** es la estructura que define los tipos para deserializar la configuración. -/// Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`). +/// * **`Settings_Type`** es la referencia a 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. /// @@ -214,7 +211,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. /// -/// * **Sólo lectura**. La variable generada es inmutable durante toda la vida del programa. Para +/// * **Solo 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). /// @@ -223,8 +220,8 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// /// # Requisitos /// -/// * Las claves deben coincidir con los campos (*snake case*) de la estructura de ajustes. -/// * Añade `serde` con la *feature* `derive` en *Cargo.toml*: +/// * Dependencia `serde` con la *feature* `derive`. +/// * Las claves deben coincidir con los campos (*snake case*) de tu estructura `Settings_Type`. /// /// ```toml /// [dependencies] @@ -232,10 +229,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:"] @@ -244,18 +241,17 @@ 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) - .expect(concat!("Failed to set default for key ", $k)); + settings = settings.set_default($k, $v).unwrap(); )* 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 03e32a94..8a47848e 100644 --- a/src/core.rs +++ b/src/core.rs @@ -30,22 +30,28 @@ 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, 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" + /// 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. fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { let maxlen = type_name.len(); @@ -53,7 +59,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'; // Control, ningún carácter previo aún. + let mut previous_char = '\0'; // Se inicializa a carácter nulo, no hay aún carácter previo. 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 f47d3d6e..d60129c1 100644 --- a/src/core/action/list.rs +++ b/src/core/action/list.rs @@ -1,7 +1,7 @@ -use crate::AutoDefault; -use crate::core::AnyCast; use crate::core::action::{ActionBox, ActionDispatcher}; +use crate::core::AnyCast; use crate::trace; +use crate::AutoDefault; use parking_lot::RwLock; diff --git a/src/core/component/children.rs b/src/core/component/children.rs index bfedec14..617a783d 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::{Markup, html}; -use crate::{AutoDefault, UniqueId, builder_fn}; +use crate::html::{html, Markup}; +use crate::{builder_fn, AutoDefault, UniqueId}; use parking_lot::Mutex; diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 9ff7251c..63b2daad 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::web::HttpRequest; -use crate::{CowStr, builder_fn, util}; +use crate::service::HttpRequest; +use crate::{builder_fn, util, CowStr}; use std::any::Any; use std::cell::Cell; diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index b7ceaa9a..718e3b37 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::{Markup, html}; +use crate::html::{html, Markup}; /// Permite clonar un componente. /// diff --git a/src/core/component/error.rs b/src/core/component/error.rs index 86f9e4aa..beb7c8f2 100644 --- a/src/core/component/error.rs +++ b/src/core/component/error.rs @@ -1,4 +1,4 @@ -use crate::html::{Markup, html}; +use crate::html::{html, Markup}; 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 f9081d83..b787c9a8 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -1,43 +1,60 @@ use crate::core::action::add_action; use crate::core::extension::ExtensionRef; use crate::core::theme::all::THEMES; -use crate::web::Router; -use crate::{global, serve_static_files, trace, web}; +use crate::{global, service, static_files_service, trace}; -use std::sync::OnceLock; +use parking_lot::RwLock; -static EXTENSIONS: OnceLock> = OnceLock::new(); +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())); // **< REGISTRO DE LAS EXTENSIONES >**************************************************************** pub fn register_extensions(root_extension: Option) { - // Garantiza que ocurre sólo una vez cuando los tests se ejecutan en paralelo. - EXTENSIONS.get_or_init(|| { - let mut list: Vec = Vec::new(); + // Prepara la lista de extensiones habilitadas. + let mut enabled_list: Vec = Vec::new(); - // Primero añade el tema básico a la lista de extensiones habilitadas. - add_to_enabled(&mut list, &crate::base::theme::Basic); + // Primero añade el tema básico a la lista de extensiones habilitadas. + add_to_enabled(&mut enabled_list, &crate::base::theme::Basic); - // 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); - } + // 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); + } - // 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); - } + // 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); + } - list - }); + // 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); } 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().into_iter().rev() { - add_to_enabled(list, d); + for d in extension.dependencies().iter().rev() { + add_to_enabled(list, *d); } // Añade la propia extensión a la lista. @@ -60,11 +77,40 @@ 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 EXTENSIONS.get().into_iter().flatten() { - for a in extension.actions() { + for extension in ENABLED_EXTENSIONS.read().iter() { + for a in extension.actions().into_iter() { add_action(a); } } @@ -74,28 +120,25 @@ pub fn register_actions() { pub fn initialize_extensions() { trace::info!("Calling application bootstrap"); - for e in EXTENSIONS.get().into_iter().flatten() { - e.initialize(); + for extension in ENABLED_EXTENSIONS.read().iter() { + extension.initialize(); } } -// **< CONFIGURA LAS RUTAS >************************************************************************ +// **< CONFIGURA LOS SERVICIOS >******************************************************************** -pub fn configure_routes(router: Router) -> Router { +pub fn configure_services(scfg: &mut service::web::ServiceConfig) { // Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos. #[cfg(debug_assertions)] - let router = router.route( + scfg.route( + // Ruta automática lanzada por Chrome DevTools. "/.well-known/appspecific/com.chrome.devtools.json", - web::get(|| async { web::http::StatusCode::NOT_FOUND }), + service::web::get().to(|| async { service::HttpResponse::NotFound().finish() }), ); - let router = EXTENSIONS - .get() - .into_iter() - .flatten() - .fold(router, |r, e| e.configure_router(r)); + for extension in ENABLED_EXTENSIONS.read().iter() { + extension.configure_service(scfg); + } - serve_static_files!(router, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/pagetop"); - - router + static_files_service!(scfg, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/"); } diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 984a5cc1..a5d2b723 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -1,9 +1,8 @@ -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::web::Router; +use crate::{actions, service}; /// Interfaz común que debe implementar cualquier extensión de PageTop. /// @@ -12,15 +11,15 @@ use crate::web::Router; /// /// ```rust /// # use pagetop::prelude::*; -/// pub struct MyExtension; +/// pub struct Blog; /// -/// impl Extension for MyExtension { +/// impl Extension for Blog { /// fn name(&self) -> L10n { -/// L10n::n("My Extension") +/// L10n::n("Blog") /// } /// /// fn description(&self) -> L10n { -/// L10n::n("Does something useful") +/// L10n::n("Blog system") /// } /// } /// ``` @@ -87,95 +86,31 @@ pub trait Extension: AnyInfo + Send + Sync { /// aceptar cualquier petición HTTP. fn initialize(&self) {} - /// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación. + /// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros + /// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig). /// - /// 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. - /// - /// # 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::*; - /// # async fn list_posts() -> &'static str { "" } - /// # async fn view_post() -> &'static str { "" } - /// # async fn create_post() -> &'static str { "" } - /// pub struct Blog; - /// - /// 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)) - /// } - /// } - /// ``` - /// - /// ## 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* + /// # Ejemplo /// /// ```rust,ignore /// # use pagetop::prelude::*; - /// pub struct Api; + /// pub struct ExtensionSample; /// - /// impl Extension for Api { - /// fn configure_router(&self, router: Router) -> Router { - /// router - /// .route("/api/data", web::get(get_data)) - /// .layer(auth_layer()) + /// impl Extension for ExtensionSample { + /// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + /// scfg.route("/sample", web::get().to(route_sample)); /// } /// } /// ``` + #[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. /// - /// ## 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 + /// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras + /// implementaciones. + fn drop_extensions(&self) -> Vec { + vec![] } } diff --git a/src/core/theme.rs b/src/core/theme.rs index 43649db1..a8c1f3a4 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::{Markup, html}; +use crate::html::{html, Markup}; use crate::locale::L10n; -use crate::{AutoDefault, util}; +use crate::{util, AutoDefault}; // **< Region >************************************************************************************* diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 0b036dd4..17f3b391 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::{Markup, html}; +use crate::html::{html, Markup}; use crate::locale::L10n; use crate::response::page::Page; -use crate::web::http::StatusCode; +use crate::service::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 a10e3ecc..a2b71ff2 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::{AutoDefault, UniqueId, builder_fn}; +use crate::{builder_fn, AutoDefault, UniqueId}; use parking_lot::RwLock; diff --git a/src/global.rs b/src/global.rs index d6bdbc47..8bf753e3 100644 --- a/src/global.rs +++ b/src/global.rs @@ -20,26 +20,28 @@ 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.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, // [dev] - "dev.pagetop_static_dir" => "", + "dev.pagetop_static_dir" => "", // [log] - "log.enabled" => true, - "log.tracing" => "Info", - "log.rolling" => "Stdout", - "log.path" => "log", - "log.prefix" => "tracing.log", - "log.format" => "Full", + "log.enabled" => true, + "log.tracing" => "Info", + "log.rolling" => "Stdout", + "log.path" => "log", + "log.prefix" => "tracing.log", + "log.format" => "Full", // [server] - "server.bind_address" => "localhost", - "server.bind_port" => 8080, + "server.bind_address" => "localhost", + "server.bind_port" => 8080, + "server.session_lifetime" => 604_800, ]); // **< Settings >*********************************************************************************** @@ -83,6 +85,11 @@ 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, @@ -109,7 +116,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,tower_http=Debug,axum::rejection=trace"*. + /// Ejemplo: *"Error,actix_server::builder=Info,tracing_actix_web=Debug"*. 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"*. @@ -129,4 +136,8 @@ 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 6020627d..21809c08 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,7 +1,7 @@ //! HTML en código. mod maud; -pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, display, html, html_private}; +pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE}; mod route; pub use route::RoutePath; diff --git a/src/html/assets.rs b/src/html/assets.rs index 80cb3b26..fe5f5b7c 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::{Markup, html}; +use crate::html::{html, Markup}; 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 9d0fb688..1a4174bf 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::{Markup, html}; +use crate::html::{html, Markup}; 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 [`serve_static_files!`](crate::serve_static_files). +/// > servirse usando [`static_files_service!`](crate::static_files_service). /// /// # Ejemplo /// @@ -165,12 +165,14 @@ impl Favicon { } } - // 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`). + /// 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. 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 6af0fd55..62126895 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::{Markup, PreEscaped, html}; -use crate::{AutoDefault, CowStr, Weight, util}; +use crate::html::{html, Markup, PreEscaped}; +use crate::{util, AutoDefault, CowStr, Weight}; /// 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 [`serve_static_files!`](crate::serve_static_files). +/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service). /// /// # Ejemplo /// diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index fb71fd44..5a6d98c5 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::{Markup, PreEscaped, html}; -use crate::{AutoDefault, CowStr, Weight, util}; +use crate::html::{html, Markup, PreEscaped}; +use crate::{util, AutoDefault, CowStr, Weight}; /// 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 [`serve_static_files!`](crate::serve_static_files). +/// > servirse usando [`static_files_service!`](crate::static_files_service). /// /// # Ejemplo /// diff --git a/src/html/attr.rs b/src/html/attr.rs index 8f25a5eb..61f7252c 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -1,5 +1,5 @@ use crate::locale::{L10n, LangId}; -use crate::{AutoDefault, builder_fn}; +use crate::{builder_fn, AutoDefault}; /// Valor opcional para atributos HTML. /// diff --git a/src/html/classes.rs b/src/html/classes.rs index 2f665c19..3465d6b3 100644 --- a/src/html/classes.rs +++ b/src/html/classes.rs @@ -1,4 +1,4 @@ -use crate::{AutoDefault, CowStr, builder_fn, util}; +use crate::{builder_fn, util, AutoDefault, CowStr}; use std::collections::HashSet; diff --git a/src/html/logo.rs b/src/html/logo.rs index 7746da7a..d5dcaa0b 100644 --- a/src/html/logo.rs +++ b/src/html/logo.rs @@ -1,7 +1,7 @@ -use crate::AutoDefault; use crate::core::component::Context; -use crate::html::{Markup, html}; +use crate::html::{html, Markup}; use crate::locale::L10n; +use crate::AutoDefault; /// Representación SVG del **logotipo de PageTop** para incrustar en HTML. /// diff --git a/src/html/route.rs b/src/html/route.rs index ae694857..a1efb0d8 100644 --- a/src/html/route.rs +++ b/src/html/route.rs @@ -1,4 +1,4 @@ -use crate::{AutoDefault, CowStr, builder_fn}; +use crate::{builder_fn, AutoDefault, CowStr}; use std::fmt; diff --git a/src/lib.rs b/src/lib.rs index d4712a0c..0213e61e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,12 +53,12 @@ use pagetop::prelude::*; struct HelloWorld; impl Extension for HelloWorld { - fn configure_router(&self, router: Router) -> Router { - router.route("/", web::get(hello_world)) + fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { + scfg.route("/", service::web::get().to(hello_world)); } } -async fn hello_world(request: HttpRequest) -> Result { +async fn hello_world(request: HttpRequest) -> ResultPage { 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("/pagetop/css/normalize.css").with_version("8.0.1"), +/// StyleSheet::from("/css/normalize.css").with_version("8.0.1"), /// )) /// .alter_assets(AssetsOp::AddStyleSheet( -/// StyleSheet::from("/pagetop/css/basic.css").with_version(PAGETOP_VERSION), +/// StyleSheet::from("/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::{AutoDefault, builder_fn, html, main, test}; +pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault}; -pub use pagetop_statics::{StaticResource, resource}; +pub use pagetop_statics::{resource, StaticResource}; 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 rutas web. -pub mod web; +// Gestión del servidor y servicios web. +pub mod service; // 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 bffc805c..06a07c49 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::{LanguageIdentifier, langid}; +use super::{langid, LanguageIdentifier}; use std::sync::LazyLock; diff --git a/src/locale/l10n.rs b/src/locale/l10n.rs index e75e103d..af5e9535 100644 --- a/src/locale/l10n.rs +++ b/src/locale/l10n.rs @@ -1,5 +1,5 @@ use crate::html::{Markup, PreEscaped}; -use crate::{AutoDefault, CowStr, include_locales}; +use crate::{include_locales, AutoDefault, CowStr}; use super::{LangId, Locale}; diff --git a/src/locale/languages.rs b/src/locale/languages.rs index cda4483d..f1962a14 100644 --- a/src/locale/languages.rs +++ b/src/locale/languages.rs @@ -1,6 +1,6 @@ use crate::util; -use super::{LanguageIdentifier, langid}; +use super::{langid, LanguageIdentifier}; use std::collections::HashMap; use std::sync::LazyLock; diff --git a/src/locale/request.rs b/src/locale/request.rs index 53e4e032..6f3af13d 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -1,5 +1,5 @@ use crate::global; -use crate::web::HttpRequest; +use crate::service::HttpRequest; use super::{LangId, LanguageIdentifier, Locale}; diff --git a/src/prelude.rs b/src/prelude.rs index 5e6f7ec1..818bfc91 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::web -pub use crate::serve_static_files; +// crate::service +pub use crate::static_files_service; // 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::web; -pub use crate::web::{HttpRequest, Router}; +pub use crate::service; +pub use crate::service::{HttpMessage, HttpRequest, HttpResponse}; 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::*}; +pub use crate::response::{json::*, page::*, redirect::*, ResponseError}; pub use crate::base::action; pub use crate::base::component::*; diff --git a/src/response.rs b/src/response.rs index 55150b71..4078d420 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,5 +1,7 @@ //! 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 7ef4b402..23b8ab2c 100644 --- a/src/response/json.rs +++ b/src/response/json.rs @@ -1,4 +1,4 @@ -//! Extractor y generador de respuestas JSON (reexporta [`axum::Json`]). +//! Extractor y generador de respuestas JSON (reexporta [`actix_web::web::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) -> web::http::StatusCode { +//! async fn crear_usuario(payload: Json) -> HttpResponse { //! // `payload` ya es `NuevoUsuario`; si la deserialización falla, -//! // devolverá automáticamente 400 Bad Request. -//! web::http::StatusCode::OK +//! // devolverá automáticamente 400 Bad Request con un cuerpo JSON que describe el error. +//! HttpResponse::Ok().finish() //! } //! ``` //! @@ -36,4 +36,4 @@ //! `Json` funciona con cualquier tipo que implemente `serde::Serialize` (para respuestas) y/o //! `serde::Deserialize` (para peticiones). -pub use axum::Json; +pub use actix_web::web::Json; diff --git a/src/response/page.rs b/src/response/page.rs index 2376fd39..d8bd4b16 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -16,16 +16,18 @@ 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::web::HttpRequest; -use crate::{AutoDefault, builder_fn}; +use crate::service::HttpRequest; +use crate::{builder_fn, AutoDefault}; // **< ReservedRegion >***************************************************************************** @@ -225,8 +227,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 [`Result`] con el [`Markup`] final. - pub fn render(&mut self) -> Result { + /// devuelve un [`ResultPage`] con el [`Markup`] final. + pub fn render(&mut self) -> ResultPage { // 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 2c92d02e..fd9959c2 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,7 +1,9 @@ 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; @@ -29,9 +31,13 @@ pub enum ErrorPage { } impl ErrorPage { - // 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. + /// 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. 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(); @@ -45,19 +51,7 @@ impl ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - 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, + f.write_str(&code.to_string()) } } } @@ -75,7 +69,7 @@ impl fmt::Display for ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(self.status_code().as_str()) + f.write_str(&self.status_code().to_string()) } } @@ -86,7 +80,7 @@ impl fmt::Display for ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(self.status_code().as_str()) + f.write_str(&self.status_code().to_string()) } } @@ -102,17 +96,22 @@ impl fmt::Display for ErrorPage { } } -/// 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() +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, + } } } diff --git a/src/response/redirect.rs b/src/response/redirect.rs index ebe470f8..a3bec0cd 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -18,12 +18,12 @@ //! //! - **Respuestas especiales**. -use crate::web::{IntoResponse, Response, http}; +use crate::service::HttpResponse; /// Funciones predefinidas para generar respuestas HTTP de redirección. /// -/// Ofrece atajos para construir respuestas con el código de estado apropiado y la cabecera -/// `Location`, evitando repetir la misma secuencia en cada controlador. +/// 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. pub struct Redirect; impl Redirect { @@ -34,12 +34,10 @@ 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) -> Response { - ( - http::StatusCode::MOVED_PERMANENTLY, - [(http::header::LOCATION, redirect_to_url.to_owned())], - ) - .into_response() + pub fn moved(redirect_to_url: &str) -> HttpResponse { + HttpResponse::MovedPermanently() + .append_header(("Location", redirect_to_url)) + .finish() } /// Redirección **permanente**. Código de estado **308**. Mantiene método y cuerpo sin cambios. @@ -47,12 +45,10 @@ 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) -> Response { - ( - http::StatusCode::PERMANENT_REDIRECT, - [(http::header::LOCATION, redirect_to_url.to_owned())], - ) - .into_response() + pub fn permanent(redirect_to_url: &str) -> HttpResponse { + HttpResponse::PermanentRedirect() + .append_header(("Location", redirect_to_url)) + .finish() } /// Redirección **temporal**. Código de estado **302**. El método GET (y normalmente HEAD) se @@ -61,12 +57,10 @@ 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) -> Response { - ( - http::StatusCode::FOUND, - [(http::header::LOCATION, redirect_to_url.to_owned())], - ) - .into_response() + pub fn found(redirect_to_url: &str) -> HttpResponse { + HttpResponse::Found() + .append_header(("Location", redirect_to_url)) + .finish() } /// Redirección **temporal**. Código de estado **303**. Método GET se mantiene tal cual. Los @@ -75,12 +69,10 @@ 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) -> Response { - ( - http::StatusCode::SEE_OTHER, - [(http::header::LOCATION, redirect_to_url.to_owned())], - ) - .into_response() + pub fn see_other(redirect_to_url: &str) -> HttpResponse { + HttpResponse::SeeOther() + .append_header(("Location", redirect_to_url)) + .finish() } /// Redirección **temporal**. Código de estado **307**. Conserva método y cuerpo íntegros. @@ -88,12 +80,10 @@ 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) -> Response { - ( - http::StatusCode::TEMPORARY_REDIRECT, - [(http::header::LOCATION, redirect_to_url.to_owned())], - ) - .into_response() + pub fn temporary(redirect_to_url: &str) -> HttpResponse { + HttpResponse::TemporaryRedirect() + .append_header(("Location", redirect_to_url)) + .finish() } /// Respuesta **especial**. Código de estado **304**. Se envía tras una petición condicional, @@ -102,7 +92,7 @@ impl Redirect { /// /// No es una redirección, el cliente debe reutilizar su copia local. #[must_use] - pub fn not_modified() -> Response { - http::StatusCode::NOT_MODIFIED.into_response() + pub fn not_modified() -> HttpResponse { + HttpResponse::NotModified().finish() } } diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 00000000..10665413 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,128 @@ +//! 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 5c1fff49..6ea4b70d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -58,15 +58,14 @@ 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(input: &str) -> Result, NormalizeAsciiError> { +pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsciiError> { let bytes = input.as_bytes(); if bytes.is_empty() { return Err(NormalizeAsciiError::IsEmpty); } - // 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 start = 0usize; + let mut end = 0usize; let mut needs_alloc = false; let mut needs_alloc_ws = false; @@ -111,7 +110,6 @@ pub fn normalize_ascii(input: &str) -> Result, NormalizeAsciiError> return Ok(Cow::Borrowed(slice)); } - // Segunda pasada, construye la cadena normalizada. let mut output = String::with_capacity(slice.len()); let mut prev_sep = true; @@ -134,8 +132,8 @@ pub fn normalize_ascii(input: &str) -> Result, NormalizeAsciiError> /// /// - 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 no ASCII; y emite un `trace::debug!` con el campo -/// `target`. +/// - Devuelve `None` si la entrada contiene bytes non-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) { @@ -173,25 +171,15 @@ 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 proporcionado, o current_dir() en su defecto. - base.or_else(|| env::current_dir().ok()) + // 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()) .unwrap_or_else(|| PathBuf::from(".")) .join(path) }; @@ -203,8 +191,10 @@ pub fn resolve_absolute_dir_with_base>( if absolute_dir.is_dir() { Ok(absolute_dir) } else { - let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); - trace::warn!(msg); - Err(io::Error::new(io::ErrorKind::InvalidInput, msg)) + Err({ + let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); + trace::warn!(msg); + io::Error::new(io::ErrorKind::InvalidInput, msg) + }) } } diff --git a/src/web.rs b/src/web.rs deleted file mode 100644 index 63916cd3..00000000 --- a/src/web.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! 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 00fe0d21..1cc03ffc 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -1,8 +1,8 @@ :root { - --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-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-color: #7a430e; --intro-bg-block-1: #ffb84b; --intro-bg-block-2: #ffc66f;