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