Compare commits
No commits in common. "af548b03c9e763b52dac4822905db4e69fc02df7" and "eb3157c4a4e0bc97325b9ce41bad9a7f6a74086f" have entirely different histories.
af548b03c9
...
eb3157c4a4
39 changed files with 317 additions and 932 deletions
|
|
@ -23,7 +23,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_opening(IntroOpening::Custom)
|
||||||
.with_title(L10n::t("title", &LOC))
|
.with_title(L10n::t("title", &LOC))
|
||||||
.with_slogan(L10n::t("slogan", &LOC))
|
.with_slogan(L10n::t("slogan", &LOC))
|
||||||
.with_button(None::<(L10n, Route)>)
|
.with_button(None::<(L10n, FnPathByContext)>)
|
||||||
// Bloque 1: casillas, interruptores y botones de opción.
|
// Bloque 1: casillas, interruptores y botones de opción.
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ impl Extension for HelloName {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_name(
|
async fn hello_name(
|
||||||
request: HttpRequest,
|
|
||||||
web::Path(name): web::Path<String>,
|
web::Path(name): web::Path<String>,
|
||||||
|
request: HttpRequest,
|
||||||
) -> Result<Markup, ErrorPage> {
|
) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(Html::with(move |_| {
|
.with_child(Html::with(move |_| {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_opening(IntroOpening::Custom)
|
||||||
.with_title(L10n::n("PageTop"))
|
.with_title(L10n::n("PageTop"))
|
||||||
.with_slogan(L10n::t("colors_slogan", &LOC))
|
.with_slogan(L10n::t("colors_slogan", &LOC))
|
||||||
.with_button(None::<(L10n, Route)>)
|
.with_button(None::<(L10n, FnPathByContext)>)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "1"))
|
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "1"))
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,13 @@ impl Extension for SuperMenu {
|
||||||
.with_expand(token::BreakPoint::LG)
|
.with_expand(token::BreakPoint::LG)
|
||||||
.with_item(bs::navbar::Item::nav(
|
.with_item(bs::navbar::Item::nav(
|
||||||
bs::Nav::new()
|
bs::Nav::new()
|
||||||
.with_item(bs::nav::Item::link(L10n::t("menus_item_link", &LOC), "/"))
|
.with_item(bs::nav::Item::link(
|
||||||
|
L10n::t("menus_item_link", &LOC),
|
||||||
|
|cx| cx.route("/"),
|
||||||
|
))
|
||||||
.with_item(bs::nav::Item::link_blank(
|
.with_item(bs::nav::Item::link_blank(
|
||||||
L10n::t("menus_item_blank", &LOC),
|
L10n::t("menus_item_blank", &LOC),
|
||||||
"https://docs.rs/pagetop",
|
|_| "https://docs.rs/pagetop".into(),
|
||||||
))
|
))
|
||||||
.with_item(bs::nav::Item::dropdown(
|
.with_item(bs::nav::Item::dropdown(
|
||||||
bs::Dropdown::new()
|
bs::Dropdown::new()
|
||||||
|
|
@ -34,15 +37,15 @@ impl Extension for SuperMenu {
|
||||||
)))
|
)))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_dev_getting_started", &LOC),
|
L10n::t("menus_dev_getting_started", &LOC),
|
||||||
"/dev/getting-started",
|
|cx| cx.route("/dev/getting-started"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_dev_guides", &LOC),
|
L10n::t("menus_dev_guides", &LOC),
|
||||||
"/dev/guides",
|
|cx| cx.route("/dev/guides"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::link_blank(
|
.with_item(bs::dropdown::Item::link_blank(
|
||||||
L10n::t("menus_dev_forum", &LOC),
|
L10n::t("menus_dev_forum", &LOC),
|
||||||
"https://forum.example.dev",
|
|_| "https://forum.example.dev".into(),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::divider())
|
.with_item(bs::dropdown::Item::divider())
|
||||||
.with_item(bs::dropdown::Item::header(L10n::t(
|
.with_item(bs::dropdown::Item::header(L10n::t(
|
||||||
|
|
@ -51,15 +54,15 @@ impl Extension for SuperMenu {
|
||||||
)))
|
)))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_sdk_rust", &LOC),
|
L10n::t("menus_sdk_rust", &LOC),
|
||||||
"/dev/sdks/rust",
|
|cx| cx.route("/dev/sdks/rust"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_sdk_js", &LOC),
|
L10n::t("menus_sdk_js", &LOC),
|
||||||
"/dev/sdks/js",
|
|cx| cx.route("/dev/sdks/js"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_sdk_python", &LOC),
|
L10n::t("menus_sdk_python", &LOC),
|
||||||
"/dev/sdks/python",
|
|cx| cx.route("/dev/sdks/python"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::divider())
|
.with_item(bs::dropdown::Item::divider())
|
||||||
.with_item(bs::dropdown::Item::header(L10n::t(
|
.with_item(bs::dropdown::Item::header(L10n::t(
|
||||||
|
|
@ -68,22 +71,22 @@ impl Extension for SuperMenu {
|
||||||
)))
|
)))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_plugin_auth", &LOC),
|
L10n::t("menus_plugin_auth", &LOC),
|
||||||
"/dev/sdks/rust/plugins/auth",
|
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::link(
|
.with_item(bs::dropdown::Item::link(
|
||||||
L10n::t("menus_plugin_cache", &LOC),
|
L10n::t("menus_plugin_cache", &LOC),
|
||||||
"/dev/sdks/rust/plugins/cache",
|
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
||||||
))
|
))
|
||||||
.with_item(bs::dropdown::Item::divider())
|
.with_item(bs::dropdown::Item::divider())
|
||||||
.with_item(bs::dropdown::Item::label(L10n::t("menus_item_label", &LOC)))
|
.with_item(bs::dropdown::Item::label(L10n::t("menus_item_label", &LOC)))
|
||||||
.with_item(bs::dropdown::Item::link_disabled(
|
.with_item(bs::dropdown::Item::link_disabled(
|
||||||
L10n::t("menus_item_disabled", &LOC),
|
L10n::t("menus_item_disabled", &LOC),
|
||||||
"#",
|
|cx| cx.route("#"),
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.with_item(bs::nav::Item::link_disabled(
|
.with_item(bs::nav::Item::link_disabled(
|
||||||
L10n::t("menus_item_disabled", &LOC),
|
L10n::t("menus_item_disabled", &LOC),
|
||||||
"#",
|
|cx| cx.route("#"),
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
.with_item(bs::navbar::Item::nav(
|
.with_item(bs::navbar::Item::nav(
|
||||||
|
|
@ -94,11 +97,11 @@ impl Extension for SuperMenu {
|
||||||
)))
|
)))
|
||||||
.with_item(bs::nav::Item::link(
|
.with_item(bs::nav::Item::link(
|
||||||
L10n::t("menus_item_sign_up", &LOC),
|
L10n::t("menus_item_sign_up", &LOC),
|
||||||
"/auth/sign-up",
|
|cx| cx.route("/auth/sign-up"),
|
||||||
))
|
))
|
||||||
.with_item(bs::nav::Item::link(
|
.with_item(bs::nav::Item::link(
|
||||||
L10n::t("menus_item_login", &LOC),
|
L10n::t("menus_item_login", &LOC),
|
||||||
"/auth/login",
|
|cx| cx.route("/auth/login"),
|
||||||
)),
|
)),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ impl Theme for Aliner {
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegions::Footer,
|
&DefaultRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
/// Generador de respuestas HTML parciales con cabeceras HTMX.
|
/// Generador de respuestas HTML parciales con cabeceras HTMX.
|
||||||
///
|
///
|
||||||
/// En una aplicación HTMX, los handlers del servidor devuelven con frecuencia fragmentos HTML
|
/// En una aplicación HTMX, los *handlers* del servidor devuelven con frecuencia fragmentos HTML
|
||||||
/// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la
|
/// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la
|
||||||
/// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc.
|
/// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc.
|
||||||
///
|
///
|
||||||
/// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse
|
/// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse
|
||||||
/// directamente desde cualquier handler.
|
/// directamente desde cualquier *handler*.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@
|
||||||
//!
|
//!
|
||||||
//! Estas funciones integran los valores como literales escapados, no como parámetros de base de
|
//! Estas funciones integran los valores como literales escapados, no como parámetros de base de
|
||||||
//! datos. Para consultas con datos del usuario, el sistema de entidades es más robusto. Si aun así
|
//! datos. Para consultas con datos del usuario, el sistema de entidades es más robusto. Si aun así
|
||||||
//! se necesita SQL en crudo con parámetros reales, se puede construir un [`sea_orm::Statement`]
|
//! se necesita SQL en crudo con parámetros reales, se puede construir un [`api::Statement`]
|
||||||
//! directamente con [`sea_orm::Statement::from_sql_and_values`].
|
//! directamente con [`api::Statement::from_sql_and_values`].
|
||||||
//!
|
//!
|
||||||
//! # Tipos esenciales
|
//! ## Tipos esenciales
|
||||||
//!
|
//!
|
||||||
//! Destacan los siguientes elementos de uso más frecuente:
|
//! Destacan los siguientes elementos de uso más frecuente:
|
||||||
//!
|
//!
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
//! - **Errores**: [`DbErr`].
|
//! - **Errores**: [`DbErr`].
|
||||||
//! - **Resultados**: [`QueryResult`] (filas sin tipar), [`ExecResult`] (INSERT/UPDATE/DELETE).
|
//! - **Resultados**: [`QueryResult`] (filas sin tipar), [`ExecResult`] (INSERT/UPDATE/DELETE).
|
||||||
//!
|
//!
|
||||||
//! # Definir una entidad
|
//! ## Definir una entidad
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use pagetop_seaorm::db::*;
|
//! use pagetop_seaorm::db::*;
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
//! impl ActiveModelBehavior for ActiveModel {}
|
//! impl ActiveModelBehavior for ActiveModel {}
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Operaciones CRUD
|
//! ## Operaciones CRUD
|
||||||
//!
|
//!
|
||||||
//! ```rust,ignore
|
//! ```rust,ignore
|
||||||
//! use pagetop_seaorm::db::*;
|
//! use pagetop_seaorm::db::*;
|
||||||
|
|
@ -106,19 +106,19 @@
|
||||||
//!
|
//!
|
||||||
//! Para migraciones y definición de esquemas usa [`migration`](crate::migration).
|
//! Para migraciones y definición de esquemas usa [`migration`](crate::migration).
|
||||||
//!
|
//!
|
||||||
//! # Acceso completo a SeaORM
|
//! ## Acceso completo a SeaORM
|
||||||
//!
|
//!
|
||||||
//! Este módulo re-exporta el crate `sea_orm` íntegro. Úsalo cuando necesites un tipo o función que
|
//! El módulo [`api`] re-exporta el crate `sea_orm` íntegro bajo ese alias. Úsalo cuando necesites
|
||||||
//! no esté expuesto directamente en `db::*`:
|
//! un tipo o función que no esté expuesto directamente en `db::*`:
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use pagetop_seaorm::db::sea_orm;
|
//! use pagetop_seaorm::db::api;
|
||||||
//!
|
//!
|
||||||
//! // Tipos o utilidades no incluidos en db::*:
|
//! // Tipos o utilidades no incluidos en db::*:
|
||||||
//! let _: sea_orm::DatabaseBackend = sea_orm::DatabaseBackend::Sqlite;
|
//! let _: api::DatabaseBackend = api::DatabaseBackend::Sqlite;
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Construcción de consultas en tiempo de ejecución
|
//! ## Construcción de consultas en tiempo de ejecución
|
||||||
//!
|
//!
|
||||||
//! El módulo [`query`] re-exporta `sea_query` para construir las sentencias SQL que se pasan a
|
//! El módulo [`query`] re-exporta `sea_query` para construir las sentencias SQL que se pasan a
|
||||||
//! [`fetch_all`] y [`fetch_one`]. Es el compañero natural de esas funciones dentro del módulo `db`:
|
//! [`fetch_all`] y [`fetch_one`]. Es el compañero natural de esas funciones dentro del módulo `db`:
|
||||||
|
|
@ -139,25 +139,17 @@
|
||||||
|
|
||||||
pub use sea_orm::prelude::*;
|
pub use sea_orm::prelude::*;
|
||||||
|
|
||||||
// Re-exporta el crate `sea_orm` íntegro como puerta de acceso a su API completa.
|
|
||||||
//
|
|
||||||
// Útil para tipos o utilidades que no están expuestos directamente en `db::*`; la inmensa mayoría
|
|
||||||
// de operaciones no necesitan este módulo, `db::*` cubre los casos habituales.
|
|
||||||
//
|
|
||||||
// Por otro lado, este re-export es funcionalmente necesario para que los derive de `sea-orm-macros`
|
|
||||||
// (`DeriveEntityModel`, `DeriveRelation`...) compilen sin declarar `sea-orm` como dependencia
|
|
||||||
// directa en el `Cargo.toml` de la extensión o aplicación. Los derive generan rutas `sea_orm::...`
|
|
||||||
// (vía `quote!`, sin `Span::mixed_site()`), que resuelven contra cualquier item `sea_orm` visible
|
|
||||||
// en el módulo que invoca la macro. Basta con declarar `use pagetop_seaorm::db::*;` en cada entidad
|
|
||||||
// para compilar sin errores.
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use sea_orm;
|
|
||||||
|
|
||||||
pub use sea_orm::{
|
pub use sea_orm::{
|
||||||
ActiveValue, Condition, DatabaseTransaction, DbBackend, ExecResult, NotSet, Order, QueryOrder,
|
ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait,
|
||||||
QuerySelect, Set, TransactionError, TransactionTrait, Unchanged,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Re-exporta el crate `sea_orm` íntegro como puerta de acceso a su API completa.
|
||||||
|
///
|
||||||
|
/// Útil para tipos o utilidades que no están expuestos directamente en [`db::*`](self). La inmensa
|
||||||
|
/// mayoría de operaciones no necesitan este módulo; `db::*` cubre los casos habituales.
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use sea_orm as api;
|
||||||
|
|
||||||
/// Re-exporta `sea_query` para construir sentencias SQL en tiempo de ejecución.
|
/// Re-exporta `sea_query` para construir sentencias SQL en tiempo de ejecución.
|
||||||
///
|
///
|
||||||
/// Proporciona los constructores de consultas (`Query`, `Expr`, `Alias`, ...) que se pasan a
|
/// Proporciona los constructores de consultas (`Query`, `Expr`, `Alias`, ...) que se pasan a
|
||||||
|
|
@ -215,7 +207,7 @@ pub fn dbconn() -> &'static DatabaseConnection {
|
||||||
pub async fn execute(stmt: impl Into<String>) -> Result<ExecResult, DbErr> {
|
pub async fn execute(stmt: impl Into<String>) -> Result<ExecResult, DbErr> {
|
||||||
let conn = dbconn();
|
let conn = dbconn();
|
||||||
let backend = conn.get_database_backend();
|
let backend = conn.get_database_backend();
|
||||||
conn.execute(sea_orm::Statement::from_string(backend, stmt.into()))
|
conn.execute(api::Statement::from_string(backend, stmt.into()))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,12 +248,12 @@ pub async fn fetch_all<Q: query::QueryStatementWriter>(
|
||||||
) -> Result<Vec<QueryResult>, DbErr> {
|
) -> Result<Vec<QueryResult>, DbErr> {
|
||||||
let conn = dbconn();
|
let conn = dbconn();
|
||||||
let backend = conn.get_database_backend();
|
let backend = conn.get_database_backend();
|
||||||
conn.query_all(sea_orm::Statement::from_string(
|
conn.query_all(api::Statement::from_string(
|
||||||
backend,
|
backend,
|
||||||
match backend {
|
match backend {
|
||||||
sea_orm::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder),
|
api::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder),
|
||||||
sea_orm::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder),
|
api::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder),
|
||||||
sea_orm::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder),
|
api::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|
@ -306,12 +298,12 @@ pub async fn fetch_one<Q: query::QueryStatementWriter>(
|
||||||
) -> Result<Option<QueryResult>, DbErr> {
|
) -> Result<Option<QueryResult>, DbErr> {
|
||||||
let conn = dbconn();
|
let conn = dbconn();
|
||||||
let backend = conn.get_database_backend();
|
let backend = conn.get_database_backend();
|
||||||
conn.query_one(sea_orm::Statement::from_string(
|
conn.query_one(api::Statement::from_string(
|
||||||
backend,
|
backend,
|
||||||
match backend {
|
match backend {
|
||||||
sea_orm::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder),
|
api::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder),
|
||||||
sea_orm::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder),
|
api::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder),
|
||||||
sea_orm::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder),
|
api::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -128,9 +128,8 @@ pub use manager::*;
|
||||||
//pub use migrator::*;
|
//pub use migrator::*;
|
||||||
|
|
||||||
//pub use async_trait;
|
//pub use async_trait;
|
||||||
pub use sea_orm;
|
//pub use sea_orm;
|
||||||
pub use sea_orm::sea_query;
|
//pub use sea_orm::sea_query;
|
||||||
|
|
||||||
pub use sea_orm::DbErr;
|
pub use sea_orm::DbErr;
|
||||||
|
|
||||||
pub trait MigrationName {
|
pub trait MigrationName {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ mod figfont;
|
||||||
|
|
||||||
use crate::core::{extension, extension::ExtensionRef};
|
use crate::core::{extension, extension::ExtensionRef};
|
||||||
use crate::locale::Locale;
|
use crate::locale::Locale;
|
||||||
use crate::response::{render_error_pages, response_for_panic, route_not_found};
|
use crate::response::page::{render_error_pages, response_for_panic, route_not_found};
|
||||||
use crate::web::Router;
|
use crate::web::Router;
|
||||||
use crate::{PAGETOP_VERSION, global, trace};
|
use crate::{PAGETOP_VERSION, global, trace};
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ use std::sync::LazyLock;
|
||||||
/// preparando un entorno de pruebas, se usa [`test()`](Application::test).
|
/// preparando un entorno de pruebas, se usa [`test()`](Application::test).
|
||||||
///
|
///
|
||||||
/// Los **errores controlados** (403, 404, o un fallo que un handler devuelva explícitamente como
|
/// Los **errores controlados** (403, 404, o un fallo que un handler devuelva explícitamente como
|
||||||
/// [`ErrorPage`](crate::response::ErrorPage)) se renderizan usando el tema activo (ver
|
/// [`ErrorPage`](crate::response::page::ErrorPage)) se renderizan usando el tema activo (ver
|
||||||
/// [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
/// [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
||||||
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) y
|
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) y
|
||||||
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
||||||
|
|
@ -102,6 +102,11 @@ impl Application {
|
||||||
print!("\n{app_ff}");
|
print!("\n{app_ff}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Descripción de la aplicación.
|
||||||
|
if !global::SETTINGS.app.description.is_empty() {
|
||||||
|
println!("{}", global::SETTINGS.app.description.cyan());
|
||||||
|
}
|
||||||
|
|
||||||
// Versión de PageTop.
|
// Versión de PageTop.
|
||||||
println!(
|
println!(
|
||||||
"{} {}\n",
|
"{} {}\n",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Acciones para alterar el contenido de las páginas a renderizar.
|
//! Acciones para alterar el contenido de las páginas a renderizar.
|
||||||
|
|
||||||
use crate::response::Page;
|
use crate::response::page::Page;
|
||||||
|
|
||||||
/// Tipo de función para manipular una página durante su construcción o renderizado.
|
/// Tipo de función para manipular una página durante su construcción o renderizado.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use std::sync::Arc;
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Para renderizar contenido que dependa del contexto, se puede acceder a él dentro del closure:
|
/// Para renderizar contenido que dependa del contexto, se puede acceder a él dentro del *closure*:
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ pub enum IntroOpening {
|
||||||
/// .with_slogan(L10n::l("intro_custom_slogan"))
|
/// .with_slogan(L10n::l("intro_custom_slogan"))
|
||||||
/// .with_button(Some((
|
/// .with_button(Some((
|
||||||
/// L10n::l("intro_learn_more"),
|
/// L10n::l("intro_learn_more"),
|
||||||
/// "/learn-more".into()
|
/// |_| "/learn-more".into()
|
||||||
/// )));
|
/// )));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
|
@ -57,7 +57,7 @@ pub enum IntroOpening {
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let intro = Intro::default()
|
/// let intro = Intro::default()
|
||||||
/// .with_button(None::<(L10n, Route)>)
|
/// .with_button(None::<(L10n, FnPathByContext)>)
|
||||||
/// .with_opening(IntroOpening::Custom);
|
/// .with_opening(IntroOpening::Custom);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
|
@ -86,7 +86,7 @@ pub struct Intro {
|
||||||
/// Devuelve el eslogan de la entrada.
|
/// Devuelve el eslogan de la entrada.
|
||||||
slogan: L10n,
|
slogan: L10n,
|
||||||
/// Devuelve el botón de llamada a la acción, si existe.
|
/// Devuelve el botón de llamada a la acción, si existe.
|
||||||
button: Option<(L10n, Route)>,
|
button: Option<(L10n, FnPathByContext)>,
|
||||||
/// Devuelve el modo de apertura configurado.
|
/// Devuelve el modo de apertura configurado.
|
||||||
opening: IntroOpening,
|
opening: IntroOpening,
|
||||||
/// Devuelve la lista de componentes hijo de la intro.
|
/// Devuelve la lista de componentes hijo de la intro.
|
||||||
|
|
@ -100,7 +100,7 @@ impl Default for Intro {
|
||||||
Intro {
|
Intro {
|
||||||
title: L10n::l("intro_default_title"),
|
title: L10n::l("intro_default_title"),
|
||||||
slogan: L10n::l("intro_default_slogan").with_arg("app", &global::SETTINGS.app.name),
|
slogan: L10n::l("intro_default_slogan").with_arg("app", &global::SETTINGS.app.name),
|
||||||
button: Some((L10n::l("intro_default_button"), BUTTON_LINK.into())),
|
button: Some((L10n::l("intro_default_button"), |_| BUTTON_LINK.into())),
|
||||||
opening: IntroOpening::default(),
|
opening: IntroOpening::default(),
|
||||||
children: Children::default(),
|
children: Children::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +159,7 @@ impl Component for Intro {
|
||||||
div class="intro-button" {
|
div class="intro-button" {
|
||||||
a
|
a
|
||||||
class="intro-button-link"
|
class="intro-button-link"
|
||||||
href=(lnk.resolve(cx))
|
href=((lnk)(cx))
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
{
|
{
|
||||||
|
|
@ -249,21 +249,21 @@ impl Intro {
|
||||||
|
|
||||||
/// Configura el botón opcional de llamada a la acción.
|
/// Configura el botón opcional de llamada a la acción.
|
||||||
///
|
///
|
||||||
/// - Usa `Some((texto, ruta))` para mostrarlo, donde [`Route`] resuelve la ruta o URL final al
|
/// - Usa `Some((texto, closure_url))` para mostrarlo, donde [`FnPathByContext`] recibe el
|
||||||
/// pulsar el botón según el contexto de renderizado.
|
/// [`Context`] y devuelve la ruta o URL final al pulsar el botón.
|
||||||
/// - Usa `None` para ocultarlo.
|
/// - Usa `None` para ocultarlo.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// // Define un botón con texto y una ruta interna (preserva `lang` si corresponde).
|
/// // Define un botón con texto y una URL fija.
|
||||||
/// let intro = Intro::default().with_button(Some((L10n::n("Start"), "/start".into())));
|
/// let intro = Intro::default().with_button(Some((L10n::n("Start"), |_| "/start".into())));
|
||||||
/// // Descarta el botón de la intro.
|
/// // Descarta el botón de la intro.
|
||||||
/// let intro_no_button = Intro::default().with_button(None);
|
/// let intro_no_button = Intro::default().with_button(None);
|
||||||
/// ```
|
/// ```
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_button(mut self, button: Option<(L10n, Route)>) -> Self {
|
pub fn with_button(mut self, button: Option<(L10n, FnPathByContext)>) -> Self {
|
||||||
self.button = button;
|
self.button = button;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,21 +92,18 @@
|
||||||
//!
|
//!
|
||||||
//! # Usando tus opciones de configuración
|
//! # Usando tus opciones de configuración
|
||||||
//!
|
//!
|
||||||
//! Los ajustes de cada extensión y los globales de PageTop
|
|
||||||
//! ([`global::SETTINGS`](crate::global::SETTINGS)) conviven sin conflicto: cada cual vive en su
|
|
||||||
//! propia sección del TOML y pueden combinarse libremente cuando se necesiten.
|
|
||||||
//!
|
|
||||||
//! ```rust,ignore
|
//! ```rust,ignore
|
||||||
//! use pagetop::prelude::*;
|
//! use pagetop::prelude::*;
|
||||||
//! use crate::config;
|
//! use crate::config;
|
||||||
//!
|
//!
|
||||||
//! fn global_settings() {
|
//! fn global_settings() {
|
||||||
//! println!("Nombre de la app: {}", &global::SETTINGS.app.name);
|
//! println!("Nombre de la app: {}", &global::SETTINGS.app.name);
|
||||||
|
//! println!("Descripción: {}", &global::SETTINGS.app.description);
|
||||||
//! println!("Run mode: {}", &global::SETTINGS.app.run_mode);
|
//! println!("Run mode: {}", &global::SETTINGS.app.run_mode);
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! fn extension_settings() {
|
//! fn extension_settings() {
|
||||||
//! println!("{}", &config::SETTINGS.myapp.name);
|
//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
|
||||||
//! println!("{}", &config::SETTINGS.myapp.width);
|
//! println!("{}", &config::SETTINGS.myapp.width);
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ where
|
||||||
|
|
||||||
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
|
/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada.
|
||||||
///
|
///
|
||||||
/// Funciona igual que [`dispatch_actions`], pero el closure puede devolver
|
/// Funciona igual que [`dispatch_actions`], pero el *closure* puede devolver
|
||||||
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
|
/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o
|
||||||
/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente.
|
/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
//! API para construir nuevos componentes.
|
//! API para construir nuevos componentes.
|
||||||
//!
|
|
||||||
//! Para cualquier `href`, `action` o redirección que deba preservar el idioma negociado (anexando a
|
use crate::html::RoutePath;
|
||||||
//! la URL el parámetro de consulta `?lang=...`), PageTop no usa `String`/`&str` sueltos, sino que
|
|
||||||
//! usa [`Context::route()`] cuando ya se tiene el contexto de renderizado a mano, o [`Route`] para
|
|
||||||
//! campos de componente que se construyen una vez y se resuelven más tarde, en cada petición. La
|
|
||||||
//! documentación de [`Route`] explica el criterio completo para elegir entre ambos.
|
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::ComponentError;
|
pub use error::ComponentError;
|
||||||
|
|
@ -22,9 +18,6 @@ pub use message::{MessageLevel, StatusMessage};
|
||||||
mod context;
|
mod context;
|
||||||
pub use context::{AssetsOp, Context, ContextError, Contextual};
|
pub use context::{AssetsOp, Context, ContextError, Contextual};
|
||||||
|
|
||||||
mod route;
|
|
||||||
pub use route::Route;
|
|
||||||
|
|
||||||
/// Alias de función (*callback*) para **determinar si un componente se renderiza o no**.
|
/// Alias de función (*callback*) para **determinar si un componente se renderiza o no**.
|
||||||
///
|
///
|
||||||
/// Puede usarse para permitir que una instancia concreta de un tipo de componente dado decida
|
/// Puede usarse para permitir que una instancia concreta de un tipo de componente dado decida
|
||||||
|
|
@ -78,3 +71,41 @@ pub use route::Route;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub type FnIsRenderable = fn(cx: &Context) -> bool;
|
pub type FnIsRenderable = fn(cx: &Context) -> bool;
|
||||||
|
|
||||||
|
/// Alias de función (*callback*) para **resolver una ruta URL** según el contexto de renderizado.
|
||||||
|
///
|
||||||
|
/// Se usa para generar enlaces dinámicos en función del contexto (petición, idioma, parámetros,
|
||||||
|
/// etc.). Devuelve una [`RoutePath`], que representa un *path* base junto con una lista opcional de
|
||||||
|
/// parámetros de consulta.
|
||||||
|
///
|
||||||
|
/// El caso más común es construir rutas relativas dependientes del contexto, normalmente usando
|
||||||
|
/// [`Context::route`](crate::core::component::Context::route):
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # let relative_route: FnPathByContext =
|
||||||
|
/// |cx| cx.route("/path/to/page")
|
||||||
|
/// # ;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// También es posible usar rutas estáticas sin asignaciones adicionales:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # let external_route: FnPathByContext =
|
||||||
|
/// |_| "https://www.example.com".into()
|
||||||
|
/// # ;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// O componer rutas dinámicas en tiempo de ejecución:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # let dynamic_route: FnPathByContext =
|
||||||
|
/// |cx| RoutePath::new("/user").with_param("id", cx.param::<u64>("user_id").unwrap().to_string())
|
||||||
|
/// # ;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Los componentes que acepten un [`FnPathByContext`] invocarán esta función durante el renderizado
|
||||||
|
/// para obtener la URL final que se asignará al atributo HTML correspondiente.
|
||||||
|
pub type FnPathByContext = fn(cx: &Context) -> RoutePath;
|
||||||
|
|
|
||||||
|
|
@ -310,8 +310,7 @@ impl Children {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Añade un componente hijo al final de la lista. También lo usa `core::theme::regions` al
|
// Añade un componente hijo al final de la lista.
|
||||||
// fusionar regiones, fuera de este módulo.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn add(&mut self, child: Child) -> &mut Self {
|
pub(crate) fn add(&mut self, child: Child) -> &mut Self {
|
||||||
self.0.push(child);
|
self.0.push(child);
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ use crate::html::{Markup, Props, PropsOp, 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::web::HttpRequest;
|
||||||
use crate::{builder_fn, util};
|
use crate::{CowStr, builder_fn, util};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
|
/// Operaciones para modificar recursos asociados al [`Context`] de un documento.
|
||||||
pub enum AssetsOp {
|
pub enum AssetsOp {
|
||||||
|
|
@ -40,15 +40,14 @@ pub enum AssetsOp {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||||
#[derive(Debug, Error)]
|
///
|
||||||
|
/// - [`ContextError::ParamNotFound`]: la clave no existe.
|
||||||
|
/// - [`ContextError::ParamTypeMismatch`]: la clave existe, pero el valor guardado no coincide con
|
||||||
|
/// el tipo solicitado. Incluye nombre de la clave (`key`), tipo esperado (`expected`) y tipo
|
||||||
|
/// realmente guardado (`saved`) para facilitar el diagnóstico.
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ContextError {
|
pub enum ContextError {
|
||||||
/// La clave no existe.
|
|
||||||
#[error("parameter not found")]
|
|
||||||
ParamNotFound,
|
ParamNotFound,
|
||||||
/// La clave existe, pero el valor guardado no coincide con el tipo solicitado. Incluye
|
|
||||||
/// nombre de la clave (`key`), tipo esperado (`expected`) y tipo realmente guardado (`saved`)
|
|
||||||
/// para facilitar el diagnóstico.
|
|
||||||
#[error("type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\"")]
|
|
||||||
ParamTypeMismatch {
|
ParamTypeMismatch {
|
||||||
key: &'static str,
|
key: &'static str,
|
||||||
expected: &'static str,
|
expected: &'static str,
|
||||||
|
|
@ -56,6 +55,26 @@ pub enum ContextError {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ContextError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ContextError::ParamNotFound => {
|
||||||
|
write!(f, "parameter not found")
|
||||||
|
}
|
||||||
|
ContextError::ParamTypeMismatch {
|
||||||
|
key,
|
||||||
|
expected,
|
||||||
|
saved,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ContextError {}
|
||||||
|
|
||||||
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
|
/// Interfaz para gestionar el **contexto de renderizado** de un documento HTML.
|
||||||
///
|
///
|
||||||
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
||||||
|
|
@ -67,7 +86,7 @@ pub enum ContextError {
|
||||||
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
||||||
///
|
///
|
||||||
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
|
/// Lo implementan, típicamente, estructuras que manejan el contexto de renderizado, como
|
||||||
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::Page).
|
/// [`Context`](crate::core::component::Context) o [`Page`](crate::response::page::Page).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -236,51 +255,12 @@ pub trait Contextual: LangId {
|
||||||
fn remove_param(&mut self, key: &'static str) -> bool;
|
fn remove_param(&mut self, key: &'static str) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cómo obtener la plantilla activa del contexto: la del tema (por defecto o de administración), o
|
|
||||||
// una fijada explícitamente. Se resuelve contra el tema activo al leer `Context::template()`, no al
|
|
||||||
// asignarla, para que un cambio de tema posterior con `with_theme()` se refleje automáticamente.
|
|
||||||
enum TemplateSource {
|
|
||||||
// Plantilla por defecto.
|
|
||||||
Default,
|
|
||||||
// Plantilla de administración del tema activo.
|
|
||||||
Admin,
|
|
||||||
// Plantilla fijada explícitamente con `with_template()`.
|
|
||||||
Explicit(TemplateRef),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TemplateSource {
|
|
||||||
fn resolve(&self, theme: ThemeRef) -> TemplateRef {
|
|
||||||
match self {
|
|
||||||
Self::Default => theme.default_template(),
|
|
||||||
Self::Admin => theme.admin_template(),
|
|
||||||
Self::Explicit(template) => *template,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementa un **contexto de renderizado** para un documento HTML.
|
/// Implementa un **contexto de renderizado** para un documento HTML.
|
||||||
///
|
///
|
||||||
/// Se crea una sola vez por petición usando [`Context::new()`] (típicamente a través de
|
/// Extiende [`Contextual`] con métodos para **instanciar** y configurar un nuevo contexto,
|
||||||
/// [`Page::new()`](crate::response::Page::new) o [`Page::admin()`](crate::response::Page::admin)),
|
/// **renderizar los recursos** del documento (incluyendo el [`Favicon`], las hojas de estilo
|
||||||
/// y es la única vía por la que un componente, una acción o el tema activo conocen: la petición
|
/// [`StyleSheet`] y los scripts [`JavaScript`]), o extender el uso de **parámetros dinámicos
|
||||||
/// HTTP de origen, el idioma negociado, el usuario autenticado
|
/// tipados** con nuevos métodos.
|
||||||
/// ([`current_user()`](Contextual::current_user)), el tema y la plantilla en uso, y los recursos
|
|
||||||
/// (favicon, hojas de estilo, scripts) acumulados hasta ese momento. Otros datos que los
|
|
||||||
/// componentes necesiten durante el renderizado pueden ser parámetros dinámicos tipados con
|
|
||||||
/// [`with_param()`](Contextual::with_param)/[`param()`](Contextual::param).
|
|
||||||
///
|
|
||||||
/// La implementación extiende [`Contextual`], que aporta los métodos *builder* (`with_*`) y los
|
|
||||||
/// *getters* comunes a cualquier estructura que gestione un contexto de renderizado (también los
|
|
||||||
/// implementa [`Page`](crate::response::Page)). Además, `Context` añade:
|
|
||||||
///
|
|
||||||
/// - [`route()`](Self::route) para construir URLs que preserven `?lang=...` cuando corresponda.
|
|
||||||
/// - [`build_id()`](Self::build_id)/[`required_id()`](Self::required_id) para generar
|
|
||||||
/// identificadores HTML únicos por tipo de componente.
|
|
||||||
/// - [`push_message()`](Self::push_message)/[`messages()`](Self::messages) para acumular
|
|
||||||
/// [`StatusMessage`] que mostrar en algún momento del renderizado.
|
|
||||||
/// - [`render_assets()`](Self::render_assets)/[`render_region_named()`](Self::render_region_named),
|
|
||||||
/// usados internamente por [`Page`](crate::response::Page) para producir el HTML final del
|
|
||||||
/// documento.
|
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
|
|
@ -326,6 +306,29 @@ impl TemplateSource {
|
||||||
/// let _unique_id = cx.build_id::<Menu>(1); // => "menu-1" si es el primero
|
/// let _unique_id = cx.build_id::<Menu>(1); // => "menu-1" si es el primero
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
|
// Cómo obtener la plantilla activa del contexto: la del tema (por defecto o de administración), o
|
||||||
|
// una fijada explícitamente. Se resuelve contra el tema activo al leer `Context::template()`, no al
|
||||||
|
// asignarla, para que un cambio de tema posterior con `with_theme()` se refleje automáticamente.
|
||||||
|
enum TemplateSource {
|
||||||
|
// Plantilla por defecto.
|
||||||
|
Default,
|
||||||
|
// Plantilla de administración del tema activo.
|
||||||
|
Admin,
|
||||||
|
// Plantilla fijada explícitamente con `with_template()`.
|
||||||
|
Explicit(TemplateRef),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateSource {
|
||||||
|
fn resolve(&self, theme: ThemeRef) -> TemplateRef {
|
||||||
|
match self {
|
||||||
|
Self::Default => theme.default_template(),
|
||||||
|
Self::Admin => theme.admin_template(),
|
||||||
|
Self::Explicit(template) => *template,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
request : Option<HttpRequest>, // Petición HTTP de origen.
|
request : Option<HttpRequest>, // Petición HTTP de origen.
|
||||||
|
|
@ -437,24 +440,17 @@ impl Context {
|
||||||
|
|
||||||
/// Construye una ruta aplicada al contexto actual.
|
/// Construye una ruta aplicada al contexto actual.
|
||||||
///
|
///
|
||||||
/// Acepta cualquier tipo convertible a [`RoutePath`] (un literal, un `String`, un `&str` de
|
/// La ruta resultante se envuelve en un [`RoutePath`], que permite añadir parámetros de
|
||||||
/// cualquier vida, o un [`RoutePath`] ya construido con sus propios parámetros). Si la política
|
/// consulta de forma tipada. Si la política de negociación de idioma actual
|
||||||
/// de negociación del idioma ([`LangNegotiation`](crate::global::LangNegotiation)) indica que
|
/// [`LangNegotiation`](crate::global::LangNegotiation) indica que debe propagarse el idioma
|
||||||
/// debe propagarse el idioma para esta petición, se añade o actualiza automáticamente el
|
/// para esta petición, se añade o actualiza el parámetro de *query* `lang=...` con el
|
||||||
/// parámetro de *query* `lang=...` con el identificador de idioma definido en el contexto.
|
/// identificador de idioma efectivo del contexto.
|
||||||
///
|
///
|
||||||
/// Esto garantiza que los enlaces generados desde el contexto preservan la preferencia de
|
/// Esto garantiza que los enlaces generados desde el contexto preservan la preferencia de
|
||||||
/// idioma del usuario durante la navegación. Si `path` **parece** una URL externa (ver
|
/// idioma del usuario cuando procede.
|
||||||
/// [`util::url_looks_external()`](crate::util::url_looks_external)), nunca se le añade `lang`.
|
pub fn route(&self, path: impl Into<CowStr>) -> RoutePath {
|
||||||
///
|
let mut route = RoutePath::new(path);
|
||||||
/// Este método asume que ya tienes `cx` a mano en el momento de construir la ruta (dentro de
|
if self.locale.needs_lang_query() {
|
||||||
/// `prepare()`, un *handler* HTTP, etc.). Si lo que estás definiendo es un campo de componente
|
|
||||||
/// que se construye una sola vez y se reutiliza en peticiones futuras (un menú, un botón,
|
|
||||||
/// etc.), usa [`Route`](crate::core::component::Route) en su lugar (su documentación explica el
|
|
||||||
/// criterio completo para elegir entre ambos).
|
|
||||||
pub fn route(&self, path: impl Into<RoutePath>) -> RoutePath {
|
|
||||||
let mut route = path.into();
|
|
||||||
if !route.is_external() && self.locale.needs_lang_query() {
|
|
||||||
route.alter_param("lang", self.locale.langid().to_string());
|
route.alter_param("lang", self.locale.langid().to_string());
|
||||||
}
|
}
|
||||||
route
|
route
|
||||||
|
|
|
||||||
|
|
@ -110,27 +110,15 @@ pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync {
|
||||||
|
|
||||||
/// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
|
/// Genera el marcado HTML del componente cuando ningún tema lo sobrescribe.
|
||||||
///
|
///
|
||||||
/// Este es el cuarto paso del [ciclo de renderizado](ComponentRender) tras llamar al
|
/// Cuarto paso del [ciclo de renderizado](ComponentRender): se invoca tras
|
||||||
/// [`setup()`](Self::setup) del componente y despachar la acción
|
/// [`setup()`](Self::setup) y la acción
|
||||||
/// [`BeforeRender`](crate::base::action::component::BeforeRender) que atiende los cambios de
|
/// [`BeforeRender`](crate::base::action::component::BeforeRender), pero solamente si ningún
|
||||||
/// otras extensiones antes de renderizar. Se invoca sólo si ningún tema en la cadena devuelve
|
/// tema en la cadena devuelve `Some` en
|
||||||
/// `Some` en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) para
|
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component).
|
||||||
/// este componente.
|
|
||||||
///
|
|
||||||
/// Es `async`, a diferencia de [`setup()`](Self::setup), para permitir a los componentes
|
|
||||||
/// realizar aquí sus llamadas asíncronas, como consultas a base de datos, peticiones a
|
|
||||||
/// servicios externos, o cualquier operación de E/S, que necesiten para preparar su contenido.
|
|
||||||
///
|
///
|
||||||
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
|
/// Se recomienda obtener los datos del componente a través de sus propios métodos para que los
|
||||||
/// temas puedan implementar `handle_component()` sin depender de los detalles internos.
|
/// temas puedan implementar `handle_component()` sin depender de los detalles internos.
|
||||||
///
|
///
|
||||||
/// Los campos que representen contenido no deben almacenar [`Markup`] ya generado, sino que
|
|
||||||
/// guardarán el dato en bruto, por ejemplo [`L10n`](crate::locale::L10n) para textos
|
|
||||||
/// traducibles, o un componente anidado como [`Html`](crate::base::component::Html), o
|
|
||||||
/// cualquier otro `Component`, normalmente a través de [`Embed`](crate::core::component::Embed)
|
|
||||||
/// o [`Child`](crate::core::component::Child); y será este método quien lo convierta en HTML en
|
|
||||||
/// el momento del renderizado, no quien construye la instancia.
|
|
||||||
///
|
|
||||||
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
|
/// Por defecto, devuelve un [`Markup`] vacío (`Ok(html! {})`). En caso de error, devuelve un
|
||||||
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
|
/// [`ComponentError`] que puede incluir un marcado alternativo (*fallback*).
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use crate::html::{Markup, html};
|
use crate::html::{Markup, html};
|
||||||
use crate::{AutoDefault, Getters};
|
use crate::{AutoDefault, Getters};
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
/// Error producido durante el renderizado de un componente.
|
/// Error producido durante el renderizado de un componente.
|
||||||
///
|
///
|
||||||
/// Se usa en [`Component::prepare()`](super::Component::prepare) para devolver
|
/// Se usa en [`Component::prepare()`](super::Component::prepare) para devolver
|
||||||
|
|
@ -24,8 +22,7 @@ use thiserror::Error;
|
||||||
/// }
|
/// }
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Debug, Error, Getters)]
|
#[derive(AutoDefault, Debug, Getters)]
|
||||||
#[error("{message}")]
|
|
||||||
pub struct ComponentError {
|
pub struct ComponentError {
|
||||||
/// Mensaje descriptivo del error.
|
/// Mensaje descriptivo del error.
|
||||||
message: String,
|
message: String,
|
||||||
|
|
@ -54,9 +51,18 @@ impl ComponentError {
|
||||||
|
|
||||||
// **< ComponentError GETTERS >*****************************************************************
|
// **< ComponentError GETTERS >*****************************************************************
|
||||||
|
|
||||||
// Consume el error y devuelve su marcado alternativo. Se invoca internamente desde
|
/// Consume el error y devuelve su marcado alternativo.
|
||||||
// `ComponentRender::render()`, en el mismo módulo `core::component`.
|
///
|
||||||
pub(super) fn into_fallback(self) -> Markup {
|
/// Se invoca internamente en [`ComponentRender`](crate::core::component::ComponentRender).
|
||||||
|
pub(crate) fn into_fallback(self) -> Markup {
|
||||||
self.fallback
|
self.fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ComponentError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ComponentError {}
|
||||||
|
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
use crate::core::component::Context;
|
|
||||||
use crate::html::RoutePath;
|
|
||||||
use crate::util;
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// Encapsula una función que **resuelve una ruta URL** según el contexto de renderizado.
|
|
||||||
///
|
|
||||||
/// `Route` envuelve un closure (`Arc<dyn Fn>`) que le permite capturar valores de su entorno,
|
|
||||||
/// normalmente un identificador u otro dato dinámico, para construir la ruta. También puede
|
|
||||||
/// clonarse a bajo coste (sólo el `Arc` interno) y compartirse entre hilos.
|
|
||||||
///
|
|
||||||
/// Los componentes que acepten una `Route` la invocan con [`resolve()`](Self::resolve) durante el
|
|
||||||
/// renderizado para obtener la [`RoutePath`] final, que se asigna al atributo HTML correspondiente
|
|
||||||
/// (normalmente `href` o `action`).
|
|
||||||
///
|
|
||||||
/// # ¿Cuál usar, `RoutePath`, `Route` o `Context::route()`?
|
|
||||||
///
|
|
||||||
/// [`RoutePath`] es el valor común. Incluye un *path* más una lista de parámetros de consulta, sin
|
|
||||||
/// ninguna noción de [`Context`] ni de idioma. Es lo que devuelven [`Context::route()`] y
|
|
||||||
/// [`resolve()`](Self::resolve), y lo que reciben directamente funciones que ya se llaman con `cx`
|
|
||||||
/// a mano y consumen el resultado de inmediato, como
|
|
||||||
/// [`Waypoint::append_to()`](crate::response::Waypoint::append_to) /
|
|
||||||
/// [`Waypoint::or()`](crate::response::Waypoint::or), [`Redirect::*`](crate::response::Redirect) o
|
|
||||||
/// [`SortLink`](crate::base::component::table::SortLink). Al no depender de `Context`, también
|
|
||||||
/// sirve fuera del ciclo de renderizado, por ejemplo, para construir la URL de una API externa.
|
|
||||||
///
|
|
||||||
/// Entre `Route` y `Context::route()`, cuál usar depende de una sola pregunta: ¿el valor es una
|
|
||||||
/// propiedad de un componente, que se resolverá más tarde en su propio
|
|
||||||
/// [`prepare()`](crate::core::component::Component::prepare), o se va a resolver desde una función
|
|
||||||
/// que ya tiene `cx: &Context`?
|
|
||||||
///
|
|
||||||
/// - Los componentes suelen usar `Route` en sus propiedades. Su contenido llega en bruto y se
|
|
||||||
/// resuelve más tarde, normalmente en la ejecución de su `prepare()`, cuando ya existe un
|
|
||||||
/// `Context` concreto para esa petición.
|
|
||||||
///
|
|
||||||
/// Un literal (`"/path"`) o un `String` se enrutan automáticamente mediante conversión implícita
|
|
||||||
/// a `Route`, así que no hace falta invocarlo a mano salvo que quieras añadir parámetros
|
|
||||||
/// adicionales.
|
|
||||||
///
|
|
||||||
/// - Si tienes `cx: &Context` (o `&mut Context`) a tu disposición, ya sea en un handler HTTP,
|
|
||||||
/// dentro de un [`Html::with(|cx| ...)`](crate::base::component::Html::with) o cualquier otra
|
|
||||||
/// función que reciba `cx`, puedes usar `Context::route()` para obtener el `RoutePath` de
|
|
||||||
/// inmediato, seguir componiéndolo con `.with_param()`/`.with_flag()`, o insertarlo directamente
|
|
||||||
/// en el marcado.
|
|
||||||
///
|
|
||||||
/// A diferencia de `Route`, un literal pasado directamente a las funciones anteriores **no**
|
|
||||||
/// reconoce `Context` ni idioma, por lo que habría que llamar a `cx.route(...)` a mano antes de
|
|
||||||
/// invocarlas (ver el ejemplo con `Waypoint::append_to()` más abajo).
|
|
||||||
///
|
|
||||||
/// # Detección automática de URLs externas
|
|
||||||
///
|
|
||||||
/// La conversión implícita desde `&str`/`String` reconoce si el texto **parece** una URL externa
|
|
||||||
/// (ver [`util::url_looks_external()`](crate::util::url_looks_external)) y la trata como tal. En
|
|
||||||
/// ese caso se comporta como [`Route::external()`], sin pasar por `Context::route()`. Una URL
|
|
||||||
/// externa nunca debe llevar `lang`, porque no pertenece al espacio de rutas de la aplicación.
|
|
||||||
///
|
|
||||||
/// Esta protección no depende sólo de `Route`: [`Context::route()`] también comprueba si el
|
|
||||||
/// resultado parece externo antes de añadir `lang`, así que llamarlo directamente (por ejemplo, con
|
|
||||||
/// una URL dinámica que no ha pasado por `Route`) tampoco añade el parámetro.
|
|
||||||
///
|
|
||||||
/// Esta detección es una comodidad, no una garantía: se basa en el prefijo del texto. Usa
|
|
||||||
/// [`Route::external()`] explícitamente cuando quieras dejar clara la intención sin ambigüedad, o
|
|
||||||
/// para esquemas que la heurística aplicada no reconozca.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// El caso más común es una ruta relativa dependiente del contexto, normalmente usando
|
|
||||||
/// [`Context::route()`] para que el enlace preserve el parámetro `lang` cuando corresponda:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let route = Route::with(|cx| cx.route("/path/to/page"));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Un literal o un `String` se convierten directamente, y también pasan por [`Context::route()`]:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let route: Route = "/path/to/page".into();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Una ruta dinámica puede capturar valores del entorno, algo que un simple puntero a función no
|
|
||||||
/// permite:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let user_id = 42;
|
|
||||||
/// let route = Route::with(move |cx| cx.route(format!("/users/{user_id}")));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Si ya tienes un [`RoutePath`] resuelto -- por ejemplo, combinando
|
|
||||||
/// [`Context::route()`] con [`Waypoint::append_to()`](crate::response::Waypoint::append_to) --
|
|
||||||
/// se convierte directamente, sin volver a procesarlo:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// # let cx = Context::new(None);
|
|
||||||
/// # let waypoint = Waypoint::default();
|
|
||||||
/// let route: Route = waypoint.append_to(cx.route("/items")).into();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Una URL externa fija, que no debe llevar `lang`, se detecta automáticamente por su prefijo al
|
|
||||||
/// convertir un literal, y se comporta como [`Route::external()`]:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let route: Route = "https://www.example.com".into();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// [`Route::external()`] sigue siendo útil para dejar la intención explícita, o para esquemas que
|
|
||||||
/// la heurística no reconozca (por ejemplo una URL construida dinámicamente que no empieza
|
|
||||||
/// literalmente por uno de los prefijos detectados):
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// let route = Route::external("ftp://files.example.com");
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Route(Arc<dyn Fn(&Context) -> RoutePath + Send + Sync>);
|
|
||||||
|
|
||||||
impl Route {
|
|
||||||
/// Crea una `Route` a partir de un closure que resuelve la ruta según el contexto.
|
|
||||||
pub fn with<F>(f: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(&Context) -> RoutePath + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
Route(Arc::new(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una `Route` para una URL externa fija.
|
|
||||||
///
|
|
||||||
/// Una URL externa nunca debe llevar el parámetro `lang` ya que no pertenece al espacio de
|
|
||||||
/// rutas de la aplicación. Un literal o un `String` con pinta de URL externa (ver "Detección
|
|
||||||
/// automática de URLs externas" más arriba) ya se comportan así al convertirse a `Route`; usa
|
|
||||||
/// `external()` explícitamente para dejar la intención clara sin ambigüedad, o cuando el
|
|
||||||
/// heurístico no reconozca el esquema.
|
|
||||||
pub fn external(url: impl Into<RoutePath>) -> Self {
|
|
||||||
url.into().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Invoca el closure interno para obtener la [`RoutePath`] final.
|
|
||||||
pub fn resolve(&self, cx: &Context) -> RoutePath {
|
|
||||||
(self.0)(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Route {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_tuple("Route")
|
|
||||||
.field(&"Fn(&Context) -> RoutePath")
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Route {
|
|
||||||
fn default() -> Self {
|
|
||||||
Route::with(|_| RoutePath::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Envuelve un [`RoutePath`] que ya tienes construido, sin volver a procesarlo.
|
|
||||||
///
|
|
||||||
/// Es el caso de un `RoutePath` obtenido con [`Context::route()`], solo o combinado con
|
|
||||||
/// [`Waypoint::append_to()`](crate::response::Waypoint::append_to) o
|
|
||||||
/// [`Waypoint::or()`](crate::response::Waypoint::or): el idioma (`lang`) ya se aplicó ahí, y esta
|
|
||||||
/// conversión evita volver a aplicarlo.
|
|
||||||
///
|
|
||||||
/// Ojo: esta conversión asume que el `RoutePath` ya pasó por `Context::route()`. Si construyes uno
|
|
||||||
/// a mano (por ejemplo con `RoutePath::new(...)` directamente, sin pasar por `cx.route()`), esta
|
|
||||||
/// conversión no añadirá `lang` por ti -- simplemente envuelve el valor tal cual, igual que
|
|
||||||
/// [`Route::external()`].
|
|
||||||
impl From<RoutePath> for Route {
|
|
||||||
fn from(path: RoutePath) -> Self {
|
|
||||||
Route::with(move |_| path.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&'static str> for Route {
|
|
||||||
fn from(path: &'static str) -> Self {
|
|
||||||
if util::url_looks_external(path) {
|
|
||||||
Route::external(path)
|
|
||||||
} else {
|
|
||||||
Route::with(move |cx| cx.route(path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Route {
|
|
||||||
fn from(path: String) -> Self {
|
|
||||||
if util::url_looks_external(&path) {
|
|
||||||
Route::external(path)
|
|
||||||
} else {
|
|
||||||
Route::with(move |cx| cx.route(path.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
//! del documento a partir de regiones ([`Region`]). Cada región es un contenedor lógico
|
//! del documento a partir de regiones ([`Region`]). Cada región es un contenedor lógico
|
||||||
//! identificado por un nombre para agrupar y renderizar componentes.
|
//! identificado por un nombre para agrupar y renderizar componentes.
|
||||||
//!
|
//!
|
||||||
//! Una página ([`Page`](crate::response::Page)) es un documento HTML completo. Implementa
|
//! Una página ([`Page`](crate::response::page::Page)) es un documento HTML completo. Implementa
|
||||||
//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio [`Context`], donde
|
//! [`Contextual`](crate::core::component::Contextual) para gestionar su propio [`Context`], donde
|
||||||
//! mantiene el tema activo, la plantilla seleccionada y los componentes asociados a cada región a
|
//! mantiene el tema activo, la plantilla seleccionada y los componentes asociados a cada región a
|
||||||
//! renderizar.
|
//! renderizar.
|
||||||
|
|
@ -104,7 +104,7 @@ use crate::{AutoDefault, util};
|
||||||
/// manera distinta.
|
/// manera distinta.
|
||||||
///
|
///
|
||||||
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
|
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
|
||||||
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::Page)).
|
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Region: Send + Sync {
|
pub trait Region: Send + Sync {
|
||||||
/// Devuelve el nombre de la región.
|
/// Devuelve el nombre de la región.
|
||||||
|
|
@ -209,7 +209,7 @@ impl Region for DefaultRegions {
|
||||||
/// Interfaz común para definir plantillas de contenido.
|
/// Interfaz común para definir plantillas de contenido.
|
||||||
///
|
///
|
||||||
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
|
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
|
||||||
/// de una página ([`Page`](crate::response::Page)). El tema utiliza esta información para
|
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
|
||||||
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
|
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Template: Send + Sync {
|
pub trait Template: Send + Sync {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::core::theme::{DefaultRegions, DefaultTemplates, TemplateRef};
|
||||||
use crate::global;
|
use crate::global;
|
||||||
use crate::html::{Markup, html};
|
use crate::html::{Markup, html};
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::response::Page;
|
use crate::response::page::Page;
|
||||||
use crate::web::http::StatusCode;
|
use crate::web::http::StatusCode;
|
||||||
|
|
||||||
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
||||||
|
|
@ -63,7 +63,7 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// propone como predeterminada.
|
/// propone como predeterminada.
|
||||||
///
|
///
|
||||||
/// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página
|
/// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página
|
||||||
/// ([`Page`](crate::response::Page)) por si no se elige ninguna otra plantilla con
|
/// ([`Page`](crate::response::page::Page)) por si no se elige ninguna otra plantilla con
|
||||||
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
|
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
|
||||||
///
|
///
|
||||||
/// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Standard`] con una
|
/// La implementación por defecto devuelve la plantilla [`DefaultTemplates::Standard`] con una
|
||||||
|
|
@ -299,12 +299,12 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// Permite al tema preparar y componer una página de **error fatal controlado**.
|
/// Permite al tema preparar y componer una página de **error fatal controlado**.
|
||||||
///
|
///
|
||||||
/// Esta función decide explícitamente devolver
|
/// Esta función decide explícitamente devolver
|
||||||
/// [`ErrorPage::BadRequest`](crate::response::ErrorPage::BadRequest),
|
/// [`ErrorPage::BadRequest`](crate::response::page::ErrorPage::BadRequest),
|
||||||
/// [`ErrorPage::InternalError`](crate::response::ErrorPage::InternalError),
|
/// [`ErrorPage::InternalError`](crate::response::page::ErrorPage::InternalError),
|
||||||
/// [`ErrorPage::ServiceUnavailable`](crate::response::ErrorPage::ServiceUnavailable) o
|
/// [`ErrorPage::ServiceUnavailable`](crate::response::page::ErrorPage::ServiceUnavailable) o
|
||||||
/// [`ErrorPage::GatewayTimeout`](crate::response::ErrorPage::GatewayTimeout) porque algo ha
|
/// [`ErrorPage::GatewayTimeout`](crate::response::page::ErrorPage::GatewayTimeout) porque algo
|
||||||
/// fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de componentes
|
/// ha fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de
|
||||||
/// funcionan con normalidad.
|
/// componentes funcionan con normalidad.
|
||||||
///
|
///
|
||||||
/// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya
|
/// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya
|
||||||
/// activa en la página (normalmente [`DefaultTemplates::Standard`]) y muestra un componente
|
/// activa en la página (normalmente [`DefaultTemplates::Standard`]) y muestra un componente
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,3 @@
|
||||||
//! (basado en [chrono](https://docs.rs/chrono)).
|
//! (basado en [chrono](https://docs.rs/chrono)).
|
||||||
|
|
||||||
pub use chrono::prelude::*;
|
pub use chrono::prelude::*;
|
||||||
|
|
||||||
// `Duration` no forma parte de `chrono::prelude`, pero es de uso tan habitual junto al resto de
|
|
||||||
// tipos de este módulo (sumar/restar intervalos a un `NaiveDateTime`, calcular expiraciones...) que
|
|
||||||
// se reexporta igualmente.
|
|
||||||
pub use chrono::Duration;
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ pub use log_format::LogFormat;
|
||||||
include_config!(SETTINGS: Settings => [
|
include_config!(SETTINGS: Settings => [
|
||||||
// [app]
|
// [app]
|
||||||
"app.name" => "PageTop App",
|
"app.name" => "PageTop App",
|
||||||
|
"app.description" => "Developed with the amazing PageTop framework.",
|
||||||
"app.theme" => "Basic",
|
"app.theme" => "Basic",
|
||||||
"app.lang_negotiation" => "Full",
|
"app.lang_negotiation" => "Full",
|
||||||
"app.startup_banner" => "Slant",
|
"app.startup_banner" => "Slant",
|
||||||
|
|
@ -57,6 +58,8 @@ pub struct Settings {
|
||||||
pub struct App {
|
pub struct App {
|
||||||
/// Nombre de la aplicación.
|
/// Nombre de la aplicación.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Breve descripción de la aplicación.
|
||||||
|
pub description: String,
|
||||||
/// Tema predeterminado.
|
/// Tema predeterminado.
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
/// Idioma predeterminado de la aplicación (p. ej., *"es-ES"* o *"en-US"*).
|
/// Idioma predeterminado de la aplicación (p. ej., *"es-ES"* o *"en-US"*).
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
pub(crate) mod maud;
|
pub(crate) mod maud;
|
||||||
pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, Render, display, html, html_private};
|
pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, Render, display, html, html_private};
|
||||||
|
|
||||||
mod route_path;
|
mod route;
|
||||||
pub use route_path::RoutePath;
|
pub use route::RoutePath;
|
||||||
|
|
||||||
// **< HTML DOCUMENT ASSETS >***********************************************************************
|
// **< HTML DOCUMENT ASSETS >***********************************************************************
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ impl JavaScript {
|
||||||
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
|
/// Equivale a `<script>...</script>`. El parámetro `name` se usa como identificador interno del
|
||||||
/// script.
|
/// script.
|
||||||
///
|
///
|
||||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
|
@ -139,7 +139,7 @@ impl JavaScript {
|
||||||
///
|
///
|
||||||
/// En condiciones normales, los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
|
/// En condiciones normales, los scripts con `defer` se ejecutan antes de `DOMContentLoaded`.
|
||||||
///
|
///
|
||||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
pub fn on_load<F>(name: impl Into<CowStr>, f: F) -> Self
|
pub fn on_load<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
|
@ -153,10 +153,11 @@ impl JavaScript {
|
||||||
/// Crea un **script embebido** con un **handler asíncrono**.
|
/// Crea un **script embebido** con un **handler asíncrono**.
|
||||||
///
|
///
|
||||||
/// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
|
/// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
|
||||||
/// emplea una función `async` para que el cuerpo devuelto por un closure pueda usar `await`.
|
/// emplea una función `async` para que el cuerpo devuelto por la función *closure* pueda usar
|
||||||
/// Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas iniciales.
|
/// `await`. Ideal para hidratar la interfaz, cargar módulos dinámicos o realizar lecturas
|
||||||
|
/// iniciales.
|
||||||
///
|
///
|
||||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
pub fn on_load_async<F>(name: impl Into<CowStr>, f: F) -> Self
|
pub fn on_load_async<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use crate::{AutoDefault, CowStr, Weight, util};
|
||||||
// Informa al navegador de la naturaleza del recurso para que pueda asignarle la prioridad correcta
|
// Informa al navegador de la naturaleza del recurso para que pueda asignarle la prioridad correcta
|
||||||
// y aplicar la política de caché adecuada.
|
// y aplicar la política de caché adecuada.
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
enum AsType {
|
pub(crate) enum AsType {
|
||||||
// Fuente web (`@font-face`). Implica `crossorigin` automático.
|
// Fuente web (`@font-face`). Implica `crossorigin` automático.
|
||||||
#[default]
|
#[default]
|
||||||
Font,
|
Font,
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ impl StyleSheet {
|
||||||
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
|
/// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
|
||||||
/// recurso.
|
/// recurso.
|
||||||
///
|
///
|
||||||
/// Un closure recibirá el [`Context`] por si se necesita durante el renderizado.
|
/// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
|
||||||
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
F: Fn(&mut Context) -> String + Send + Sync + 'static,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ use crate::core::TypeInfo;
|
||||||
use crate::html::maud::{Escaper, Render};
|
use crate::html::maud::{Escaper, Render};
|
||||||
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Write};
|
use std::fmt::{self, Write};
|
||||||
|
|
@ -39,15 +37,16 @@ impl fmt::Debug for PropsExtra {
|
||||||
// **< PropsError >*********************************************************************************
|
// **< PropsError >*********************************************************************************
|
||||||
|
|
||||||
/// Errores de acceso a valores extra de [`Props`].
|
/// Errores de acceso a valores extra de [`Props`].
|
||||||
#[derive(Debug, PartialEq, Eq, Error)]
|
///
|
||||||
|
/// - [`PropsError::ExtraNotFound`]: la clave no existe. Incluye la clave (`key`).
|
||||||
|
/// - [`PropsError::ExtraTypeMismatch`]: la clave existe pero el tipo solicitado no coincide con el
|
||||||
|
/// almacenado. Incluye la clave (`key`), tipo esperado (`expected`) y tipo realmente encontrado
|
||||||
|
/// (`found`) para facilitar el diagnóstico.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum PropsError {
|
pub enum PropsError {
|
||||||
/// La clave no existe. Incluye la clave (`key`).
|
ExtraNotFound {
|
||||||
#[error("extra \"{key}\" not found")]
|
key: &'static str,
|
||||||
ExtraNotFound { key: &'static str },
|
},
|
||||||
/// La clave existe pero el tipo solicitado no coincide con el almacenado. Incluye la clave
|
|
||||||
/// (`key`), tipo esperado (`expected`) y tipo realmente encontrado (`found`) para facilitar el
|
|
||||||
/// diagnóstico.
|
|
||||||
#[error("type mismatch for extra \"{key}\": expected \"{expected}\", found \"{found}\"")]
|
|
||||||
ExtraTypeMismatch {
|
ExtraTypeMismatch {
|
||||||
key: &'static str,
|
key: &'static str,
|
||||||
expected: &'static str,
|
expected: &'static str,
|
||||||
|
|
@ -55,6 +54,24 @@ pub enum PropsError {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PropsError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
PropsError::ExtraNotFound { key } => write!(f, "extra \"{key}\" not found"),
|
||||||
|
PropsError::ExtraTypeMismatch {
|
||||||
|
key,
|
||||||
|
expected,
|
||||||
|
found,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"type mismatch for extra \"{key}\": expected \"{expected}\", found \"{found}\""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for PropsError {}
|
||||||
|
|
||||||
// **< PropsOp >************************************************************************************
|
// **< PropsOp >************************************************************************************
|
||||||
|
|
||||||
/// Operaciones sobre el identificador, clases CSS, atributos HTML y valores extra en [`Props`].
|
/// Operaciones sobre el identificador, clases CSS, atributos HTML y valores extra en [`Props`].
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{AutoDefault, CowStr, builder_fn};
|
use crate::{AutoDefault, CowStr, builder_fn};
|
||||||
|
|
||||||
use std::fmt::{self, Write as _};
|
use std::fmt;
|
||||||
|
|
||||||
/// Representa una ruta como un *path* inicial más una lista opcional de parámetros.
|
/// Representa una ruta como un *path* inicial más una lista opcional de parámetros.
|
||||||
///
|
///
|
||||||
|
|
@ -8,22 +8,13 @@ use std::fmt::{self, Write as _};
|
||||||
/// pensadas para usarse en atributos HTML como `href`, `action` o `src`.
|
/// pensadas para usarse en atributos HTML como `href`, `action` o `src`.
|
||||||
///
|
///
|
||||||
/// `RoutePath` no valida ni interpreta la estructura del *path*; simplemente concatena los
|
/// `RoutePath` no valida ni interpreta la estructura del *path*; simplemente concatena los
|
||||||
/// parámetros de consulta sobre el valor proporcionado. El *path* tampoco se codifica: se asume
|
/// parámetros de consulta sobre el valor proporcionado.
|
||||||
/// que ya es válido (rutas propias de la aplicación, normalmente literales o formadas a partir de
|
|
||||||
/// identificadores conocidos).
|
|
||||||
///
|
|
||||||
/// # Codificación de los valores
|
|
||||||
///
|
|
||||||
/// El método [`with_param()`](Self::with_param) codifica el **valor** (no la clave) según RFC 3986
|
|
||||||
/// antes de insertarlo. Así, cualquier valor (como una búsqueda de usuario, un destino con su
|
|
||||||
/// propia *query string*, etc.) puede pasarse tal cual, sin que quien llama tenga que codificarlo
|
|
||||||
/// primero.
|
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// // Ruta relativa con parámetros y un *flag* sin valor.
|
/// // Ruta relativa con parámetros y una *flag* sin valor.
|
||||||
/// let route = RoutePath::new("/search")
|
/// let route = RoutePath::new("/search")
|
||||||
/// .with_param("q", "rust")
|
/// .with_param("q", "rust")
|
||||||
/// .with_param("page", "2")
|
/// .with_param("page", "2")
|
||||||
|
|
@ -33,12 +24,8 @@ use std::fmt::{self, Write as _};
|
||||||
/// // Ruta absoluta a un recurso externo.
|
/// // Ruta absoluta a un recurso externo.
|
||||||
/// let external = RoutePath::new("https://example.com/export").with_param("format", "csv");
|
/// let external = RoutePath::new("https://example.com/export").with_param("format", "csv");
|
||||||
/// assert_eq!(external.to_string(), "https://example.com/export?format=csv");
|
/// assert_eq!(external.to_string(), "https://example.com/export?format=csv");
|
||||||
///
|
|
||||||
/// // Un valor con espacios o símbolos se codifica automáticamente.
|
|
||||||
/// let search = RoutePath::new("/search").with_param("q", "rust & htmx");
|
|
||||||
/// assert_eq!(search.to_string(), "/search?q=rust%20%26%20htmx");
|
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault)]
|
||||||
pub struct RoutePath {
|
pub struct RoutePath {
|
||||||
/// *Path* inicial sobre el que se añadirán los parámetros.
|
/// *Path* inicial sobre el que se añadirán los parámetros.
|
||||||
///
|
///
|
||||||
|
|
@ -65,17 +52,9 @@ impl RoutePath {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade o sustituye un parámetro `key=value`. Si la clave ya existe, el valor se sobrescribe.
|
/// Añade o sustituye un parámetro `key=value`. Si la clave ya existe, el valor se sobrescribe.
|
||||||
///
|
|
||||||
/// El valor se codifica según RFC 3986, los caracteres no reservados (alfanuméricos ASCII, `-`,
|
|
||||||
/// `_`, `.`, `~`) quedan intactos, el resto se sustituye por su secuencia `%XX`. La clave se
|
|
||||||
/// inserta tal cual, sin codificar.
|
|
||||||
///
|
|
||||||
/// Un `value` vacío no se distingue de [`with_flag()`](Self::with_flag): ambos se renderizan
|
|
||||||
/// como `?key`, sin `=`.
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||||
self.query
|
self.query.insert(key.into(), value.into());
|
||||||
.insert(key.into(), Self::encode_query_value(&value.into()));
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,29 +69,6 @@ impl RoutePath {
|
||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indica si el *path* **parece** una URL externa por su prefijo (ver
|
|
||||||
/// [`util::url_looks_external()`](crate::util::url_looks_external)).
|
|
||||||
pub fn is_external(&self) -> bool {
|
|
||||||
crate::util::url_looks_external(&self.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< RoutePath HELPERS >**********************************************************************
|
|
||||||
|
|
||||||
// Codifica un valor para su uso seguro como parte de una *query string* según RFC 3986: los
|
|
||||||
// caracteres no reservados quedan intactos y el resto se codifica como `%XX`.
|
|
||||||
fn encode_query_value(value: &str) -> String {
|
|
||||||
let mut out = String::with_capacity(value.len());
|
|
||||||
for byte in value.bytes() {
|
|
||||||
match byte {
|
|
||||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
|
||||||
out.push(byte as char);
|
|
||||||
}
|
|
||||||
_ => write!(out, "%{byte:02X}").unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for RoutePath {
|
impl fmt::Display for RoutePath {
|
||||||
|
|
@ -135,13 +91,9 @@ impl fmt::Display for RoutePath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cualquier `&str`, sea cual sea su vida, se acepta copiándolo a un `String` propio: así, por
|
impl From<&'static str> for RoutePath {
|
||||||
// ejemplo, una función hipotética que devuelva un `&str` con una vida atada a una petición, no
|
fn from(path: &'static str) -> Self {
|
||||||
// `'static` (del estilo `fn resolve_target<'a>(next: &'a str, fallback: &'a str) -> &'a str`),
|
RoutePath::new(path)
|
||||||
// sigue pudiendo construir un `RoutePath` sin que quien llama tenga que convertir el valor a mano.
|
|
||||||
impl From<&str> for RoutePath {
|
|
||||||
fn from(path: &str) -> Self {
|
|
||||||
RoutePath::new(path.to_owned())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5,19 +5,17 @@ use super::{LanguageIdentifier, langid};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// Tabla de idiomas soportados por PageTop.
|
/// Tabla de idiomas soportados por PageTop.
|
||||||
//
|
///
|
||||||
// Cada entrada asocia un código de idioma en minúsculas (por ejemplo, "en" o "es-es") con:
|
/// Cada entrada asocia un código de idioma en minúsculas (por ejemplo, `"en"` o `"es-es"`) con:
|
||||||
//
|
///
|
||||||
// - Su `LanguageIdentifier` canónico.
|
/// - Su [`LanguageIdentifier`] canónico.
|
||||||
// - La clave de traducción definida en `src/locale/{lang}/languages.ftl` para mostrar su nombre en
|
/// - La clave de traducción definida en `src/locale/{lang}/languages.ftl` para mostrar su nombre en
|
||||||
// el idioma activo.
|
/// el idioma activo.
|
||||||
//
|
///
|
||||||
// Esto permite admitir alias de idioma como "en" o "es" y, al mismo tiempo, mantener un
|
/// Esto permite admitir alias de idioma como `"en"` o `"es"` y, al mismo tiempo, mantener un
|
||||||
// identificador de idioma canónico (por ejemplo, `langid!("en-US")` o `langid!("es-ES")`).
|
/// identificador de idioma canónico (por ejemplo, `langid!("en-US")` o `langid!("es-ES")`).
|
||||||
//
|
pub(crate) static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> =
|
||||||
// Sólo lo usa `locale::definition`, en el mismo módulo `locale`.
|
|
||||||
pub(super) static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> =
|
|
||||||
LazyLock::new(|| {
|
LazyLock::new(|| {
|
||||||
util::kv![
|
util::kv![
|
||||||
"en" => ( langid!("en-US"), "english" ),
|
"en" => ( langid!("en-US"), "english" ),
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ pub use crate::core::theme::*;
|
||||||
|
|
||||||
pub use crate::auth::*;
|
pub use crate::auth::*;
|
||||||
|
|
||||||
pub use crate::response::*;
|
pub use crate::response::{json::*, page::*, redirect::*};
|
||||||
|
|
||||||
pub use crate::base::action;
|
pub use crate::base::action;
|
||||||
pub use crate::base::component::*;
|
pub use crate::base::component::*;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
//! Respuestas a las peticiones web en sus diferentes formatos.
|
//! Respuestas a las peticiones web en sus diferentes formatos.
|
||||||
|
|
||||||
mod page;
|
pub mod page;
|
||||||
pub use page::*;
|
|
||||||
|
|
||||||
mod json;
|
pub mod json;
|
||||||
pub use json::*;
|
|
||||||
|
|
||||||
mod redirect;
|
pub mod redirect;
|
||||||
pub use redirect::*;
|
|
||||||
|
|
||||||
mod waypoint;
|
|
||||||
pub use waypoint::*;
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
//!
|
//!
|
||||||
//! - **Respuestas especiales**.
|
//! - **Respuestas especiales**.
|
||||||
|
|
||||||
use crate::html::RoutePath;
|
|
||||||
use crate::web::{IntoResponse, Response, http};
|
use crate::web::{IntoResponse, Response, http};
|
||||||
|
|
||||||
/// Funciones predefinidas para generar respuestas HTTP de redirección.
|
/// Funciones predefinidas para generar respuestas HTTP de redirección.
|
||||||
|
|
@ -35,10 +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: impl Into<RoutePath>) -> Response {
|
pub fn moved(redirect_to_url: &str) -> Response {
|
||||||
(
|
(
|
||||||
http::StatusCode::MOVED_PERMANENTLY,
|
http::StatusCode::MOVED_PERMANENTLY,
|
||||||
[(http::header::LOCATION, redirect_to_url.into().to_string())],
|
[(http::header::LOCATION, redirect_to_url.to_owned())],
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -48,10 +47,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: impl Into<RoutePath>) -> Response {
|
pub fn permanent(redirect_to_url: &str) -> Response {
|
||||||
(
|
(
|
||||||
http::StatusCode::PERMANENT_REDIRECT,
|
http::StatusCode::PERMANENT_REDIRECT,
|
||||||
[(http::header::LOCATION, redirect_to_url.into().to_string())],
|
[(http::header::LOCATION, redirect_to_url.to_owned())],
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -62,10 +61,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: impl Into<RoutePath>) -> Response {
|
pub fn found(redirect_to_url: &str) -> Response {
|
||||||
(
|
(
|
||||||
http::StatusCode::FOUND,
|
http::StatusCode::FOUND,
|
||||||
[(http::header::LOCATION, redirect_to_url.into().to_string())],
|
[(http::header::LOCATION, redirect_to_url.to_owned())],
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -76,10 +75,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: impl Into<RoutePath>) -> Response {
|
pub fn see_other(redirect_to_url: &str) -> Response {
|
||||||
(
|
(
|
||||||
http::StatusCode::SEE_OTHER,
|
http::StatusCode::SEE_OTHER,
|
||||||
[(http::header::LOCATION, redirect_to_url.into().to_string())],
|
[(http::header::LOCATION, redirect_to_url.to_owned())],
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -89,10 +88,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: impl Into<RoutePath>) -> Response {
|
pub fn temporary(redirect_to_url: &str) -> Response {
|
||||||
(
|
(
|
||||||
http::StatusCode::TEMPORARY_REDIRECT,
|
http::StatusCode::TEMPORARY_REDIRECT,
|
||||||
[(http::header::LOCATION, redirect_to_url.into().to_string())],
|
[(http::header::LOCATION, redirect_to_url.to_owned())],
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
//! Parámetro `waypoint`, URL de destino transportada entre pantallas.
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::AutoDefault;
|
|
||||||
use crate::html::RoutePath;
|
|
||||||
|
|
||||||
/// URL de destino transportada por el parámetro `waypoint`.
|
|
||||||
///
|
|
||||||
/// Cualquier pantalla alcanzada desde un listado (alta, edición, confirmación, o una cadena de
|
|
||||||
/// varias de ellas) puede recibir en su *query string* un parámetro `waypoint` con la URL de la
|
|
||||||
/// página a la que ir para continuar.
|
|
||||||
///
|
|
||||||
/// `Waypoint` transporta ese valor a través de la cadena de pantallas. Se extrae de la petición
|
|
||||||
/// entrante con [`web::Query`](crate::web::Query), igual que cualquier otro parámetro, y se repone
|
|
||||||
/// en cada enlace o acción de formulario intermedia con [`append_to()`](Self::append_to) para que
|
|
||||||
/// sobreviva a la siguiente petición. Se resuelve con [`or()`](Self::or) al decidir el destino de
|
|
||||||
/// un enlace de vuelta o de una redirección.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// const ITEMS_PATH: &str = "/items";
|
|
||||||
///
|
|
||||||
/// /// GET /items/{id}/edit - Formulario de edición, alcanzado desde el listado.
|
|
||||||
/// async fn edit_get(
|
|
||||||
/// web::Path(id): web::Path<i32>,
|
|
||||||
/// web::Query(waypoint): web::Query<Waypoint>,
|
|
||||||
/// ) -> Markup {
|
|
||||||
/// // El listado de origen (si lo hay) viaja en la acción del formulario, para volver después.
|
|
||||||
/// let action = waypoint.append_to(format!("{ITEMS_PATH}/{id}/edit"));
|
|
||||||
/// html! { form action=(action) method="post" { /* ... */ } }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// /// POST /items/{id}/edit - Guarda los cambios y vuelve al listado de origen, o a `ITEMS_PATH`.
|
|
||||||
/// async fn edit_post(
|
|
||||||
/// web::Path(id): web::Path<i32>,
|
|
||||||
/// web::Query(waypoint): web::Query<Waypoint>,
|
|
||||||
/// ) -> Response {
|
|
||||||
/// Redirect::see_other(waypoint.or(ITEMS_PATH))
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Deserialize)]
|
|
||||||
#[serde(from = "RawWaypoint")]
|
|
||||||
pub struct Waypoint {
|
|
||||||
waypoint: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Waypoint {
|
|
||||||
/// Crea un valor explícito.
|
|
||||||
///
|
|
||||||
/// Sólo acepta una ruta local que empiece por `/`, pero no por `//` ni `/\` que el navegador
|
|
||||||
/// interpretaría como una URL *protocol-relative*. Una cadena vacía, una URL absoluta
|
|
||||||
/// (`https://...`), un esquema arbitrario (`javascript:...`) o cualquier otro valor que no
|
|
||||||
/// cumpla esa forma se trata igual que `None`.
|
|
||||||
///
|
|
||||||
/// Este filtro protege contra *open redirect*; el `waypoint` viaja en la *query string* de la
|
|
||||||
/// petición, así que un cliente malicioso lo controla por completo. Por eso se aplica siempre,
|
|
||||||
/// también al deserializar con [`web::Query`](crate::web::Query), el punto de entrada habitual
|
|
||||||
/// donde el valor procede de fuera de la aplicación.
|
|
||||||
pub fn new(waypoint: impl Into<Option<String>>) -> Self {
|
|
||||||
Self {
|
|
||||||
waypoint: waypoint.into().filter(|d| {
|
|
||||||
// Sólo acepta `/algo`. Rechaza cadenas vacías, esquemas (`https:`, `javascript:`) y
|
|
||||||
// las URLs *protocol-relative* (`//evil.example`, `/\evil.example`).
|
|
||||||
let mut chars = d.chars();
|
|
||||||
match chars.next() {
|
|
||||||
Some('/') => !matches!(chars.next(), Some('/') | Some('\\')),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la URL de destino, si se proporcionó una y es una ruta local válida.
|
|
||||||
pub fn as_str(&self) -> Option<&str> {
|
|
||||||
self.waypoint.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade `?waypoint=<url codificada>` a `route`, si hay una URL de destino propia. Si no,
|
|
||||||
/// devuelve `route` sin modificar.
|
|
||||||
///
|
|
||||||
/// Es la operación habitual para que un enlace o la acción de un formulario transporten el
|
|
||||||
/// waypoint a la siguiente pantalla. Igual que [`or()`](Self::or), devuelve un [`RoutePath`]
|
|
||||||
/// convertible directamente en [`Route`](crate::core::component::Route) sin volver a
|
|
||||||
/// procesarse. A diferencia de [`or()`](Self::or), nunca sustituye `route`; úsalo para propagar
|
|
||||||
/// el waypoint a un enlace intermedio, no para resolver un destino final.
|
|
||||||
///
|
|
||||||
/// Aparece en dos momentos típicos: el `action` de un formulario, para que un envío no pierda
|
|
||||||
/// el waypoint (ver el ejemplo del módulo), y el `href` de los enlaces que un listado genera
|
|
||||||
/// hacia otras pantallas (ver el ejemplo).
|
|
||||||
///
|
|
||||||
/// Preserva cualquier *query string* ya presente en `route` sin romperla (como `lang=...` si
|
|
||||||
/// `route` se construyó con [`Context::route()`](crate::core::component::Context::route)).
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// const ITEMS_PATH: &str = "/items";
|
|
||||||
///
|
|
||||||
/// fn edit_href(cx: &Context, waypoint: &Waypoint, item_id: i32) -> RoutePath {
|
|
||||||
/// waypoint.append_to(cx.route(format!("{ITEMS_PATH}/{item_id}/edit")))
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub fn append_to(&self, route: impl Into<RoutePath>) -> RoutePath {
|
|
||||||
let mut route = route.into();
|
|
||||||
if let Some(d) = self.as_str() {
|
|
||||||
route.alter_param("waypoint", d);
|
|
||||||
}
|
|
||||||
route
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la URL de destino, o `fallback` si no se proporcionó ninguna.
|
|
||||||
///
|
|
||||||
/// Es la operación habitual al decidir el destino de un enlace de vuelta o de una redirección:
|
|
||||||
/// el waypoint transportado si lo hay, o la URL del listado por defecto si se llegó a esta
|
|
||||||
/// pantalla sin pasar por ninguno. A diferencia de [`append_to()`](Self::append_to), sustituye
|
|
||||||
/// por completo el destino. Úsalo cuando necesites un único `RoutePath` final, no para propagar
|
|
||||||
/// el waypoint a otro enlace intermedio.
|
|
||||||
///
|
|
||||||
/// Aparece en dos momentos típicos de una misma pantalla: el `href` del enlace "volver" al
|
|
||||||
/// renderizarla (ver el ejemplo) y el destino de `Redirect::see_other(...)` tras guardar con
|
|
||||||
/// éxito en un POST, como en el ejemplo del módulo.
|
|
||||||
///
|
|
||||||
/// Devuelve un [`RoutePath`], no una cadena ya renderizada, para que el resultado pueda
|
|
||||||
/// convertirse directamente en una [`Route`](crate::core::component::Route) (por ejemplo al
|
|
||||||
/// pasarlo a [`Form::with_action()`](crate::base::component::Form::with_action)) sin volver a
|
|
||||||
/// procesarse. `fallback` acepta cualquier tipo convertible a `RoutePath` (un literal, un
|
|
||||||
/// `String`, o un `RoutePath` ya construido); preservando los parámetros que correspondan.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// const ITEMS_PATH: &str = "/items";
|
|
||||||
///
|
|
||||||
/// fn back_href(cx: &Context, waypoint: &Waypoint) -> RoutePath {
|
|
||||||
/// waypoint.or(cx.route(ITEMS_PATH))
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub fn or(&self, fallback: impl Into<RoutePath>) -> RoutePath {
|
|
||||||
match self.as_str() {
|
|
||||||
Some(d) => RoutePath::new(d.to_owned()),
|
|
||||||
None => fallback.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Option<String>> for Waypoint {
|
|
||||||
fn from(waypoint: Option<String>) -> Self {
|
|
||||||
Self::new(waypoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Waypoint {
|
|
||||||
fn from(waypoint: String) -> Self {
|
|
||||||
Self::new(Some(waypoint))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valor tal y como llega en la deserialización (p. ej. desde `web::Query`), antes de ser filtrado.
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct RawWaypoint {
|
|
||||||
#[serde(default)]
|
|
||||||
waypoint: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RawWaypoint> for Waypoint {
|
|
||||||
fn from(raw: RawWaypoint) -> Self {
|
|
||||||
Self::new(raw.waypoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
39
src/util.rs
39
src/util.rs
|
|
@ -168,45 +168,6 @@ pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indica si una URL **parece** externa por su prefijo.
|
|
||||||
///
|
|
||||||
/// No es una validación de la URL; sólo mira el inicio del texto: `//` (relativa al protocolo),
|
|
||||||
/// `http://`, `https://`, `mailto:` o `tel:`. La comparación ignora mayúsculas/minúsculas ASCII en
|
|
||||||
/// los prefijos (`HTTPS://...` o `MAILTO:...` se detectan igual), porque el esquema de una URI es
|
|
||||||
/// *case-insensitive* según RFC 3986.
|
|
||||||
///
|
|
||||||
/// La usan [`RoutePath::is_external()`](crate::html::RoutePath::is_external) y las conversiones
|
|
||||||
/// `From<&str>`/`From<String>` de [`Route`](crate::core::component::Route) para decidir si una
|
|
||||||
/// ruta debe evitar [`Context::route()`](crate::core::component::Context::route).
|
|
||||||
///
|
|
||||||
/// Cualquier otro código que necesite el mismo criterio (por ejemplo, para decidir si un enlace
|
|
||||||
/// debe llevar `target="_blank"`) puede usarla en lugar de reimplementar su propia versión.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop::util;
|
|
||||||
/// assert!(util::url_looks_external("https://example.com"));
|
|
||||||
/// assert!(util::url_looks_external("mailto:info@example.com"));
|
|
||||||
/// assert!(util::url_looks_external("HTTPS://EXAMPLE.COM"));
|
|
||||||
/// assert!(!util::url_looks_external("/admin/users"));
|
|
||||||
/// ```
|
|
||||||
pub fn url_looks_external(url: &str) -> bool {
|
|
||||||
starts_with_ignore_ascii_case(url, "//")
|
|
||||||
|| starts_with_ignore_ascii_case(url, "http://")
|
|
||||||
|| starts_with_ignore_ascii_case(url, "https://")
|
|
||||||
|| starts_with_ignore_ascii_case(url, "mailto:")
|
|
||||||
|| starts_with_ignore_ascii_case(url, "tel:")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compara si `text` empieza por `prefix` ignorando mayúsculas/minúsculas en ASCII (los esquemas de
|
|
||||||
// URI son case-insensitive según RFC 3986). No reserva memoria: sólo compara el primer tramo de
|
|
||||||
// bytes de `text` con `prefix`.
|
|
||||||
fn starts_with_ignore_ascii_case(text: &str, prefix: &str) -> bool {
|
|
||||||
text.get(..prefix.len())
|
|
||||||
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
|
/// Resuelve y valida la ruta de un directorio existente, devolviendo una ruta absoluta.
|
||||||
///
|
///
|
||||||
/// - Si la ruta es relativa, se resuelve respecto al directorio del proyecto según la variable de
|
/// - Si la ruta es relativa, se resuelve respecto al directorio del proyecto según la variable de
|
||||||
|
|
|
||||||
57
src/web.rs
57
src/web.rs
|
|
@ -1,13 +1,12 @@
|
||||||
//! Servidor web y rutas de la aplicación (basado en [Axum](https://docs.rs/axum)).
|
//! Servidor web y rutas de la aplicación (basado en [Axum](https://docs.rs/axum)).
|
||||||
//!
|
//!
|
||||||
//! Define rutas y handlers: el [`Router`], las operaciones HTTP ([`get`], [`post`], [`put`],
|
//! Define rutas y *handlers*: el [`Router`], las operaciones HTTP ([`get`], [`post`], [`put`],
|
||||||
//! [`delete`], [`patch`]), los extractores ([`Path`], [`Query`], [`Form`], [`RawForm`],
|
//! [`delete`], [`patch`]), los extractores ([`Path`], [`Query`]) e [`IntoResponse`], y re-exporta
|
||||||
//! [`Request`]) e [`IntoResponse`], el módulo [`middleware`] para *middlewares* de función, y
|
//! el módulo `http` para tipos de bajo nivel como `StatusCode`, `HeaderName` o `Method`. También
|
||||||
//! re-exporta el módulo `http` para tipos de bajo nivel como `StatusCode`, `HeaderName` o `Method`.
|
//! ofrece utilidades para servir archivos estáticos, [`ServeDir`] y [`ServeEmbedded`].
|
||||||
//! También ofrece utilidades para servir archivos estáticos, [`ServeDir`] y [`ServeEmbedded`].
|
|
||||||
//!
|
//!
|
||||||
//! Los handlers son las funciones asíncronas que el servidor invoca al recibir una petición HTTP en
|
//! Los *handlers* son las funciones asíncronas que el servidor invoca al recibir una petición
|
||||||
//! una ruta concreta.
|
//! HTTP en una ruta concreta.
|
||||||
|
|
||||||
use crate::StaticFile;
|
use crate::StaticFile;
|
||||||
|
|
||||||
|
|
@ -18,7 +17,7 @@ pub use axum::http;
|
||||||
pub use axum::Router;
|
pub use axum::Router;
|
||||||
|
|
||||||
// Extractores de petición.
|
// Extractores de petición.
|
||||||
pub use axum::extract::{Form, Path, Query, RawForm, Request};
|
pub use axum::extract::{Path, Query};
|
||||||
|
|
||||||
// Para implementar respuestas.
|
// Para implementar respuestas.
|
||||||
pub use axum::response::{IntoResponse, Response};
|
pub use axum::response::{IntoResponse, Response};
|
||||||
|
|
@ -26,9 +25,6 @@ pub use axum::response::{IntoResponse, Response};
|
||||||
// Operaciones HTTP para registrar rutas.
|
// Operaciones HTTP para registrar rutas.
|
||||||
pub use axum::routing::{delete, get, patch, post, put};
|
pub use axum::routing::{delete, get, patch, post, put};
|
||||||
|
|
||||||
// Middlewares de función (`from_fn`) y tipos asociados (`Next`).
|
|
||||||
pub use axum::middleware;
|
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::extract::FromRequestParts;
|
use axum::extract::FromRequestParts;
|
||||||
|
|
||||||
|
|
@ -47,7 +43,7 @@ use std::task::{Context, Poll};
|
||||||
///
|
///
|
||||||
/// Puede declararse directamente como parámetro en un handler para pasarlo al
|
/// Puede declararse directamente como parámetro en un handler para pasarlo al
|
||||||
/// [`Context`](crate::core::component::Context) de renderizado y a las variantes de
|
/// [`Context`](crate::core::component::Context) de renderizado y a las variantes de
|
||||||
/// [`ErrorPage`](crate::response::ErrorPage):
|
/// [`ErrorPage`](crate::response::page::ErrorPage):
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
|
|
@ -66,12 +62,25 @@ use std::task::{Context, Poll};
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// `HttpRequest` no consume las extensiones de la petición, por lo que el resto de extractores del
|
/// # Orden de parámetros en el handler
|
||||||
/// handler que también las necesiten (como `Path<T>`, `Extension<T>`...) las siguen viendo
|
///
|
||||||
/// intactas, sea cual sea la posición en la que se declare `HttpRequest`. Por convención, se
|
/// `HttpRequest` toma las extensiones de la petición al extraerse. Los extractores que leen de ahí
|
||||||
/// declara como primer parámetro del handler. El único orden que sigue siendo obligatorio es el que
|
/// (como `Path<T>` o `Extension<T>`) deben aparecer **antes** en la lista de parámetros del
|
||||||
/// impone Axum: un extractor que consuma el cuerpo de la petición (`Form<T>`, `RawForm`...) debe ir
|
/// handler:
|
||||||
/// siempre el último.
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// // Correcto: Path<T> se declara antes que HttpRequest.
|
||||||
|
/// async fn view_post(
|
||||||
|
/// web::Path(id): web::Path<u32>,
|
||||||
|
/// request: HttpRequest,
|
||||||
|
/// ) -> Result<Markup, ErrorPage> {
|
||||||
|
/// # todo!()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Los extractores que no dependen de las extensiones, como `Query<T>` o `Method`, no están sujetos
|
||||||
|
/// a esta restricción.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct HttpRequest {
|
pub struct HttpRequest {
|
||||||
uri: http::Uri,
|
uri: http::Uri,
|
||||||
|
|
@ -119,18 +128,20 @@ impl HttpRequest {
|
||||||
impl<S: Send + Sync> FromRequestParts<S> for HttpRequest {
|
impl<S: Send + Sync> FromRequestParts<S> for HttpRequest {
|
||||||
type Rejection = Infallible;
|
type Rejection = Infallible;
|
||||||
|
|
||||||
// Clona (no toma) las extensiones inyectadas por middleware, para que otros extractores del
|
// Extrae la petición y toma las extensiones que han sido inyectadas por middleware. Las
|
||||||
// handler (`Path<T>`, `Extension<T>`...) las sigan viendo intactas sin importar en qué posición
|
// extensiones se mueven a un `Arc` compartido para que `HttpRequest` sea `Clone`.
|
||||||
// se declare `HttpRequest`. El clon se envuelve en un `Arc` compartido para que `HttpRequest`
|
//
|
||||||
// sea `Clone` a coste mínimo en el resto de su ciclo de vida.
|
// Nota: tras este extractor `parts.extensions` queda vacío; otros extractores que dependan de
|
||||||
|
// `Extension<T>` deben registrarse antes en la cadena del handler.
|
||||||
async fn from_request_parts(
|
async fn from_request_parts(
|
||||||
parts: &mut http::request::Parts,
|
parts: &mut http::request::Parts,
|
||||||
_state: &S,
|
_state: &S,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let extensions = std::mem::take(&mut parts.extensions);
|
||||||
Ok(HttpRequest {
|
Ok(HttpRequest {
|
||||||
uri: parts.uri.clone(),
|
uri: parts.uri.clone(),
|
||||||
headers: parts.headers.clone(),
|
headers: parts.headers.clone(),
|
||||||
extensions: Arc::new(parts.extensions.clone()),
|
extensions: Arc::new(extensions),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
tests/route.rs
124
tests/route.rs
|
|
@ -1,124 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// Forces an effective language different from the default negotiated one (en-US, with no `?lang` in
|
|
||||||
// the request), so that `Context::route()` decides to propagate `?lang=...` in local routes.
|
|
||||||
fn cx_with_lang(lang: &str) -> Context {
|
|
||||||
Context::new(None).with_langid(&Locale::resolve(lang))
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Route - automatic detection of external URLs >***********************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_from_str_detects_external_urls_by_prefix() {
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
for url in [
|
|
||||||
"http://example.com",
|
|
||||||
"https://example.com",
|
|
||||||
"//example.com",
|
|
||||||
"mailto:user@example.com",
|
|
||||||
"tel:+123456789",
|
|
||||||
] {
|
|
||||||
let route: Route = url.into();
|
|
||||||
assert_eq!(
|
|
||||||
route.resolve(&cx).to_string(),
|
|
||||||
url,
|
|
||||||
"expected {url:?} to be detected as external and left unmodified"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_from_string_behaves_like_from_str() {
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
let external: Route = String::from("https://example.com").into();
|
|
||||||
assert_eq!(external.resolve(&cx).to_string(), "https://example.com");
|
|
||||||
|
|
||||||
let local: Route = String::from("/local/path").into();
|
|
||||||
assert_eq!(local.resolve(&cx).to_string(), "/local/path?lang=es-ES");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_local_path_goes_through_context_route_and_preserves_lang() {
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
let route: Route = "/local/path".into();
|
|
||||||
assert_eq!(route.resolve(&cx).to_string(), "/local/path?lang=es-ES");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_unrecognized_scheme_is_not_detected_as_external() {
|
|
||||||
// `ftp://` is not among the prefixes recognized by the heuristic, so the implicit conversion
|
|
||||||
// treats it as a local path and goes through `Context::route()`; for a URL like this,
|
|
||||||
// `Route::external()` is still necessary (see the test below).
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
let route: Route = "ftp://files.example.com".into();
|
|
||||||
assert_eq!(
|
|
||||||
route.resolve(&cx).to_string(),
|
|
||||||
"ftp://files.example.com?lang=es-ES"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Route::external() / From<RoutePath> (share the same fixed resolution) >**********************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_external_ignores_the_rendering_context() {
|
|
||||||
let route = Route::external("/fixed/path");
|
|
||||||
|
|
||||||
let cx_es = cx_with_lang("es-ES");
|
|
||||||
let cx_en = cx_with_lang("en-US");
|
|
||||||
|
|
||||||
assert_eq!(route.resolve(&cx_es).to_string(), "/fixed/path");
|
|
||||||
assert_eq!(route.resolve(&cx_en).to_string(), "/fixed/path");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_from_route_path_ignores_the_rendering_context() {
|
|
||||||
let route: Route = RoutePath::new("/fixed/path").with_param("q", "rust").into();
|
|
||||||
|
|
||||||
let cx_es = cx_with_lang("es-ES");
|
|
||||||
let cx_en = cx_with_lang("en-US");
|
|
||||||
|
|
||||||
assert_eq!(route.resolve(&cx_es).to_string(), "/fixed/path?q=rust");
|
|
||||||
assert_eq!(route.resolve(&cx_en).to_string(), "/fixed/path?q=rust");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_external_never_adds_lang_even_for_unrecognized_schemes() {
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
let route = Route::external("ftp://files.example.com");
|
|
||||||
assert_eq!(route.resolve(&cx).to_string(), "ftp://files.example.com");
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Route::with() >******************************************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_with_captures_dynamic_values_from_the_environment() {
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
let user_id = 42;
|
|
||||||
let route = Route::with(move |cx| cx.route(format!("/users/{user_id}")));
|
|
||||||
|
|
||||||
assert_eq!(route.resolve(&cx).to_string(), "/users/42?lang=es-ES");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn route_default_resolves_to_an_empty_path() {
|
|
||||||
let cx = Context::new(None);
|
|
||||||
assert_eq!(Route::default().resolve(&cx).to_string(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Context::route() - direct protection against external URLs >*********************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn context_route_never_adds_lang_to_an_external_looking_url() {
|
|
||||||
let cx = cx_with_lang("es-ES");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
cx.route("https://example.com/help").to_string(),
|
|
||||||
"https://example.com/help"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< Waypoint - only accepts local paths >********************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn waypoint_accepts_local_paths() {
|
|
||||||
for path in ["/", "/admin/users", "/admin/users?page=2"] {
|
|
||||||
let w = Waypoint::from(path.to_owned());
|
|
||||||
assert_eq!(w.as_str(), Some(path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn waypoint_rejects_open_redirect_targets() {
|
|
||||||
// Targets that a malicious client could slip into `?waypoint=...` to redirect off-site: they
|
|
||||||
// must be discarded just as if no waypoint had been provided.
|
|
||||||
for target in [
|
|
||||||
"",
|
|
||||||
"https://evil.example",
|
|
||||||
"//evil.example",
|
|
||||||
"/\\evil.example",
|
|
||||||
"javascript:alert(1)",
|
|
||||||
] {
|
|
||||||
let w = Waypoint::from(target.to_owned());
|
|
||||||
assert_eq!(w.as_str(), None, "expected {target:?} to be rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn waypoint_deserialize_rejects_open_redirect_targets() {
|
|
||||||
// Reproduces the real entry point (`web::Query<Waypoint>`): the value arrives via
|
|
||||||
// deserialization, not through `Waypoint::from(String)` as in the previous test.
|
|
||||||
let w: Waypoint = serde_json::from_str(r#"{"waypoint":"https://evil.example"}"#).unwrap();
|
|
||||||
assert_eq!(w.as_str(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Waypoint::or() >*****************************************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn waypoint_or_returns_transported_local_path() {
|
|
||||||
let w = Waypoint::from("/admin/users?page=2".to_owned());
|
|
||||||
assert_eq!(w.or("/fallback").to_string(), "/admin/users?page=2");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn waypoint_or_falls_back_when_target_is_not_local() {
|
|
||||||
let w = Waypoint::from("https://evil.example".to_owned());
|
|
||||||
assert_eq!(w.or("/admin/users").to_string(), "/admin/users");
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Waypoint::append_to() >**********************************************************************
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn waypoint_append_to_adds_param_only_when_present() {
|
|
||||||
let w = Waypoint::from("/admin/users".to_owned());
|
|
||||||
assert_eq!(
|
|
||||||
w.append_to("/items").to_string(),
|
|
||||||
"/items?waypoint=%2Fadmin%2Fusers"
|
|
||||||
);
|
|
||||||
|
|
||||||
let empty = Waypoint::default();
|
|
||||||
assert_eq!(empty.append_to("/items").to_string(), "/items");
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue