Compare commits

..

No commits in common. "c1afe0e70c7405d23c50b361df20f93ef0b27441" and "026448e51108ef76d077c3d81ea7dbf5e7c2fc06" have entirely different histories.

75 changed files with 680 additions and 948 deletions

View file

@ -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] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
@ -15,45 +75,12 @@ members = [
[workspace.package] [workspace.package]
repository = "https://git.cillero.es/manuelcillero/pagetop" repository = "https://git.cillero.es/manuelcillero/pagetop"
homepage = "https://pagetop.cillero.es" homepage = "https://pagetop.cillero.es"
edition = "2024"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
authors = ["Manuel Cillero <manuel@cillero.es>"] authors = ["Manuel Cillero <manuel@cillero.es>"]
[workspace.dependencies] [workspace.dependencies]
async-trait = "0.1" actix-web = { version = "4.13", default-features = false }
axum = { version = "0.8" }
change-detection = "1.2"
chrono = "0.4"
colored = "3.1"
concat-string = "1.0"
config = { version = "0.15", default-features = false, features = ["toml"] }
figlet-rs = "1.0"
fluent-templates = "0.14"
getter-methods = "2.0"
grass = "0.13"
indexmap = "2.14"
indoc = "2.0"
itoa = "1.0"
mime_guess = "2.0"
parking_lot = "0.12"
pastey = "0.2"
path-slash = "0.2"
proc-macro2 = "1.0"
proc-macro2-diagnostics = { version = "0.10", default-features = false }
quote = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { 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 # Helpers
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" } pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" } 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-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
# PageTop # PageTop
pagetop = { version = "0.5", path = "." } 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

View file

@ -59,7 +59,7 @@ impl Extension for HelloWorld {
} }
} }
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.add_child(Html::with(|_| html! { h1 { "Hello World!" } })) .add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render() .render()

View file

@ -11,12 +11,12 @@ impl Extension for FormControls {
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
} }
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
router.route("/", web::get(form_controls)) scfg.route("/", service::web::get().to(form_controls));
} }
} }
async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn form_controls(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child( .with_child(
Intro::default() Intro::default()

View file

@ -3,15 +3,16 @@ use pagetop::prelude::*;
struct HelloName; struct HelloName;
impl Extension for HelloName { impl Extension for HelloName {
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
router.route("/hello/{name}", web::get(hello_name)) scfg.route("/hello/{name}", service::web::get().to(hello_name));
} }
} }
async fn hello_name( async fn hello_name(
request: HttpRequest, request: HttpRequest,
web::Path(name): web::Path<String>, path: service::web::Path<String>,
) -> Result<Markup, ErrorPage> { ) -> ResultPage<Markup, ErrorPage> {
let name = path.into_inner();
Page::new(request) Page::new(request)
.with_child(Html::with(move |_| { .with_child(Html::with(move |_| {
html! { html! {

View file

@ -3,12 +3,12 @@ use pagetop::prelude::*;
struct HelloWorld; struct HelloWorld;
impl Extension for HelloWorld { impl Extension for HelloWorld {
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
router.route("/", web::get(hello_world)) scfg.route("/", service::web::get().to(hello_world));
} }
} }
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(|_| { .with_child(Html::with(|_| {
html! { html! {

View file

@ -5,12 +5,12 @@ include_locales!(LOC from "examples/locale");
struct IntroColors; struct IntroColors;
impl Extension for IntroColors { impl Extension for IntroColors {
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
router.route("/", web::get(intro_colors)) scfg.route("/", service::web::get().to(intro_colors));
} }
} }
async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn intro_colors(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child( .with_child(
Intro::default() Intro::default()

View file

@ -8,11 +8,7 @@ struct SuperMenu;
impl Extension for SuperMenu { impl Extension for SuperMenu {
fn dependencies(&self) -> Vec<ExtensionRef> { fn dependencies(&self) -> Vec<ExtensionRef> {
vec![ vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
&pagetop_aliner::Aliner,
&pagetop_bootsier::Bootsier,
&pagetop::base::extension::Welcome,
]
} }
fn initialize(&self) { fn initialize(&self) {

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-aliner" name = "pagetop-aliner"
version = "0.1.0" version = "0.1.0"
edition = "2021"
description = """ description = """
Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
pagetop.workspace = true pagetop.workspace = true
[dev-dependencies]
tokio.workspace = true
[build-dependencies] [build-dependencies]
pagetop-build.workspace = true pagetop-build.workspace = true

View file

@ -64,7 +64,7 @@ o **fuerza el tema por código** en una página concreta:
use pagetop::prelude::*; use pagetop::prelude::*;
use pagetop_aliner::Aliner; use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_theme(&Aliner) .with_theme(&Aliner)
.add_child( .add_child(

View file

@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta:
use pagetop::prelude::*; use pagetop::prelude::*;
use pagetop_aliner::Aliner; use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_theme(&Aliner) .with_theme(&Aliner)
.with_child( .with_child(
@ -109,21 +109,20 @@ impl Extension for Aliner {
Some(&Self) Some(&Self)
} }
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
serve_static_files!(router, [aliner] => "/aliner"); static_files_service!(scfg, [aliner] => "/aliner");
router
} }
} }
impl Theme for Aliner { impl Theme for Aliner {
fn before_render_page_body(&self, page: &mut Page) { fn before_render_page_body(&self, page: &mut Page) {
page.alter_assets(AssetsOp::AddStyleSheet( page.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/normalize.css") StyleSheet::from("/css/normalize.css")
.with_version("8.0.1") .with_version("8.0.1")
.with_weight(-99), .with_weight(-99),
)) ))
.alter_assets(AssetsOp::AddStyleSheet( .alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/basic.css") StyleSheet::from("/css/basic.css")
.with_version(PAGETOP_VERSION) .with_version(PAGETOP_VERSION)
.with_weight(-99), .with_weight(-99),
)) ))

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-bootsier" name = "pagetop-bootsier"
version = "0.1.1" version = "0.1.1"
edition = "2021"
description = """ description = """
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles. 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
@ -18,8 +18,5 @@ authors.workspace = true
pagetop.workspace = true pagetop.workspace = true
serde.workspace = true serde.workspace = true
[dev-dependencies]
tokio.workspace = true
[build-dependencies] [build-dependencies]
pagetop-build.workspace = true pagetop-build.workspace = true

View file

@ -64,7 +64,7 @@ o **fuerza el tema por código** en una página concreta:
use pagetop::prelude::*; use pagetop::prelude::*;
use pagetop_bootsier::Bootsier; use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_theme(&Bootsier) .with_theme(&Bootsier)
.add_child( .add_child(

View file

@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta:
use pagetop::prelude::*; use pagetop::prelude::*;
use pagetop_bootsier::Bootsier; use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_theme(&Bootsier) .with_theme(&Bootsier)
.with_child( .with_child(
@ -140,10 +140,9 @@ impl Extension for Bootsier {
Some(&Self) Some(&Self)
} }
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
serve_static_files!(router, [bootsier_bs] => "/bootsier/bs"); static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs");
serve_static_files!(router, [bootsier_js] => "/bootsier/js"); static_files_service!(scfg, [bootsier_js] => "/bootsier/js");
router
} }
} }

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::theme::BreakPoint;
use crate::theme::attrs::{ScaleSize, Side}; use crate::theme::attrs::{ScaleSize, Side};
use crate::theme::BreakPoint;
// **< Margin >************************************************************************************* // **< Margin >*************************************************************************************

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER;
/// Componente para crear un **menú desplegable** ([`dropdown`]). /// Componente para crear un **menú desplegable** ([`dropdown`]).
/// ///

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::form; use crate::theme::form;
use crate::LOCALES_BOOTSIER;
/// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*). /// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*).
/// ///

View file

@ -2,8 +2,8 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::form; use crate::theme::form;
use crate::LOCALES_BOOTSIER;
use std::fmt; use std::fmt;

View file

@ -2,8 +2,8 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::form; use crate::theme::form;
use crate::LOCALES_BOOTSIER;
// **< Item >*************************************************************************************** // **< Item >***************************************************************************************

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::form; use crate::theme::form;
use crate::LOCALES_BOOTSIER;
/// Componente para crear un **área de texto** de formulario. /// Componente para crear un **área de texto** de formulario.
/// ///

View file

@ -55,7 +55,7 @@ impl Component for Image {
{ {
(logo.render(cx)) (logo.render(cx))
} }
}); })
} }
image::Source::Responsive(source) => Some(source), image::Source::Responsive(source) => Some(source),
image::Source::Thumbnail(source) => Some(source), image::Source::Thumbnail(source) => Some(source),

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER;
// **< ItemKind >*********************************************************************************** // **< ItemKind >***********************************************************************************

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER;
const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas"; const TOGGLE_OFFCANVAS: &str = "offcanvas";

View file

@ -1,7 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER;
/// Componente para crear un **panel lateral deslizante** ([`offcanvas`]). /// Componente para crear un **panel lateral deslizante** ([`offcanvas`]).
/// ///

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-seaorm" name = "pagetop-seaorm"
version = "0.0.4" version = "0.0.4"
edition = "2021"
description = """ description = """
Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales. 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
@ -20,10 +20,17 @@ postgres = ["sea-orm/sqlx-postgres"]
sqlite = ["sea-orm/sqlx-sqlite"] sqlite = ["sea-orm/sqlx-sqlite"]
[dependencies] [dependencies]
async-trait.workspace = true
pagetop.workspace = true pagetop.workspace = true
sea-orm.workspace = true
sea-schema.workspace = true
serde.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"

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-build" name = "pagetop-build"
version = "0.3.2" version = "0.3.2"
edition = "2021"
description = """ description = """
Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
grass.workspace = true grass = "0.13"
pagetop-statics.workspace = true pagetop-statics.workspace = true

View file

@ -94,7 +94,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro
usen nombres diferentes. usen nombres diferentes.
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos 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: para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
```rust,ignore ```rust,ignore
@ -105,7 +105,7 @@ pub struct MyExtension;
impl Extension for MyExtension { impl Extension for MyExtension {
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`. // Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { 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");
} }
} }
``` ```

View file

@ -95,7 +95,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro
usen nombres diferentes. usen nombres diferentes.
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos 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: para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
```rust,ignore ```rust,ignore
@ -104,10 +104,9 @@ use pagetop::prelude::*;
pub struct MyExtension; pub struct MyExtension;
impl Extension for MyExtension { impl Extension for MyExtension {
/// Registra los recursos de `guides` en el router bajo `/ruta/a/guides`. /// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
fn configure_router(&self, mut router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
serve_static_files!(router, [guides] => "/ruta/a/guides"); static_files_service!(scfg, guides => "/ruta/a/guides");
router
} }
} }
``` ```
@ -117,10 +116,10 @@ impl Extension for MyExtension {
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
)] )]
use grass::{Options, OutputStyle, from_path}; use grass::{from_path, Options, OutputStyle};
use pagetop_statics::{ResourceDir, resource_dir}; 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::io::Write;
use std::path::Path; use std::path::Path;

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-macros" name = "pagetop-macros"
version = "0.3.0" version = "0.3.0"
edition = "2021"
description = """ description = """
Una colección de macros que mejoran la experiencia de desarrollo con PageTop. 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
@ -18,7 +18,7 @@ authors.workspace = true
proc-macro = true proc-macro = true
[dependencies] [dependencies]
proc-macro2.workspace = true proc-macro2 = "1.0"
proc-macro2-diagnostics.workspace = true proc-macro2-diagnostics = { version = "0.10", default-features = false }
quote.workspace = true quote = "1.0"
syn.workspace = true syn = { version = "2.0", features = ["full", "extra-traits"] }

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-minimal" name = "pagetop-minimal"
version = "0.1.0" version = "0.1.0"
edition = "2021"
description = """ description = """
Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
concat-string.workspace = true concat-string = "1.0"
indoc.workspace = true indoc = "2.0"
pastey.workspace = true pastey = "0.2"

View file

@ -1,6 +1,7 @@
[package] [package]
name = "pagetop-statics" name = "pagetop-statics"
version = "0.1.3" version = "0.1.3"
edition = "2021"
description = """ description = """
Librería para automatizar la recopilación de recursos estáticos en PageTop. 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 repository.workspace = true
homepage.workspace = true homepage.workspace = true
edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
@ -19,11 +19,15 @@ default = ["change-detection"]
sort = [] sort = []
[dependencies] [dependencies]
change-detection = { workspace = true, optional = true } change-detection = { version = "1.2", optional = true }
mime_guess.workspace = true mime_guess = "2.0"
path-slash.workspace = true 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] [build-dependencies]
change-detection = { workspace = true, optional = true } change-detection = { version = "1.2", optional = true }
mime_guess.workspace = true mime_guess = "2.0"
path-slash.workspace = true path-slash = "0.2"

View file

@ -13,7 +13,7 @@ use resource_dir::resource_dir;
mod sets { mod sets {
include!("src/sets.rs"); include!("src/sets.rs");
} }
use sets::{SplitByCount, generate_resources_sets}; use sets::{generate_resources_sets, SplitByCount};
use std::{env, path::Path}; use std::{env, path::Path};

View file

@ -1,4 +1,4 @@
use super::sets::{SplitByCount, generate_resources_sets}; use super::sets::{generate_resources_sets, SplitByCount};
use std::{ use std::{
env, io, env, io,
path::{Path, PathBuf}, path::{Path, PathBuf},

View file

@ -5,8 +5,8 @@ use std::{
}; };
use super::resource::{ use super::resource::{
DEFAULT_VARIABLE_NAME, collect_resources, generate_function_end, generate_function_header, collect_resources, generate_function_end, generate_function_header, generate_resource_insert,
generate_resource_insert, generate_uses, generate_variable_header, generate_variable_return, generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME,
}; };
/// Defines the split strategie. /// Defines the split strategie.

View file

@ -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"

View file

@ -5,21 +5,25 @@ mod figfont;
use crate::core::{extension, extension::ExtensionRef}; use crate::core::{extension, extension::ExtensionRef};
use crate::html::Markup; use crate::html::Markup;
use crate::locale::Locale; use crate::locale::Locale;
use crate::response::page::ErrorPage; use crate::response::page::{ErrorPage, ResultPage};
use crate::web::{HttpRequest, Router}; use crate::service::HttpRequest;
use crate::{PAGETOP_VERSION, global, trace}; 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::io::Error;
use std::sync::LazyLock; use std::sync::LazyLock;
/// Punto de entrada de una aplicación PageTop. /// Punto de entrada de una aplicación PageTop.
/// ///
/// No almacena datos, **encapsula** el inicio completo de la configuración y puesta en marcha de la /// No almacena datos, **encapsula** el inicio completo de configuración y puesta en marcha. Para
/// aplicación. Para instanciarla se puede usar [`new()`](Application::new) o /// instanciarla se puede usar [`new()`](Application::new) o [`prepare()`](Application::prepare).
/// [`prepare()`](Application::prepare). Después sólo hay que llamar a [`run()`](Application::run) /// Después sólo hay que llamar a [`run()`](Application::run) para ejecutar la aplicación (o a
/// para ejecutar la aplicación (o a [`test()`](Application::test) si se está preparando un entorno /// [`test()`](Application::test) si se está preparando un entorno de pruebas).
/// de pruebas).
pub struct Application; pub struct Application;
impl Default for Application { impl Default for Application {
@ -29,24 +33,24 @@ impl Default for Application {
} }
impl Application { impl Application {
/// Crea una instancia mínima de la aplicación, sin extensión raíz. /// Crea una instancia de la aplicación.
///
/// Útil para verificar que el servidor arranca correctamente. Para una aplicación real, usa
/// [`prepare()`](Application::prepare) con una extensión raíz.
pub fn new() -> Self { pub fn new() -> Self {
Self::internal_prepare(None) Self::internal_prepare(None)
} }
/// Prepara una instancia de la aplicación a partir de una extensión raíz. /// Prepara una instancia de la aplicación a partir de una extensión raíz.
/// ///
/// Las dependencias se habilitan en orden: primero las que no dependen de ninguna otra, luego /// Esa extensión suele declarar:
/// las que dependen de extensiones ya habilitadas, y así sucesivamente hasta dejar habilitada ///
/// la extensión raíz. /// - 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 { pub fn prepare(root_extension: ExtensionRef) -> Self {
Self::internal_prepare(Some(root_extension)) 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<ExtensionRef>) -> Self { fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self {
// Al arrancar muestra una cabecera para la aplicación. // Al arrancar muestra una cabecera para la aplicación.
Self::show_banner(); Self::show_banner();
@ -69,10 +73,10 @@ impl Application {
Self 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() { fn show_banner() {
use colored::Colorize; use colored::Colorize;
use terminal_size::{Width, terminal_size}; use terminal_size::{terminal_size, Width};
if global::SETTINGS.app.startup_banner != global::StartupBanner::Off { if global::SETTINGS.app.startup_banner != global::StartupBanner::Off {
// Nombre de la aplicación, ajustado al ancho del terminal si es necesario. // 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 let Some((Width(term_width), _)) = terminal_size() {
if term_width >= 80 { if term_width >= 80 {
let maxlen: usize = ((term_width / 10) - 2).into(); let maxlen: usize = ((term_width / 10) - 2).into();
let mut app: String = app_name.chars().take(maxlen).collect(); let mut app = app_name.substring(0, maxlen).to_string();
if app_name.chars().count() > maxlen { if app_name.len() > maxlen {
app = format!("{app}..."); app = format!("{app}...");
} }
if let Some(ff) = figfont::FIGFONT.convert(&app) { if let Some(ff) = figfont::FIGFONT.convert(&app) {
@ -99,7 +103,7 @@ impl Application {
// Descripción de la aplicación. // Descripción de la aplicación.
if !global::SETTINGS.app.description.is_empty() { if !global::SETTINGS.app.description.is_empty() {
println!("{}", global::SETTINGS.app.description.cyan()); println!("{}", global::SETTINGS.app.description.cyan());
} };
// Versión de PageTop. // Versión de PageTop.
println!( 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. /// 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 /// Devuelve [`std::io::Error`] si el *socket* no puede enlazarse (por puerto en uso, permisos,
/// el puerto ya está en uso o el proceso carece de permisos) y devuelve un [`Future`] que /// etc.).
/// ejecuta el bucle de atención de peticiones. El patrón habitual es: pub fn run(self) -> Result<service::Server, Error> {
/// // Genera clave secreta para firmar y verificar cookies.
/// ```rust,no_run let secret_key = service::cookie::Key::generate();
/// 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<impl Future<Output = Result<(), Error>>, Error> {
let addr = format!(
"{}:{}",
global::SETTINGS.server.bind_address,
global::SETTINGS.server.bind_port
);
// Enlaza el puerto de forma síncrona para detectar errores antes del *await*. // Prepara el servidor web.
let std_listener = std::net::TcpListener::bind(&addr)?; Ok(service::HttpServer::new(move || {
std_listener.set_nonblocking(true)?; Self::service_app()
.wrap(tracing_actix_web::TracingLogger::default())
let router = Self::build_router(); .wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
Ok(async move { .session_lifecycle(match global::SETTINGS.server.session_lifetime {
let listener = tokio::net::TcpListener::from_std(std_listener)?; 0 => SessionLifecycle::BrowserSession(BrowserSession::default()),
axum::serve(listener, router).await _ => 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. /// Prepara el servidor web de la aplicación para pruebas.
pub fn test(self) -> Router { pub fn test(
Self::build_router() self,
) -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
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<service::BoxBody>,
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<Markup, ErrorPage> { async fn service_not_found(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Err(ErrorPage::NotFound(request)) Err(ErrorPage::NotFound(request))
} }

View file

@ -114,7 +114,7 @@ impl Component for Intro {
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
cx.alter_assets(AssetsOp::AddStyleSheet( 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 { if *self.opening() == IntroOpening::PageTop {
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx| cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|

View file

@ -20,12 +20,12 @@ impl Extension for Welcome {
L10n::l("welcome_extension_description") L10n::l("welcome_extension_description")
} }
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
router.route("/", web::get(home)) scfg.route("/", service::web::get().to(home));
} }
} }
async fn home(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn home(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
let app = &global::SETTINGS.app.name; let app = &global::SETTINGS.app.name;
Page::new(request) Page::new(request)

View file

@ -13,12 +13,12 @@ impl Extension for Basic {
impl Theme for Basic { impl Theme for Basic {
fn before_render_page_body(&self, page: &mut Page) { fn before_render_page_body(&self, page: &mut Page) {
page.alter_assets(AssetsOp::AddStyleSheet( page.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/normalize.css") StyleSheet::from("/css/normalize.css")
.with_version("8.0.1") .with_version("8.0.1")
.with_weight(-99), .with_weight(-99),
)) ))
.alter_assets(AssetsOp::AddStyleSheet( .alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/pagetop/css/basic.css") StyleSheet::from("/css/basic.css")
.with_version(PAGETOP_VERSION) .with_version(PAGETOP_VERSION)
.with_weight(-99), .with_weight(-99),
)) ))

View file

@ -7,11 +7,11 @@
//! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada //! **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. //! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente.
//! //!
//!
//! # Orden de carga //! # Orden de carga
//! //!
//! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del //! 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 //! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación.
//! cambiar esta ubicación mediante la variable de entorno `CONFIG_DIR`.
//! //!
//! PageTop carga en este orden, y siempre de forma opcional, los siguientes archivos TOML: //! 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 //! Los archivos se combinan en el orden anterior, cada archivo sobrescribe a los anteriores en caso
//! de conflicto. //! de conflicto.
//! //!
//!
//! # Cómo añadir opciones de configuración a tu código //! # 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`: //! 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. //! Las estructuras de configuración son de **sólo lectura** durante la ejecución.
//! //!
//!
//! # Usando tus opciones de configuración //! # Usando tus opciones de configuración
//! //!
//! ```rust,ignore //! ```rust,ignore
@ -129,14 +131,9 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into()); 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)); 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 // Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Si no está definida, se usa
// fija en "test" en tiempo de compilación, sin manipular el entorno. En caso contrario se lee // por defecto DEFAULT_RUN_MODE (p. ej. PAGETOP_RUN_MODE=production).
// de PAGETOP_RUN_MODE, o se usa DEFAULT_RUN_MODE si la variable no está definida. let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into());
let rm = if cfg!(feature = "testing") {
"test".to_string()
} else {
env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into())
};
Config::builder() Config::builder()
// 1. Configuración común para todos los entornos (common.toml). // 1. Configuración común para todos los entornos (common.toml).
@ -161,7 +158,7 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
/// Hay que añadir en nuestra librería el siguiente código: /// Hay que añadir en nuestra librería el siguiente código:
/// ///
/// ```rust,ignore /// ```rust,ignore
/// include_config!(SETTINGS_NAME: SettingsType => [ /// include_config!(SETTINGS: Settings => [
/// "ruta.clave" => valor, /// "ruta.clave" => valor,
/// // ... /// // ...
/// ]); /// ]);
@ -171,8 +168,8 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
/// ///
/// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los /// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los
/// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio. /// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio.
/// * **`SettingsType`** es la estructura que define los tipos para deserializar la configuración. /// * **`Settings_Type`** es la referencia a la estructura que define los tipos para deserializar la
/// Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`). /// 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 /// * **Lista de pares** con las claves TOML que requieran valores por defecto. Siguen la notación
/// `"seccion.subclave"` para coincidir con el árbol TOML. /// `"seccion.subclave"` para coincidir con el árbol TOML.
/// ///
@ -214,7 +211,7 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
/// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p. ej. `[blog]`) para /// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p. ej. `[blog]`) para
/// evitar colisiones con otras librerías. /// 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 /// configurar distintos entornos (*dev*, *staging*, *prod*) usa los archivos TOML descritos en la
/// documentación de [`config`](crate::config). /// documentación de [`config`](crate::config).
/// ///
@ -223,8 +220,8 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
/// ///
/// # Requisitos /// # Requisitos
/// ///
/// * Las claves deben coincidir con los campos (*snake case*) de la estructura de ajustes. /// * Dependencia `serde` con la *feature* `derive`.
/// * Añade `serde` con la *feature* `derive` en *Cargo.toml*: /// * Las claves deben coincidir con los campos (*snake case*) de tu estructura `Settings_Type`.
/// ///
/// ```toml /// ```toml
/// [dependencies] /// [dependencies]
@ -232,10 +229,10 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! include_config { 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!( #[doc = concat!(
"Ajustes de configuración y **valores por defecto** para ", "Ajustes de configuración y **valores por defecto** para ",
"[`", stringify!($settings_type), "`]." "[`", stringify!($Settings_Type), "`]."
)] )]
#[doc = ""] #[doc = ""]
#[doc = "Valores predeterminados que se aplican en ausencia de configuración:"] #[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 = concat!($k, " = ", stringify!($v))]
)* )*
#[doc = "```"] #[doc = "```"]
pub static $SETTINGS_NAME: std::sync::LazyLock<$settings_type> = pub static $SETTINGS_NAME: std::sync::LazyLock<$Settings_Type> =
std::sync::LazyLock::new(|| { std::sync::LazyLock::new(|| {
let mut settings = $crate::config::CONFIG_VALUES.clone(); let mut settings = $crate::config::CONFIG_VALUES.clone();
$( $(
settings = settings.set_default($k, $v) settings = settings.set_default($k, $v).unwrap();
.expect(concat!("Failed to set default for key ", $k));
)* )*
settings settings
.build() .build()
.expect(concat!("Failed to build config for ", stringify!($settings_type))) .expect(concat!("Failed to build config for ", stringify!($Settings_Type)))
.try_deserialize::<$settings_type>() .try_deserialize::<$Settings_Type>()
.expect(concat!("Error parsing settings for ", stringify!($settings_type))) .expect(concat!("Error parsing settings for ", stringify!($Settings_Type)))
}); });
}; };
} }

View file

@ -30,22 +30,28 @@ impl TypeInfo {
} }
} }
// Extrae un rango de segmentos de `type_name` (tokens separados por `::`). /// 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 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 positivos cuentan **desde la izquierda**, empezando en `0`.
// * Los índices negativos cuentan desde la derecha; -1 es el último. /// * Los índices negativos cuentan **desde la derecha**, `-1` es el último.
// * Si `end` es `None`, el corte llega hasta el último segmento. /// * 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 "". /// * 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<i32>": ///
// /// Ejemplos (con `type_name = "alloc::vec::Vec<i32>"`):
// partial(..., 0, None) => "alloc::vec::Vec<i32>" ///
// partial(..., 1, None) => "vec::Vec<i32>" /// | Llamada | Resultado |
// partial(..., -1, None) => "Vec<i32>" /// |------------------------------|--------------------------|
// partial(..., 0, Some(-2)) => "alloc::vec" /// | `partial(..., 0, None)` | `"alloc::vec::Vec<i32>"` |
// partial(..., -5, None) => "alloc::vec::Vec<i32>" /// | `partial(..., 1, None)` | `"vec::Vec<i32>"` |
/// | `partial(..., -1, None)` | `"Vec<i32>"` |
/// | `partial(..., 0, Some(-2))` | `"alloc::vec"` |
/// | `partial(..., -5, None)` | `"alloc::vec::Vec<i32>"` |
///
/// 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<isize>) -> &'static str { fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
let maxlen = type_name.len(); let maxlen = type_name.len();
@ -53,7 +59,7 @@ impl TypeInfo {
let mut segments = Vec::new(); let mut segments = Vec::new();
let mut segment_start = 0; // Posición inicial del segmento actual. let mut segment_start = 0; // Posición inicial del segmento actual.
let mut angle_brackets = 0; // Profundidad dentro de '<...>'. 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() { for (idx, c) in type_name.char_indices() {
match c { match c {

View file

@ -1,7 +1,7 @@
use crate::AutoDefault;
use crate::core::AnyCast;
use crate::core::action::{ActionBox, ActionDispatcher}; use crate::core::action::{ActionBox, ActionDispatcher};
use crate::core::AnyCast;
use crate::trace; use crate::trace;
use crate::AutoDefault;
use parking_lot::RwLock; use parking_lot::RwLock;

View file

@ -1,6 +1,6 @@
use crate::core::component::{Component, Context}; use crate::core::component::{Component, Context};
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::{AutoDefault, UniqueId, builder_fn}; use crate::{builder_fn, AutoDefault, UniqueId};
use parking_lot::Mutex; use parking_lot::Mutex;

View file

@ -1,13 +1,13 @@
use crate::core::TypeInfo;
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::all::DEFAULT_THEME;
use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef}; 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::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{Markup, RoutePath, html};
use crate::locale::L10n; use crate::locale::L10n;
use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
use crate::web::HttpRequest; use crate::service::HttpRequest;
use crate::{CowStr, builder_fn, util}; use crate::{builder_fn, util, CowStr};
use std::any::Any; use std::any::Any;
use std::cell::Cell; use std::cell::Cell;

View file

@ -2,7 +2,7 @@ use crate::base::action;
use crate::core::component::{ComponentError, Context, Contextual}; use crate::core::component::{ComponentError, Context, Contextual};
use crate::core::theme::ThemeRef; use crate::core::theme::ThemeRef;
use crate::core::{AnyInfo, TypeInfo}; use crate::core::{AnyInfo, TypeInfo};
use crate::html::{Markup, html}; use crate::html::{html, Markup};
/// Permite clonar un componente. /// Permite clonar un componente.
/// ///

View file

@ -1,4 +1,4 @@
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::{AutoDefault, Getters}; use crate::{AutoDefault, Getters};
/// Error producido durante el renderizado de un componente. /// Error producido durante el renderizado de un componente.

View file

@ -1,43 +1,60 @@
use crate::core::action::add_action; use crate::core::action::add_action;
use crate::core::extension::ExtensionRef; use crate::core::extension::ExtensionRef;
use crate::core::theme::all::THEMES; use crate::core::theme::all::THEMES;
use crate::web::Router; use crate::{global, service, static_files_service, trace};
use crate::{global, serve_static_files, trace, web};
use std::sync::OnceLock; use parking_lot::RwLock;
static EXTENSIONS: OnceLock<Vec<ExtensionRef>> = OnceLock::new(); use std::sync::LazyLock;
// **< EXTENSIONES >********************************************************************************
static ENABLED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
static DROPPED_EXTENSIONS: LazyLock<RwLock<Vec<ExtensionRef>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
// **< REGISTRO DE LAS EXTENSIONES >**************************************************************** // **< REGISTRO DE LAS EXTENSIONES >****************************************************************
pub fn register_extensions(root_extension: Option<ExtensionRef>) { pub fn register_extensions(root_extension: Option<ExtensionRef>) {
// Garantiza que ocurre sólo una vez cuando los tests se ejecutan en paralelo. // Prepara la lista de extensiones habilitadas.
EXTENSIONS.get_or_init(|| { let mut enabled_list: Vec<ExtensionRef> = Vec::new();
let mut list: Vec<ExtensionRef> = Vec::new();
// Primero añade el tema básico a la lista de extensiones habilitadas. // Primero añade el tema básico a la lista de extensiones habilitadas.
add_to_enabled(&mut list, &crate::base::theme::Basic); 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. // Si se proporciona una extensión raíz inicial, se añade a la lista de extensiones habilitadas.
if let Some(extension) = root_extension { if let Some(extension) = root_extension {
add_to_enabled(&mut list, extension); add_to_enabled(&mut enabled_list, extension);
} }
// Añade la página de bienvenida si no hay extensión raíz. // Añade la página de bienvenida predefinida si se habilita en la configuración.
if root_extension.is_none() { if global::SETTINGS.app.welcome {
add_to_enabled(&mut list, &crate::base::extension::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<ExtensionRef> = 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<ExtensionRef>, extension: ExtensionRef) { fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
// Verifica que la extensión no esté en la lista para evitar duplicados. // Verifica que la extensión no esté en la lista para evitar duplicados.
if !list.iter().any(|e| e.type_id() == extension.type_id()) { if !list.iter().any(|e| e.type_id() == extension.type_id()) {
// Añade primero (en orden inverso) las dependencias de la extensión. // Añade primero (en orden inverso) las dependencias de la extensión.
for d in extension.dependencies().into_iter().rev() { for d in extension.dependencies().iter().rev() {
add_to_enabled(list, d); add_to_enabled(list, *d);
} }
// Añade la propia extensión a la lista. // Añade la propia extensión a la lista.
@ -60,11 +77,40 @@ fn add_to_enabled(list: &mut Vec<ExtensionRef>, extension: ExtensionRef) {
} }
} }
fn add_to_dropped(list: &mut Vec<ExtensionRef>, 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 >******************************************************************* // **< REGISTRO DE LAS ACCIONES >*******************************************************************
pub fn register_actions() { pub fn register_actions() {
for extension in EXTENSIONS.get().into_iter().flatten() { for extension in ENABLED_EXTENSIONS.read().iter() {
for a in extension.actions() { for a in extension.actions().into_iter() {
add_action(a); add_action(a);
} }
} }
@ -74,28 +120,25 @@ pub fn register_actions() {
pub fn initialize_extensions() { pub fn initialize_extensions() {
trace::info!("Calling application bootstrap"); trace::info!("Calling application bootstrap");
for e in EXTENSIONS.get().into_iter().flatten() { for extension in ENABLED_EXTENSIONS.read().iter() {
e.initialize(); 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. // Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos.
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let router = router.route( scfg.route(
// Ruta automática lanzada por Chrome DevTools.
"/.well-known/appspecific/com.chrome.devtools.json", "/.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 for extension in ENABLED_EXTENSIONS.read().iter() {
.get() extension.configure_service(scfg);
.into_iter() }
.flatten()
.fold(router, |r, e| e.configure_router(r));
serve_static_files!(router, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/pagetop"); static_files_service!(scfg, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/");
router
} }

View file

@ -1,9 +1,8 @@
use crate::actions;
use crate::core::AnyInfo;
use crate::core::action::ActionBox; use crate::core::action::ActionBox;
use crate::core::theme::ThemeRef; use crate::core::theme::ThemeRef;
use crate::core::AnyInfo;
use crate::locale::L10n; use crate::locale::L10n;
use crate::web::Router; use crate::{actions, service};
/// Interfaz común que debe implementar cualquier extensión de PageTop. /// Interfaz común que debe implementar cualquier extensión de PageTop.
/// ///
@ -12,15 +11,15 @@ use crate::web::Router;
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct MyExtension; /// pub struct Blog;
/// ///
/// impl Extension for MyExtension { /// impl Extension for Blog {
/// fn name(&self) -> L10n { /// fn name(&self) -> L10n {
/// L10n::n("My Extension") /// L10n::n("Blog")
/// } /// }
/// ///
/// fn description(&self) -> L10n { /// 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. /// aceptar cualquier petición HTTP.
fn initialize(&self) {} 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 /// # Ejemplo
/// 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*
/// ///
/// ```rust,ignore /// ```rust,ignore
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// pub struct Api; /// pub struct ExtensionSample;
/// ///
/// impl Extension for Api { /// impl Extension for ExtensionSample {
/// fn configure_router(&self, router: Router) -> Router { /// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
/// router /// scfg.route("/sample", web::get().to(route_sample));
/// .route("/api/data", web::get(get_data))
/// .layer(auth_layer())
/// } /// }
/// } /// }
/// ``` /// ```
#[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 /// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras
/// /// implementaciones.
/// La macro [`serve_static_files!`](crate::serve_static_files) sombrea `router` internamente, fn drop_extensions(&self) -> Vec<ExtensionRef> {
/// por lo que el parámetro no necesita `mut`. Sí es necesario devolverlo al final. vec![]
///
/// ```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
} }
} }

View file

@ -28,9 +28,9 @@
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas. //! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::locale::L10n; use crate::locale::L10n;
use crate::{AutoDefault, util}; use crate::{util, AutoDefault};
// **< Region >************************************************************************************* // **< Region >*************************************************************************************

View file

@ -3,10 +3,10 @@ use crate::core::component::{ChildOp, Component, ComponentError, Context, Contex
use crate::core::extension::Extension; use crate::core::extension::Extension;
use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef}; use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef};
use crate::global; use crate::global;
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::locale::L10n; use crate::locale::L10n;
use crate::response::page::Page; 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. /// Interfaz común que debe implementar cualquier tema de PageTop.
/// ///

View file

@ -1,6 +1,6 @@
use crate::core::component::{Child, ChildOp, Children, Component}; use crate::core::component::{Child, ChildOp, Children, Component};
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef}; use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
use crate::{AutoDefault, UniqueId, builder_fn}; use crate::{builder_fn, AutoDefault, UniqueId};
use parking_lot::RwLock; use parking_lot::RwLock;

View file

@ -25,6 +25,7 @@ include_config!(SETTINGS: Settings => [
"app.theme" => "Basic", "app.theme" => "Basic",
"app.lang_negotiation" => "Full", "app.lang_negotiation" => "Full",
"app.startup_banner" => "Slant", "app.startup_banner" => "Slant",
"app.welcome" => true,
// [dev] // [dev]
"dev.pagetop_static_dir" => "", "dev.pagetop_static_dir" => "",
@ -40,6 +41,7 @@ include_config!(SETTINGS: Settings => [
// [server] // [server]
"server.bind_address" => "localhost", "server.bind_address" => "localhost",
"server.bind_port" => 8080, "server.bind_port" => 8080,
"server.session_lifetime" => 604_800,
]); ]);
// **< Settings >*********************************************************************************** // **< Settings >***********************************************************************************
@ -83,6 +85,11 @@ pub struct App {
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
/// *"Starwars"*. /// *"Starwars"*.
pub startup_banner: StartupBanner, 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 /// Modo de ejecución, dado por la variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no
/// está definido. /// está definido.
pub run_mode: String, pub run_mode: String,
@ -109,7 +116,7 @@ pub struct Log {
pub enabled: bool, pub enabled: bool,
/// Opciones, o combinación de opciones separadas por comas, para filtrar las trazas: *"Error"*, /// Opciones, o combinación de opciones separadas por comas, para filtrar las trazas: *"Error"*,
/// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*. /// *"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, pub tracing: String,
/// Muestra los mensajes de traza en el terminal (*"Stdout"*) o los vuelca en archivos con /// Muestra los mensajes de traza en el terminal (*"Stdout"*) o los vuelca en archivos con
/// rotación: *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*. /// rotación: *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*.
@ -129,4 +136,8 @@ pub struct Server {
pub bind_address: String, pub bind_address: String,
/// Puerto de escucha del servidor web. /// Puerto de escucha del servidor web.
pub bind_port: u16, 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,
} }

View file

@ -1,7 +1,7 @@
//! HTML en código. //! HTML en código.
mod maud; 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; mod route;
pub use route::RoutePath; pub use route::RoutePath;

View file

@ -3,7 +3,7 @@ pub mod javascript;
pub mod stylesheet; pub mod stylesheet;
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::{AutoDefault, Weight}; use crate::{AutoDefault, Weight};
/// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de /// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de

View file

@ -1,5 +1,5 @@
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::{AutoDefault, CowStr}; use crate::{AutoDefault, CowStr};
/// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. /// 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** /// > **Nota**
/// > Los archivos de los iconos deben estar disponibles en el servidor web de la aplicación. Pueden /// > 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 /// # Ejemplo
/// ///
@ -165,12 +165,14 @@ impl Favicon {
} }
} }
// Crea un elemento <link> para el favicon. Infiere el tipo MIME según la extensión. /// Centraliza la creación de los elementos `<link>`.
// ///
// - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.). /// - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.).
// - `href`: URL del recurso. /// - `href`: URL del recurso.
// - `sizes`: tamaños opcionales. /// - `sizes`: tamaños opcionales.
// - `color`: color opcional (solo relevante para `mask-icon`). /// - `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( fn add_icon_item(
mut self, mut self,
icon_rel: &'static str, icon_rel: &'static str,

View file

@ -1,7 +1,7 @@
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::assets::Asset; use crate::html::assets::Asset;
use crate::html::{Markup, PreEscaped, html}; use crate::html::{html, Markup, PreEscaped};
use crate::{AutoDefault, CowStr, Weight, util}; use crate::{util, AutoDefault, CowStr, Weight};
/// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador. /// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador.
/// ///
@ -39,7 +39,7 @@ enum Source {
/// ///
/// > **Nota** /// > **Nota**
/// > Los archivos de los scripts deben estar disponibles en el servidor web de la aplicación. /// > 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 /// # Ejemplo
/// ///

View file

@ -1,7 +1,7 @@
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::assets::Asset; use crate::html::assets::Asset;
use crate::html::{Markup, PreEscaped, html}; use crate::html::{html, Markup, PreEscaped};
use crate::{AutoDefault, CowStr, Weight, util}; use crate::{util, AutoDefault, CowStr, Weight};
/// Define el origen del recurso CSS y cómo se incluye en el documento. /// Define el origen del recurso CSS y cómo se incluye en el documento.
/// ///
@ -56,7 +56,7 @@ impl TargetMedia {
/// ///
/// > **Nota** /// > **Nota**
/// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden /// > 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 /// # Ejemplo
/// ///

View file

@ -1,5 +1,5 @@
use crate::locale::{L10n, LangId}; use crate::locale::{L10n, LangId};
use crate::{AutoDefault, builder_fn}; use crate::{builder_fn, AutoDefault};
/// Valor opcional para atributos HTML. /// Valor opcional para atributos HTML.
/// ///

View file

@ -1,4 +1,4 @@
use crate::{AutoDefault, CowStr, builder_fn, util}; use crate::{builder_fn, util, AutoDefault, CowStr};
use std::collections::HashSet; use std::collections::HashSet;

View file

@ -1,7 +1,7 @@
use crate::AutoDefault;
use crate::core::component::Context; use crate::core::component::Context;
use crate::html::{Markup, html}; use crate::html::{html, Markup};
use crate::locale::L10n; use crate::locale::L10n;
use crate::AutoDefault;
/// Representación SVG del **logotipo de PageTop** para incrustar en HTML. /// Representación SVG del **logotipo de PageTop** para incrustar en HTML.
/// ///

View file

@ -1,4 +1,4 @@
use crate::{AutoDefault, CowStr, builder_fn}; use crate::{builder_fn, AutoDefault, CowStr};
use std::fmt; use std::fmt;

View file

@ -53,12 +53,12 @@ use pagetop::prelude::*;
struct HelloWorld; struct HelloWorld;
impl Extension for HelloWorld { impl Extension for HelloWorld {
fn configure_router(&self, router: Router) -> Router { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
router.route("/", web::get(hello_world)) scfg.route("/", service::web::get().to(hello_world));
} }
} }
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(|_| html! { h1 { "Hello World!" } })) .with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.render() .render()
@ -117,10 +117,10 @@ use std::ops::Deref;
/// fn before_render_page_body(&self, page: &mut Page) { /// fn before_render_page_body(&self, page: &mut Page) {
/// page /// page
/// .alter_assets(AssetsOp::AddStyleSheet( /// .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( /// .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( /// .alter_assets(AssetsOp::AddStyleSheet(
/// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")), /// 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. /// referencia a la versión del *crate* que lo usa.
pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION");
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; pub use getter_methods::Getters;
@ -198,8 +198,8 @@ pub mod datetime;
pub mod core; pub mod core;
// Respuestas a peticiones web en sus diferentes formatos. // Respuestas a peticiones web en sus diferentes formatos.
pub mod response; pub mod response;
// Gestión del servidor y rutas web. // Gestión del servidor y servicios web.
pub mod web; pub mod service;
// Reúne acciones, componentes, extensiones y temas predefinidos. // Reúne acciones, componentes, extensiones y temas predefinidos.
pub mod base; pub mod base;
// Prepara y ejecuta la aplicación. // Prepara y ejecuta la aplicación.

View file

@ -1,7 +1,7 @@
use crate::{global, trace}; use crate::{global, trace};
use super::languages::LANGUAGES; use super::languages::LANGUAGES;
use super::{LanguageIdentifier, langid}; use super::{langid, LanguageIdentifier};
use std::sync::LazyLock; use std::sync::LazyLock;

View file

@ -1,5 +1,5 @@
use crate::html::{Markup, PreEscaped}; use crate::html::{Markup, PreEscaped};
use crate::{AutoDefault, CowStr, include_locales}; use crate::{include_locales, AutoDefault, CowStr};
use super::{LangId, Locale}; use super::{LangId, Locale};

View file

@ -1,6 +1,6 @@
use crate::util; use crate::util;
use super::{LanguageIdentifier, langid}; use super::{langid, LanguageIdentifier};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::LazyLock; use std::sync::LazyLock;

View file

@ -1,5 +1,5 @@
use crate::global; use crate::global;
use crate::web::HttpRequest; use crate::service::HttpRequest;
use super::{LangId, LanguageIdentifier, Locale}; use super::{LangId, LanguageIdentifier, Locale};

View file

@ -14,8 +14,8 @@ pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight}
pub use crate::include_config; pub use crate::include_config;
// crate::locale // crate::locale
pub use crate::include_locales; pub use crate::include_locales;
// crate::web // crate::service
pub use crate::serve_static_files; pub use crate::static_files_service;
// crate::core::action // crate::core::action
pub use crate::actions; pub use crate::actions;
// crate::core::theme // crate::core::theme
@ -35,8 +35,8 @@ pub use crate::locale::*;
pub use crate::datetime::*; pub use crate::datetime::*;
pub use crate::web; pub use crate::service;
pub use crate::web::{HttpRequest, Router}; pub use crate::service::{HttpMessage, HttpRequest, HttpResponse};
pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; 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::extension::*;
pub use crate::core::theme::*; 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::action;
pub use crate::base::component::*; pub use crate::base::component::*;

View file

@ -1,5 +1,7 @@
//! Respuestas a las peticiones web en sus diferentes formatos. //! Respuestas a las peticiones web en sus diferentes formatos.
pub use actix_web::ResponseError;
pub mod page; pub mod page;
pub mod json; pub mod json;

View file

@ -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 //! # Uso como extractor JSON
//! //!
@ -11,10 +11,10 @@
//! struct NuevoUsuario { nombre: String, email: String } //! struct NuevoUsuario { nombre: String, email: String }
//! //!
//! /// Manejador configurado para la ruta POST "/usuarios". //! /// Manejador configurado para la ruta POST "/usuarios".
//! async fn crear_usuario(payload: Json<NuevoUsuario>) -> web::http::StatusCode { //! async fn crear_usuario(payload: Json<NuevoUsuario>) -> HttpResponse {
//! // `payload` ya es `NuevoUsuario`; si la deserialización falla, //! // `payload` ya es `NuevoUsuario`; si la deserialización falla,
//! // devolverá automáticamente 400 Bad Request. //! // devolverá automáticamente 400 Bad Request con un cuerpo JSON que describe el error.
//! web::http::StatusCode::OK //! HttpResponse::Ok().finish()
//! } //! }
//! ``` //! ```
//! //!
@ -36,4 +36,4 @@
//! `Json<T>` funciona con cualquier tipo que implemente `serde::Serialize` (para respuestas) y/o //! `Json<T>` funciona con cualquier tipo que implemente `serde::Serialize` (para respuestas) y/o
//! `serde::Deserialize` (para peticiones). //! `serde::Deserialize` (para peticiones).
pub use axum::Json; pub use actix_web::web::Json;

View file

@ -16,16 +16,18 @@
mod error; mod error;
pub use error::ErrorPage; pub use error::ErrorPage;
pub use actix_web::Result as ResultPage;
use crate::base::action; use crate::base::action;
use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual};
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; 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::{Assets, Favicon, JavaScript, StyleSheet};
use crate::html::{Attr, AttrId}; use crate::html::{Attr, AttrId};
use crate::html::{Classes, ClassesOp}; use crate::html::{Classes, ClassesOp};
use crate::html::{DOCTYPE, Markup, html};
use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::web::HttpRequest; use crate::service::HttpRequest;
use crate::{AutoDefault, builder_fn}; use crate::{builder_fn, AutoDefault};
// **< ReservedRegion >***************************************************************************** // **< ReservedRegion >*****************************************************************************
@ -225,8 +227,8 @@ impl Page {
/// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos /// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos
/// `lang` y `dir` en la etiqueta `<html>`. /// `lang` y `dir` en la etiqueta `<html>`.
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y /// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
/// devuelve un [`Result`] con el [`Markup`] final. /// devuelve un [`ResultPage`] con el [`Markup`] final.
pub fn render(&mut self) -> Result<Markup, ErrorPage> { pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
// Acciones específicas del tema antes de renderizar el <body>. // Acciones específicas del tema antes de renderizar el <body>.
self.context.theme().before_render_page_body(self); self.context.theme().before_render_page_body(self);

View file

@ -1,7 +1,9 @@
use crate::core::component::Contextual; use crate::core::component::Contextual;
use crate::locale::L10n; 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::util;
use crate::web::{HttpRequest, IntoResponse, Response, http};
use super::Page; use super::Page;
@ -29,9 +31,13 @@ pub enum ErrorPage {
} }
impl ErrorPage { impl ErrorPage {
// Renderiza una página de error genérica usando el tema activo. Deriva las claves de /// Función auxiliar para renderizar una página de error genérica usando el tema activo.
// localización del código de estado (`error<code>_title`, `_alert`, `_help`). Si el ///
// renderizado falla, escribe el texto plano del código de estado. /// Construye una [`Page`] a partir de la petición y un prefijo de clave basado en el código de
/// estado (`error<code>`), del que se derivan los textos localizados `error<code>_title`,
/// `error<code>_alert` y `error<code>_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 { fn display_error_page(&self, f: &mut fmt::Formatter<'_>, request: &HttpRequest) -> fmt::Result {
let mut page = Page::new(request.clone()); let mut page = Page::new(request.clone());
let code = self.status_code(); let code = self.status_code();
@ -45,19 +51,7 @@ impl ErrorPage {
if let Ok(rendered) = page.render() { if let Ok(rendered) = page.render() {
write!(f, "{}", rendered.into_string()) write!(f, "{}", rendered.into_string())
} else { } else {
f.write_str(code.as_str()) f.write_str(&code.to_string())
}
}
/// 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,
} }
} }
} }
@ -75,7 +69,7 @@ impl fmt::Display for ErrorPage {
if let Ok(rendered) = page.render() { if let Ok(rendered) = page.render() {
write!(f, "{}", rendered.into_string()) write!(f, "{}", rendered.into_string())
} else { } 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() { if let Ok(rendered) = page.render() {
write!(f, "{}", rendered.into_string()) write!(f, "{}", rendered.into_string())
} else { } 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 impl ResponseError for ErrorPage {
/// HTML generado por el tema activo. fn error_response(&self) -> HttpResponse {
impl IntoResponse for ErrorPage { HttpResponse::build(self.status_code())
fn into_response(self) -> Response { .insert_header(ContentType::html())
let status = self.status_code(); .body(self.to_string())
let body = self.to_string(); }
(
status, #[rustfmt::skip]
[(http::header::CONTENT_TYPE, "text/html; charset=utf-8")], fn status_code(&self) -> StatusCode {
body, match self {
) ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST,
.into_response() 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,
}
} }
} }

View file

@ -18,12 +18,12 @@
//! //!
//! - **Respuestas especiales**. //! - **Respuestas especiales**.
use crate::web::{IntoResponse, Response, http}; use crate::service::HttpResponse;
/// Funciones predefinidas para generar respuestas HTTP de redirección. /// Funciones predefinidas para generar respuestas HTTP de redirección.
/// ///
/// Ofrece atajos para construir respuestas con el código de estado apropiado y la cabecera /// Ofrece atajos para construir respuestas con el código de estado apropiado, añade la cabecera
/// `Location`, evitando repetir la misma secuencia en cada controlador. /// `Location` y la cierra con `.finish()`, evitando repetir la misma secuencia en cada controlador.
pub struct Redirect; pub struct Redirect;
impl 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 /// Emplear cuando un recurso se ha movido de forma definitiva y la URL antigua debe dejar de
/// usarse. /// usarse.
#[must_use] #[must_use]
pub fn moved(redirect_to_url: &str) -> Response { pub fn moved(redirect_to_url: &str) -> HttpResponse {
( HttpResponse::MovedPermanently()
http::StatusCode::MOVED_PERMANENTLY, .append_header(("Location", redirect_to_url))
[(http::header::LOCATION, redirect_to_url.to_owned())], .finish()
)
.into_response()
} }
/// Redirección **permanente**. Código de estado **308**. Mantiene método y cuerpo sin cambios. /// 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 /// 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. /// métodos distintos de GET (POST, PUT, ...) que no deben degradarse a GET.
#[must_use] #[must_use]
pub fn permanent(redirect_to_url: &str) -> Response { pub fn permanent(redirect_to_url: &str) -> HttpResponse {
( HttpResponse::PermanentRedirect()
http::StatusCode::PERMANENT_REDIRECT, .append_header(("Location", redirect_to_url))
[(http::header::LOCATION, redirect_to_url.to_owned())], .finish()
)
.into_response()
} }
/// Redirección **temporal**. Código de estado **302**. El método GET (y normalmente HEAD) se /// 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, /// Útil cuando un recurso está fuera de servicio de forma imprevista (mantenimiento breve,
/// sobrecarga, ...). /// sobrecarga, ...).
#[must_use] #[must_use]
pub fn found(redirect_to_url: &str) -> Response { pub fn found(redirect_to_url: &str) -> HttpResponse {
( HttpResponse::Found()
http::StatusCode::FOUND, .append_header(("Location", redirect_to_url))
[(http::header::LOCATION, redirect_to_url.to_owned())], .finish()
)
.into_response()
} }
/// Redirección **temporal**. Código de estado **303**. Método GET se mantiene tal cual. Los /// 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 /// 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. /// recargar la página de resultados sin volver a ejecutar la operación.
#[must_use] #[must_use]
pub fn see_other(redirect_to_url: &str) -> Response { pub fn see_other(redirect_to_url: &str) -> HttpResponse {
( HttpResponse::SeeOther()
http::StatusCode::SEE_OTHER, .append_header(("Location", redirect_to_url))
[(http::header::LOCATION, redirect_to_url.to_owned())], .finish()
)
.into_response()
} }
/// Redirección **temporal**. Código de estado **307**. Conserva método y cuerpo íntegros. /// 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 /// Preferible a [`found`](Self::found) cuando el sitio expone operaciones diferentes de GET que
/// deben respetarse durante la redirección. /// deben respetarse durante la redirección.
#[must_use] #[must_use]
pub fn temporary(redirect_to_url: &str) -> Response { pub fn temporary(redirect_to_url: &str) -> HttpResponse {
( HttpResponse::TemporaryRedirect()
http::StatusCode::TEMPORARY_REDIRECT, .append_header(("Location", redirect_to_url))
[(http::header::LOCATION, redirect_to_url.to_owned())], .finish()
)
.into_response()
} }
/// Respuesta **especial**. Código de estado **304**. Se envía tras una petición condicional, /// 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. /// No es una redirección, el cliente debe reutilizar su copia local.
#[must_use] #[must_use]
pub fn not_modified() -> Response { pub fn not_modified() -> HttpResponse {
http::StatusCode::NOT_MODIFIED.into_response() HttpResponse::NotModified().finish()
} }
} }

128
src/service.rs Normal file
View file

@ -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 [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
$scfg.service($crate::service::ResourceFiles::new(
$route,
[<static_files_ $bundle>]::$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 [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
$scfg.service($crate::service::ResourceFiles::new(
$route,
[<static_files_ $bundle>]::$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,
);
}
});
}};
}

View file

@ -58,15 +58,14 @@ pub enum NormalizeAsciiError {
/// # use pagetop::util; /// # use pagetop::util;
/// assert_eq!(util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), "foo bar cli"); /// assert_eq!(util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), "foo bar cli");
/// ``` /// ```
pub fn normalize_ascii(input: &str) -> Result<Cow<'_, str>, NormalizeAsciiError> { pub fn normalize_ascii<'a>(input: &'a str) -> Result<Cow<'a, str>, NormalizeAsciiError> {
let bytes = input.as_bytes(); let bytes = input.as_bytes();
if bytes.is_empty() { if bytes.is_empty() {
return Err(NormalizeAsciiError::IsEmpty); return Err(NormalizeAsciiError::IsEmpty);
} }
// Primera pasada, determina si se necesita asignación y calcula los límites del contenido. let mut start = 0usize;
let mut start = 0; let mut end = 0usize;
let mut end = 0;
let mut needs_alloc = false; let mut needs_alloc = false;
let mut needs_alloc_ws = false; let mut needs_alloc_ws = false;
@ -111,7 +110,6 @@ pub fn normalize_ascii(input: &str) -> Result<Cow<'_, str>, NormalizeAsciiError>
return Ok(Cow::Borrowed(slice)); return Ok(Cow::Borrowed(slice));
} }
// Segunda pasada, construye la cadena normalizada.
let mut output = String::with_capacity(slice.len()); let mut output = String::with_capacity(slice.len());
let mut prev_sep = true; let mut prev_sep = true;
@ -134,8 +132,8 @@ pub fn normalize_ascii(input: &str) -> Result<Cow<'_, str>, NormalizeAsciiError>
/// ///
/// - Devuelve `Some(Cow)` si la entrada es válida ASCII (normalizada a minúsculas). /// - 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 `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 /// - Devuelve `None` si la entrada contiene bytes non-ASCII; y emite un `trace::debug!` con el
/// `target`. /// campo `target`.
#[inline] #[inline]
pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option<Cow<'a, str>> { pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option<Cow<'a, str>> {
match normalize_ascii(input) { 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")); /// println!("{:#?}", util::resolve_absolute_dir("/var/www"));
/// ``` /// ```
pub fn resolve_absolute_dir<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> { pub fn resolve_absolute_dir<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
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<P: AsRef<Path>>(
path: P,
base: Option<PathBuf>,
) -> io::Result<PathBuf> {
let path = path.as_ref(); let path = path.as_ref();
let candidate = if path.is_absolute() { let candidate = if path.is_absolute() {
path.to_path_buf() path.to_path_buf()
} else { } else {
// Directorio base proporcionado, o current_dir() en su defecto. // Directorio base CARGO_MANIFEST_DIR si está disponible; o current_dir() en su defecto.
base.or_else(|| env::current_dir().ok()) env::var_os("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.or_else(|| env::current_dir().ok())
.unwrap_or_else(|| PathBuf::from(".")) .unwrap_or_else(|| PathBuf::from("."))
.join(path) .join(path)
}; };
@ -203,8 +191,10 @@ pub fn resolve_absolute_dir_with_base<P: AsRef<Path>>(
if absolute_dir.is_dir() { if absolute_dir.is_dir() {
Ok(absolute_dir) Ok(absolute_dir)
} else { } else {
Err({
let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); let msg = format!("path \"{}\" is not a directory", absolute_dir.display());
trace::warn!(msg); trace::warn!(msg);
Err(io::Error::new(io::ErrorKind::InvalidInput, msg)) io::Error::new(io::ErrorKind::InvalidInput, msg)
})
} }
} }

View file

@ -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<Markup, ErrorPage> { ... }
/// ```
#[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<S: Send + Sync> FromRequestParts<S> 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<Self, Self::Rejection> {
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<HashMap<&'static str, StaticResource>>,
}
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<http::Request<Body>> for ServeEmbedded {
type Response = http::Response<Body>;
type Error = Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<Body>) -> 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 [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
__r = __r.nest_service(
$path,
$crate::web::ServeEmbedded::new(
[<static_files_ $bundle>]::$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 [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
$router.nest_service(
$path,
$crate::web::ServeEmbedded::new(
[<static_files_ $bundle>]::$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<String>) -> 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<Body> {
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<Body>) -> http::Response<Body> {
router.clone().oneshot(req).await.unwrap()
}
}

View file

@ -1,8 +1,8 @@
:root { :root {
--intro-bg-img: url('/pagetop/img/intro-header.jpg'); --intro-bg-img: url('/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-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('/pagetop/img/intro-header-sm.jpg'); --intro-bg-img-sm: url('/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-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-color: #7a430e;
--intro-bg-block-1: #ffb84b; --intro-bg-block-1: #ffb84b;
--intro-bg-block-2: #ffc66f; --intro-bg-block-2: #ffc66f;