From 9e625c2b46d765df59c77607d87820d70170f453 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 9 May 2026 08:18:28 +0200 Subject: [PATCH 01/29] =?UTF-8?q?=F0=9F=93=9D=20Retoques=20en=20README's?= =?UTF-8?q?=20y=20documentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CREDITS.md | 7 ------- README.md | 3 ++- extensions/pagetop-aliner/README.md | 1 - extensions/pagetop-bootsier/README.md | 8 +++++++- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index eaf97333..f61bf867 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -26,13 +26,6 @@ para mostrar un banner de presentación en el terminal con el nombre de la aplic * [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck* -# 🎨 CSS - -La extensión `pagetop-bootsier` es un tema que integra [Bootstrap 5.3.8](https://getbootstrap.com/) -para los estilos y componentes de la interfaz. Bootstrap está distribuido bajo licencia -[MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE). - - # 👾 Icono "La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys). diff --git a/README.md b/README.md index 463855f2..8ce2e3f1 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ [![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop#licencia) -
+
+ PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript. Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar diff --git a/extensions/pagetop-aliner/README.md b/extensions/pagetop-aliner/README.md index 09a337eb..7b772591 100644 --- a/extensions/pagetop-aliner/README.md +++ b/extensions/pagetop-aliner/README.md @@ -9,7 +9,6 @@ [![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia) -
## 🧭 Sobre PageTop diff --git a/extensions/pagetop-bootsier/README.md b/extensions/pagetop-bootsier/README.md index b34eeb51..edb0be75 100644 --- a/extensions/pagetop-bootsier/README.md +++ b/extensions/pagetop-bootsier/README.md @@ -9,7 +9,6 @@ [![Descargas](https://img.shields.io/crates/d/pagetop-bootsier.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia) -
## 🧭 Sobre PageTop @@ -80,6 +79,13 @@ async fn homepage(request: HttpRequest) -> ResultPage { ``` +## 📚 Créditos + +Este *crate* integra la biblioteca de estilos [Bootstrap 5.3.8](https://getbootstrap.com/) para +definir el comportamiento, la apariencia y los componentes de la interfaz. Bootstrap se distribuye +bajo licencia [MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE). + + ## 🚧 Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su From 35883bdcde435965040b0b5e4b6d20d0f38646b2 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 9 May 2026 09:35:59 +0200 Subject: [PATCH 02/29] =?UTF-8?q?=E2=9C=A8=20A=C3=B1ade=20alias=20`cargo?= =?UTF-8?q?=20td`=20y=20aclara=20doc=20de=20pruebas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cargo/config.toml | 1 + README.md | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index d29b0de3..610b7b2e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,4 @@ [alias] ts = ["test", "--features", "testing"] # cargo ts tw = ["test", "--workspace", "--features", "testing"] # cargo tw +td = ["test", "--doc", "-p"] # cargo td diff --git a/README.md b/README.md index 8ce2e3f1..62de4e2b 100644 --- a/README.md +++ b/README.md @@ -116,16 +116,20 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en `.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto: -| Comando | Descripción | -| ------- | ----------- | -| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. | -| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. | -| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. | -| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. | +| Comando | Descripción | +| ----------------------- | --------------------------------------------------------------- | +| `cargo ts` | Lanza **todos los tests** de `pagetop` | +| `cargo ts --test util` | Lanza los tests de integración del archivo `tests/util.rs` | +| `cargo ts --doc locale` | Lanza los *doctests* de `pagetop` cuyo *path* contiene `locale` | +| `cargo tw` | Lanza **todos los tests** del *workspace* | +| `cargo td ` | Lanza los *doctests* de un *crate* concreto del *workspace* | > **Nota** -> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`. -> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`. +> * Todos los alias, excepto `cargo td`, aplican la *feature* `testing` para los *crates* que la +> declaren. +> * Cuando lanza **todos los tests** se incluyen las pruebas unitarias, de integración y *doctests*. +> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente +> `cargo test`. ## 🚧 Advertencia From 50abfe3b564828288d5e60d76f29fd205a3bb1a2 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 9 May 2026 10:42:48 +0200 Subject: [PATCH 03/29] =?UTF-8?q?=F0=9F=8C=90=20(aliner):=20Localiza=20nom?= =?UTF-8?q?bre=20y=20descripci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-aliner/src/lib.rs | 10 ++++++++++ .../pagetop-aliner/src/locale/en-US/extension.ftl | 2 ++ .../pagetop-aliner/src/locale/es-ES/extension.ftl | 2 ++ 3 files changed, 14 insertions(+) create mode 100644 extensions/pagetop-aliner/src/locale/en-US/extension.ftl create mode 100644 extensions/pagetop-aliner/src/locale/es-ES/extension.ftl diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 95f22196..7a29eebc 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -83,6 +83,8 @@ async fn homepage(request: HttpRequest) -> ResultPage { use pagetop::prelude::*; +include_locales!(LOCALES_ALINER); + /// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML. /// /// Define un tema mínimo útil para: @@ -94,6 +96,14 @@ use pagetop::prelude::*; pub struct Aliner; impl Extension for Aliner { + fn name(&self) -> L10n { + L10n::t("extension_name", &LOCALES_ALINER) + } + + fn description(&self) -> L10n { + L10n::t("extension_description", &LOCALES_ALINER) + } + fn theme(&self) -> Option { Some(&Self) } diff --git a/extensions/pagetop-aliner/src/locale/en-US/extension.ftl b/extensions/pagetop-aliner/src/locale/en-US/extension.ftl new file mode 100644 index 00000000..e4fca26d --- /dev/null +++ b/extensions/pagetop-aliner/src/locale/en-US/extension.ftl @@ -0,0 +1,2 @@ +extension_name = Aliner +extension_description = Minimal theme that schematically shows the HTML page composition. diff --git a/extensions/pagetop-aliner/src/locale/es-ES/extension.ftl b/extensions/pagetop-aliner/src/locale/es-ES/extension.ftl new file mode 100644 index 00000000..5501e15a --- /dev/null +++ b/extensions/pagetop-aliner/src/locale/es-ES/extension.ftl @@ -0,0 +1,2 @@ +extension_name = Aliner +extension_description = Tema mínimo que muestra esquemáticamente la composición de las páginas HTML. \ No newline at end of file From b4284f74f86e410510d7a6eaa241f07195d8169f Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 9 May 2026 10:43:04 +0200 Subject: [PATCH 04/29] =?UTF-8?q?=F0=9F=8C=90=20(bootsier):=20Localiza=20n?= =?UTF-8?q?ombre=20y=20descripci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-bootsier/src/lib.rs | 8 ++++++++ .../pagetop-bootsier/src/locale/en-US/extension.ftl | 2 ++ .../pagetop-bootsier/src/locale/es-ES/extension.ftl | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 extensions/pagetop-bootsier/src/locale/en-US/extension.ftl create mode 100644 extensions/pagetop-bootsier/src/locale/es-ES/extension.ftl diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index d562ec09..b07b42d5 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -134,6 +134,14 @@ impl Template for BootsierTemplate { pub struct Bootsier; impl Extension for Bootsier { + fn name(&self) -> L10n { + L10n::t("extension_name", &LOCALES_BOOTSIER) + } + + fn description(&self) -> L10n { + L10n::t("extension_description", &LOCALES_BOOTSIER) + } + fn theme(&self) -> Option { Some(&Self) } diff --git a/extensions/pagetop-bootsier/src/locale/en-US/extension.ftl b/extensions/pagetop-bootsier/src/locale/en-US/extension.ftl new file mode 100644 index 00000000..b2820621 --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/en-US/extension.ftl @@ -0,0 +1,2 @@ +extension_name = Bootsier +extension_description = Bootstrap-based theme with flexible styles and components. diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/extension.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/extension.ftl new file mode 100644 index 00000000..7323cc1b --- /dev/null +++ b/extensions/pagetop-bootsier/src/locale/es-ES/extension.ftl @@ -0,0 +1,2 @@ +extension_name = Bootsier +extension_description = Tema basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles. From 23d4fd8a80fd344f76b808f524645634d46357fb Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 9 May 2026 13:07:49 +0200 Subject: [PATCH 05/29] =?UTF-8?q?=E2=9C=A8=20(seaorm):=20A=C3=B1ade=20acce?= =?UTF-8?q?so=20a=20bases=20de=20datos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1823 ++++++++++++++++- Cargo.toml | 2 + README.md | 3 + extensions/pagetop-seaorm/Cargo.toml | 36 + extensions/pagetop-seaorm/LICENSE-APACHE | 201 ++ extensions/pagetop-seaorm/LICENSE-MIT | 21 + extensions/pagetop-seaorm/README.md | 155 ++ extensions/pagetop-seaorm/src/config.rs | 72 + extensions/pagetop-seaorm/src/db.rs | 132 ++ extensions/pagetop-seaorm/src/db/dbconn.rs | 69 + extensions/pagetop-seaorm/src/db/migration.rs | 33 + .../src/db/migration/connection.rs | 148 ++ .../src/db/migration/manager.rs | 167 ++ .../src/db/migration/migrator.rs | 593 ++++++ .../src/db/migration/prelude.rs | 13 + .../pagetop-seaorm/src/db/migration/schema.rs | 608 ++++++ .../src/db/migration/seaql_migrations.rs | 15 + extensions/pagetop-seaorm/src/lib.rs | 29 + .../src/locale/en-US/extension.ftl | 2 + .../src/locale/es-ES/extension.ftl | 2 + tools/changelog.sh | 9 +- tools/release.sh | 2 + 22 files changed, 4051 insertions(+), 84 deletions(-) create mode 100644 extensions/pagetop-seaorm/Cargo.toml create mode 100644 extensions/pagetop-seaorm/LICENSE-APACHE create mode 100644 extensions/pagetop-seaorm/LICENSE-MIT create mode 100644 extensions/pagetop-seaorm/README.md create mode 100644 extensions/pagetop-seaorm/src/config.rs create mode 100644 extensions/pagetop-seaorm/src/db.rs create mode 100644 extensions/pagetop-seaorm/src/db/dbconn.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration/connection.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration/manager.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration/migrator.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration/prelude.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration/schema.rs create mode 100644 extensions/pagetop-seaorm/src/db/migration/seaql_migrations.rs create mode 100644 extensions/pagetop-seaorm/src/lib.rs create mode 100644 extensions/pagetop-seaorm/src/locale/en-US/extension.ftl create mode 100644 extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl diff --git a/Cargo.lock b/Cargo.lock index bf8d11ed..f2541410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", + "bitflags 2.11.1", "bytes", "futures-core", "futures-sink", @@ -29,7 +29,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web", - "bitflags", + "bitflags 2.11.1", "bytes", "derive_more 2.1.1", "futures-core", @@ -53,7 +53,7 @@ dependencies = [ "actix-service", "actix-utils", "base64 0.22.1", - "bitflags", + "bitflags 2.11.1", "brotli", "bytes", "bytestring", @@ -73,7 +73,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand 0.10.1", - "sha1", + "sha1 0.11.0", "smallvec", "tokio", "tokio-util", @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -222,7 +222,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -273,6 +273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -287,6 +288,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -373,6 +380,196 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.4.1", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io 2.6.0", + "async-lock 3.4.2", + "blocking", + "futures-lite 2.6.1", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling 3.11.0", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 2.6.0", + "async-lock 3.4.2", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.1", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -385,17 +582,38 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] [[package]] name = "block-buffer" @@ -415,6 +633,19 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + [[package]] name = "brotli" version = "8.0.2" @@ -475,9 +706,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -589,6 +820,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.15.22" @@ -601,6 +841,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -640,6 +886,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -664,6 +920,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -701,6 +972,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -736,6 +1016,51 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -755,7 +1080,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.117", ] [[package]] @@ -777,7 +1102,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.117", "unicode-xid", ] @@ -788,18 +1113,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.1", ] @@ -811,7 +1137,34 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "educe" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", ] [[package]] @@ -823,6 +1176,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -839,6 +1212,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -902,7 +1322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -915,7 +1335,7 @@ dependencies = [ "ignore", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "unic-langid", ] @@ -933,7 +1353,7 @@ dependencies = [ "ignore", "intl-memoizer", "log", - "thiserror", + "thiserror 2.0.18", "unic-langid", ] @@ -943,6 +1363,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ + "futures-core", + "futures-sink", "spin", ] @@ -958,6 +1380,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -967,12 +1404,104 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.4.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -991,8 +1520,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -1054,7 +1588,7 @@ checksum = "c43d815f896a3c730f0d76b8348a1700dc8d8fd6c377e4590d531bdd646574d8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1086,6 +1620,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "grass" version = "0.13.4" @@ -1151,9 +1697,27 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -1161,6 +1725,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1179,6 +1761,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "0.2.12" @@ -1329,6 +1920,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1379,7 +1976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1393,6 +1990,17 @@ dependencies = [ "rustversion", ] +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "inout" version = "0.1.4" @@ -1402,6 +2010,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "intl-memoizer" version = "0.5.3" @@ -1421,6 +2038,17 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1445,9 +2073,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", @@ -1455,6 +2083,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1475,6 +2112,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -1488,6 +2128,41 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1531,6 +2206,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "matchers" @@ -1541,6 +2219,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1563,6 +2251,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1591,6 +2285,33 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1600,12 +2321,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1613,6 +2370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1633,6 +2391,82 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pagetop" version = "0.5.0" @@ -1699,7 +2533,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1711,6 +2545,19 @@ dependencies = [ "pastey", ] +[[package]] +name = "pagetop-seaorm" +version = "0.0.4" +dependencies = [ + "async-trait", + "futures", + "pagetop", + "sea-orm", + "sea-schema", + "serde", + "url", +] + [[package]] name = "pagetop-statics" version = "0.1.3" @@ -1723,6 +2570,12 @@ dependencies = [ "path-slash 0.2.1", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1741,11 +2594,17 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pastey" version = "0.2.2" @@ -1779,6 +2638,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1815,7 +2683,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1829,22 +2697,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1853,12 +2721,86 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.4.1", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "polyval" version = "0.6.2" @@ -1902,7 +2844,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1928,7 +2916,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "version_check", ] @@ -2035,7 +3023,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.11.1", ] [[package]] @@ -2073,6 +3070,26 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2088,16 +3105,30 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -2122,12 +3153,152 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sea-orm" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1fee0cf8528dbe6eda29d5798afc522a63b75e44c5b15721e6e64af9c7cc4b" +dependencies = [ + "async-stream", + "async-trait", + "futures", + "log", + "ouroboros", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "sqlx", + "strum", + "thiserror 1.0.69", + "tracing", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8737b566799ed0444f278d13c300c4c6f1a91782f60ff5825a591852d5502030" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.117", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fd043b8117af233e221f73e3ea8dfbc8e8c3c928017c474296db45c649105c" +dependencies = [ + "educe", + "inherent", + "ordered-float", + "sea-query-derive", +] + +[[package]] +name = "sea-query-binder" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754965d4aee6145bec25d0898e5c931e6c22859789ce62fd85a42a15ed5a8ce3" +dependencies = [ + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.117", + "thiserror 2.0.18", +] + +[[package]] +name = "sea-schema" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad52149fc81836ea7424c3425d8f6ed8ad448dd16d2e4f6a3907ba46f3f2fd78" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.2.2" @@ -2167,7 +3338,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2204,6 +3375,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.11.0" @@ -2212,7 +3394,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -2251,6 +3433,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -2259,9 +3451,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -2275,6 +3467,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.10" @@ -2304,18 +3506,256 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "async-io 1.13.0", + "async-std", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "async-std", + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.11.1", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1 0.10.6", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.11.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "substring" version = "1.4.5" @@ -2337,6 +3777,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -2356,7 +3807,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2365,10 +3816,10 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand", + "fastrand 2.4.1", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2378,17 +3829,37 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2399,7 +3870,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2454,10 +3925,25 @@ dependencies = [ ] [[package]] -name = "tokio" -version = "1.52.1" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2546,7 +4032,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror", + "thiserror 2.0.18", "time", "tracing-subscriber", ] @@ -2559,7 +4045,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2668,7 +4154,7 @@ checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" dependencies = [ "proc-macro-hack", "quote", - "syn", + "syn 2.0.117", "unic-langid-impl", ] @@ -2678,12 +4164,33 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -2696,6 +4203,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.5.1" @@ -2718,6 +4231,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2753,12 +4272,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -2794,10 +4331,16 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.120" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -2807,10 +4350,20 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" +name = "wasm-bindgen-futures" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2818,22 +4371,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -2866,12 +4419,38 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2881,6 +4460,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2902,7 +4487,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2913,7 +4498,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2940,13 +4525,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2958,34 +4552,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2998,24 +4625,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3053,7 +4704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -3064,10 +4715,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3083,7 +4734,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3095,7 +4746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -3150,7 +4801,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3171,7 +4822,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3191,10 +4842,16 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" @@ -3226,7 +4883,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d809c122..48bc600c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ members = [ # Extensions "extensions/pagetop-aliner", "extensions/pagetop-bootsier", + "extensions/pagetop-seaorm", ] [workspace.package] @@ -88,5 +89,6 @@ pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" } # Extensions pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" } pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" } +pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" } # PageTop pagetop = { version = "0.5", path = "." } diff --git a/README.md b/README.md index 62de4e2b..604c4b3c 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,9 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y componentes flexibles. + * **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**, + integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales. + ## 🧪 Pruebas diff --git a/extensions/pagetop-seaorm/Cargo.toml b/extensions/pagetop-seaorm/Cargo.toml new file mode 100644 index 00000000..404bb84b --- /dev/null +++ b/extensions/pagetop-seaorm/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "pagetop-seaorm" +version = "0.0.4" +edition = "2021" + +description = """ + Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales. +""" +categories = ["database", "development-tools", "asynchronous"] +keywords = ["pagetop", "database", "sql", "orm", "ssr"] + +repository.workspace = true +homepage.workspace = true +license.workspace = true +authors.workspace = true + +[features] +mysql = ["sea-orm/sqlx-mysql"] +postgres = ["sea-orm/sqlx-postgres"] +sqlite = ["sea-orm/sqlx-sqlite"] + +[dependencies] +pagetop.workspace = true +serde.workspace = true + +async-trait = "0.1" +futures = "0.3" +url = "2.5" + +[dependencies.sea-orm] +version = "~1.0" +features = ["debug-print", "macros", "runtime-async-std-native-tls"] +default-features = false + +[dependencies.sea-schema] +version = "~0.15" diff --git a/extensions/pagetop-seaorm/LICENSE-APACHE b/extensions/pagetop-seaorm/LICENSE-APACHE new file mode 100644 index 00000000..263ddac1 --- /dev/null +++ b/extensions/pagetop-seaorm/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Manuel Cillero + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/extensions/pagetop-seaorm/LICENSE-MIT b/extensions/pagetop-seaorm/LICENSE-MIT new file mode 100644 index 00000000..cd8af3d6 --- /dev/null +++ b/extensions/pagetop-seaorm/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Manuel Cillero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md new file mode 100644 index 00000000..aa76275a --- /dev/null +++ b/extensions/pagetop-seaorm/README.md @@ -0,0 +1,155 @@ +
+ +

PageTop SeaORM

+ +

Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales.

+ +[![Doc API](https://img.shields.io/docsrs/pagetop-seaorm?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-seaorm) +[![Crates.io](https://img.shields.io/crates/v/pagetop-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm) +[![Descargas](https://img.shields.io/crates/d/pagetop-seaorm.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm) +[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia) + +
+ +## 🧭 Sobre PageTop + +[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web +clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y +configurables, basadas en HTML, CSS y JavaScript. + + +## ⚡️ Guía rápida + +**Añade la dependencia** a tu `Cargo.toml` activando el motor de base de datos que necesites: + +```toml +[dependencies] +pagetop-seaorm = { version = "...", features = ["sqlite"] } +``` + +Las *features* disponibles son `mysql`, `postgres` y `sqlite`. + +**Configura la conexión** en el archivo de configuración de la aplicación: + +```toml +[database] +db_type = "sqlite" +db_name = "my_app.db" +max_pool_size = 5 +``` + +Para MySQL o PostgreSQL añade también `db_user`, `db_pass`, `db_host` y `db_port`. + +**Declara la extensión** en tu aplicación o en la extensión que la requiera: + +```rust,no_run +use pagetop::prelude::*; + +struct MyApp; + +impl Extension for MyApp { + fn dependencies(&self) -> Vec { + vec![ + &pagetop_seaorm::SeaORM, + ] + } + + fn initialize(&self) { + install_migrations!(m20240101_000001_create_users_table); + } +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&MyApp).run()?.await +} +``` + +**Escribe las migraciones** usando la API de SeaORM: + +```rust,no_run +use pagetop_seaorm::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto(Users::Table) + .col(pk_auto(Users::Id)) + .col(string_uniq(Users::Email)) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum Users { + Table, + Id, + Email, +} +``` + + +## 📚 Créditos + +Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/SeaQL) como: + +* [SeaORM](https://www.sea-ql.org/SeaORM), ORM asíncrono que usa internamente + [SQLx](https://github.com/launchbadge/sqlx) para el acceso y la ejecución de consultas a la base + de datos. + +* [SeaQuery](https://github.com/SeaQL/sea-query), generador de consultas SQL sobre el que se + construye el motor de migraciones y los *helpers* de esquema. + +* [sea-schema](https://github.com/SeaQL/sea-schema), librería de introspección de esquemas SQL, + usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas, + columnas, índices y claves externas). + +También incorpora código adaptado de las siguientes fuentes: + +* [**sea-orm-migration (v1.0.0)**](https://github.com/SeaQL/sea-orm/tree/1.0.0/sea-orm-migration): + El módulo de migraciones (`src/db/migration/`) es una versión personalizada de + [sea-orm-migration](https://crates.io/crates/sea-orm-migration). Se integra directamente en lugar + de usarlo como dependencia porque su paradigma de CLI no es compatible con el ciclo de vida de las + extensiones de PageTop, donde las migraciones deben ejecutarse durante la inicialización de cada + extensión. Los ficheros adaptados del original son: + + | Original en `sea-orm-migration` | Observaciones | + |---------------------------------|-----------------------------------------| + | `lib.rs` | Excluye módulos y exportaciones del CLI | + | `connection.rs` | Integración completa | + | `manager.rs` | Integración completa | + | `migrator.rs` | Omite la gestión de errores del CLI | + | `prelude.rs` | Excluye exportaciones del CLI | + | `seaql_migrations.rs` | Integración completa | + +* [**loco-rs/loco**](https://github.com/loco-rs/loco/blob/master/src/schema.rs): El módulo + `src/db/migration/schema.rs`, con funciones de ayuda para definir columnas de tablas de forma + ergonómica, está adaptado del fichero `src/schema.rs` del proyecto [Loco](https://loco.rs/). + + +## 🚧 Advertencia + +**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su +ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos +hasta que se libere la versión **1.0.0**. + + +## 📜 Licencia + +El código está disponible bajo una doble licencia: + + * **Licencia MIT** + ([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT) + + * **Licencia Apache, Versión 2.0** + ([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0) + +Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en +el ecosistema Rust. diff --git a/extensions/pagetop-seaorm/src/config.rs b/extensions/pagetop-seaorm/src/config.rs new file mode 100644 index 00000000..5c706659 --- /dev/null +++ b/extensions/pagetop-seaorm/src/config.rs @@ -0,0 +1,72 @@ +//! Opciones de configuración de la extensión. +//! +//! Ejemplo: +//! +//! ```toml +//! [database] +//! db_type = "mysql" +//! db_name = "db" +//! db_user = "user" +//! db_pass = "password" +//! db_host = "localhost" +//! db_port = 3306 +//! max_pool_size = 5 +//! ``` +//! +//! Uso: +//! +//! ```rust +//! use pagetop_seaorm::config; +//! +//! assert_eq!(config::SETTINGS.database.db_host, "localhost"); +//! ``` +//! +//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica +//! los valores a los ajustes. + +use pagetop::prelude::*; + +use serde::Deserialize; + +include_config!(SETTINGS: Settings => [ + // [database] + "database.db_type" => "", + "database.db_name" => "", + "database.db_user" => "", + "database.db_pass" => "", + "database.db_host" => "localhost", + "database.db_port" => 0, + "database.max_pool_size" => 5, +]); + +#[derive(Debug, Deserialize)] +/// Tipos para la sección [`[database]`](Database) de [`SETTINGS`]. +pub struct Settings { + pub database: Database, +} + +#[derive(Debug, Deserialize)] +/// Sección `[database]` de la configuración. Forma parte de [`Settings`]. +pub struct Database { + /// Tipo de base de datos: *"mysql"*, *"postgres"* ó *"sqlite"*. + /// Por defecto: *""*. + pub db_type: String, + /// Nombre (para mysql/postgres) o referencia (para sqlite) de la base de datos. + /// Por defecto: *""*. + pub db_name: String, + /// Usuario de conexión a la base de datos (para mysql/postgres). + /// Por defecto: *""*. + pub db_user: String, + /// Contraseña para la conexión a la base de datos (para mysql/postgres). + /// Por defecto: *""*. + pub db_pass: String, + /// Servidor de conexión a la base de datos (para mysql/postgres). + /// Por defecto: *"localhost"*. + pub db_host: String, + /// Puerto de conexión a la base de datos, normalmente 3306 (para mysql) ó 5432 (para postgres). + /// Por defecto: *0*. + pub db_port: u16, + /// Número máximo de conexiones habilitadas. + /// Por defecto: *5*. + pub max_pool_size: u32, +} diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs new file mode 100644 index 00000000..70b8e57c --- /dev/null +++ b/extensions/pagetop-seaorm/src/db.rs @@ -0,0 +1,132 @@ +use pagetop::core::TypeInfo; +use pagetop::trace; + +pub use url::Url as DbUri; + +pub use sea_orm::error::{DbErr, RuntimeErr}; +pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult}; + +use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + +mod dbconn; +pub(crate) use dbconn::{run_now, DBCONN}; + +// The migration module is a customized version of the sea_orm_migration module (v1.0.0) +// https://github.com/SeaQL/sea-orm/tree/1.0.0/sea-orm-migration to avoid errors caused by the +// package paradigm of PageTop. Files integrated from original: +// +// lib.rs => db/migration.rs . . . . . . . . . . . . . . (excluding some modules and exports) +// connection.rs => db/migration/connection.rs . . . . . . . . . . . . . . (full integration) +// manager.rs => db/migration/manager.rs . . . . . . . . . . . . . . . . . (full integration) +// migrator.rs => db/migration/migrator.rs . . . . . . . . . . . .(omitting error management) +// prelude.rs => db/migration/prelude.rs . . . . . . . . . . . . . . . . . . . (avoiding CLI) +// seaql_migrations.rs => db/migration/seaql_migrations.rs . . . . . . . . (full integration) +// +mod migration; +pub use migration::prelude::*; +pub use migration::schema::*; + +pub async fn query(stmt: &mut Q) -> Result, DbErr> { + let dbconn = &*DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .query_all(Statement::from_string( + dbbackend, + match dbbackend { + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + }, + )) + .await +} + +pub async fn exec(stmt: &mut Q) -> Result, DbErr> { + let dbconn = &*DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .query_one(Statement::from_string( + dbbackend, + match dbbackend { + DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), + DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), + DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), + }, + )) + .await +} + +pub async fn exec_raw(stmt: String) -> Result { + let dbconn = &*DBCONN; + let dbbackend = dbconn.get_database_backend(); + dbconn + .execute(Statement::from_string(dbbackend, stmt)) + .await +} + +pub trait MigratorBase { + fn run_up(); + + fn run_down(); +} + +#[rustfmt::skip] +impl MigratorBase for M { + fn run_up() { + if let Err(e) = run_now(Self::up(SchemaManagerConnection::Connection(&DBCONN), None)) { + trace::error!("Migration upgrade failed ({})", e); + }; + } + + fn run_down() { + if let Err(e) = run_now(Self::down(SchemaManagerConnection::Connection(&DBCONN), None)) { + trace::error!("Migration downgrade failed ({})", e); + }; + } +} + +impl MigrationName for M { + fn name(&self) -> &str { + TypeInfo::NameTo(-2).of::() + } +} + +pub type MigrationItem = Box; + +#[macro_export] +macro_rules! install_migrations { + ( $($migration_module:ident),+ $(,)? ) => {{ + use $crate::db::{MigrationItem, MigratorBase, MigratorTrait}; + + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut m = Vec::::new(); + $( + m.push(Box::new(migration::$migration_module::Migration)); + )* + m + } + } + Migrator::run_up(); + }}; +} + +#[macro_export] +macro_rules! uninstall_migrations { + ( $($migration_module:ident),+ $(,)? ) => {{ + use $crate::db::{MigrationItem, MigratorBase, MigratorTrait}; + + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut m = Vec::::new(); + $( + m.push(Box::new(migration::$migration_module::Migration)); + )* + m + } + } + Migrator::run_down(); + }}; +} diff --git a/extensions/pagetop-seaorm/src/db/dbconn.rs b/extensions/pagetop-seaorm/src/db/dbconn.rs new file mode 100644 index 00000000..bd227956 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/dbconn.rs @@ -0,0 +1,69 @@ +use pagetop::trace; + +use crate::config; +use crate::db::{DbConn, DbUri}; + +use std::sync::LazyLock; + +use sea_orm::{ConnectOptions, Database}; + +pub use futures::executor::block_on as run_now; + +pub static DBCONN: LazyLock = LazyLock::new(|| { + trace::info!( + "Connecting to database \"{}\" using a pool of {} connections", + &config::SETTINGS.database.db_name, + &config::SETTINGS.database.max_pool_size + ); + + let db_uri = match config::SETTINGS.database.db_type.as_str() { + "mysql" | "postgres" => { + let mut tmp_uri = DbUri::parse( + format!( + "{}://{}/{}", + &config::SETTINGS.database.db_type, + &config::SETTINGS.database.db_host, + &config::SETTINGS.database.db_name + ) + .as_str(), + ) + .unwrap(); + tmp_uri + .set_username(config::SETTINGS.database.db_user.as_str()) + .unwrap(); + // https://github.com/launchbadge/sqlx/issues/1624 + tmp_uri + .set_password(Some(config::SETTINGS.database.db_pass.as_str())) + .unwrap(); + if config::SETTINGS.database.db_port != 0 { + tmp_uri + .set_port(Some(config::SETTINGS.database.db_port)) + .unwrap(); + } + tmp_uri + } + "sqlite" => DbUri::parse( + format!( + "{}://{}", + &config::SETTINGS.database.db_type, + &config::SETTINGS.database.db_name + ) + .as_str(), + ) + .unwrap(), + _ => { + trace::error!( + "Unrecognized database type \"{}\"", + &config::SETTINGS.database.db_type + ); + DbUri::parse("").unwrap() + } + }; + + run_now(Database::connect::({ + let mut db_opt = ConnectOptions::new(db_uri.to_string()); + db_opt.max_connections(config::SETTINGS.database.max_pool_size); + db_opt + })) + .unwrap_or_else(|_| panic!("Failed to connect to database")) +}); diff --git a/extensions/pagetop-seaorm/src/db/migration.rs b/extensions/pagetop-seaorm/src/db/migration.rs new file mode 100644 index 00000000..29314bf6 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration.rs @@ -0,0 +1,33 @@ +//pub mod cli; +pub mod connection; +pub mod manager; +pub mod migrator; +pub mod prelude; +pub mod schema; +pub mod seaql_migrations; +//pub mod util; + +pub use connection::*; +pub use manager::*; +//pub use migrator::*; + +pub use async_trait; +//pub use sea_orm; +//pub use sea_orm::sea_query; +use sea_orm::DbErr; + +pub trait MigrationName { + fn name(&self) -> &str; +} + +/// The migration definition +#[async_trait::async_trait] +pub trait MigrationTrait: MigrationName + Send + Sync { + /// Define actions to perform when applying the migration + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>; + + /// Define actions to perform when rolling back the migration + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Err(DbErr::Migration("We Don't Do That Here".to_owned())) + } +} diff --git a/extensions/pagetop-seaorm/src/db/migration/connection.rs b/extensions/pagetop-seaorm/src/db/migration/connection.rs new file mode 100644 index 00000000..116185e4 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration/connection.rs @@ -0,0 +1,148 @@ +use futures::Future; +use sea_orm::{ + AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, + ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait, +}; +use std::pin::Pin; + +pub enum SchemaManagerConnection<'c> { + Connection(&'c DatabaseConnection), + Transaction(&'c DatabaseTransaction), +} + +#[async_trait::async_trait] +impl<'c> ConnectionTrait for SchemaManagerConnection<'c> { + fn get_database_backend(&self) -> DbBackend { + match self { + SchemaManagerConnection::Connection(conn) => conn.get_database_backend(), + SchemaManagerConnection::Transaction(trans) => trans.get_database_backend(), + } + } + + async fn execute(&self, stmt: Statement) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => conn.execute(stmt).await, + SchemaManagerConnection::Transaction(trans) => trans.execute(stmt).await, + } + } + + async fn execute_unprepared(&self, sql: &str) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => conn.execute_unprepared(sql).await, + SchemaManagerConnection::Transaction(trans) => trans.execute_unprepared(sql).await, + } + } + + async fn query_one(&self, stmt: Statement) -> Result, DbErr> { + match self { + SchemaManagerConnection::Connection(conn) => conn.query_one(stmt).await, + SchemaManagerConnection::Transaction(trans) => trans.query_one(stmt).await, + } + } + + async fn query_all(&self, stmt: Statement) -> Result, DbErr> { + match self { + SchemaManagerConnection::Connection(conn) => conn.query_all(stmt).await, + SchemaManagerConnection::Transaction(trans) => trans.query_all(stmt).await, + } + } + + fn is_mock_connection(&self) -> bool { + match self { + SchemaManagerConnection::Connection(conn) => conn.is_mock_connection(), + SchemaManagerConnection::Transaction(trans) => trans.is_mock_connection(), + } + } +} + +#[async_trait::async_trait] +impl<'c> TransactionTrait for SchemaManagerConnection<'c> { + async fn begin(&self) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => conn.begin().await, + SchemaManagerConnection::Transaction(trans) => trans.begin().await, + } + } + + async fn begin_with_config( + &self, + isolation_level: Option, + access_mode: Option, + ) -> Result { + match self { + SchemaManagerConnection::Connection(conn) => { + conn.begin_with_config(isolation_level, access_mode).await + } + SchemaManagerConnection::Transaction(trans) => { + trans.begin_with_config(isolation_level, access_mode).await + } + } + } + + async fn transaction(&self, callback: F) -> Result> + where + F: for<'a> FnOnce( + &'a DatabaseTransaction, + ) -> Pin> + Send + 'a>> + + Send, + T: Send, + E: std::error::Error + Send, + { + match self { + SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await, + SchemaManagerConnection::Transaction(trans) => trans.transaction(callback).await, + } + } + + async fn transaction_with_config( + &self, + callback: F, + isolation_level: Option, + access_mode: Option, + ) -> Result> + where + F: for<'a> FnOnce( + &'a DatabaseTransaction, + ) -> Pin> + Send + 'a>> + + Send, + T: Send, + E: std::error::Error + Send, + { + match self { + SchemaManagerConnection::Connection(conn) => { + conn.transaction_with_config(callback, isolation_level, access_mode) + .await + } + SchemaManagerConnection::Transaction(trans) => { + trans + .transaction_with_config(callback, isolation_level, access_mode) + .await + } + } + } +} + +pub trait IntoSchemaManagerConnection<'c>: Send +where + Self: 'c, +{ + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c>; +} + +impl<'c> IntoSchemaManagerConnection<'c> for SchemaManagerConnection<'c> { + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { + self + } +} + +impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseConnection { + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { + SchemaManagerConnection::Connection(self) + } +} + +impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseTransaction { + fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> { + SchemaManagerConnection::Transaction(self) + } +} diff --git a/extensions/pagetop-seaorm/src/db/migration/manager.rs b/extensions/pagetop-seaorm/src/db/migration/manager.rs new file mode 100644 index 00000000..d1cc3b6a --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration/manager.rs @@ -0,0 +1,167 @@ +use super::{IntoSchemaManagerConnection, SchemaManagerConnection}; +use sea_orm::sea_query::{ + extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, + ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, + TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, + TableTruncateStatement, +}; +use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder}; +use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; + +/// Helper struct for writing migration scripts in migration file +pub struct SchemaManager<'c> { + conn: SchemaManagerConnection<'c>, +} + +impl<'c> SchemaManager<'c> { + pub fn new(conn: T) -> Self + where + T: IntoSchemaManagerConnection<'c>, + { + Self { + conn: conn.into_schema_manager_connection(), + } + } + + pub async fn exec_stmt(&self, stmt: S) -> Result<(), DbErr> + where + S: StatementBuilder, + { + let builder = self.conn.get_database_backend(); + self.conn.execute(builder.build(&stmt)).await.map(|_| ()) + } + + pub fn get_database_backend(&self) -> DbBackend { + self.conn.get_database_backend() + } + + pub fn get_connection(&self) -> &SchemaManagerConnection<'c> { + &self.conn + } +} + +/// Schema Creation +impl<'c> SchemaManager<'c> { + pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } +} + +/// Schema Mutation +impl<'c> SchemaManager<'c> { + pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } +} + +/// Schema Inspection. +impl<'c> SchemaManager<'c> { + pub async fn has_table(&self, table: T) -> Result + where + T: AsRef, + { + has_table(&self.conn, table).await + } + + pub async fn has_column(&self, table: T, column: C) -> Result + where + T: AsRef, + C: AsRef, + { + let stmt = match self.conn.get_database_backend() { + DbBackend::MySql => MySql.has_column(table, column), + DbBackend::Postgres => Postgres.has_column(table, column), + DbBackend::Sqlite => Sqlite.has_column(table, column), + }; + + let builder = self.conn.get_database_backend(); + let res = self + .conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?; + + res.try_get("", "has_column") + } + + pub async fn has_index(&self, table: T, index: I) -> Result + where + T: AsRef, + I: AsRef, + { + let stmt = match self.conn.get_database_backend() { + DbBackend::MySql => MySql.has_index(table, index), + DbBackend::Postgres => Postgres.has_index(table, index), + DbBackend::Sqlite => Sqlite.has_index(table, index), + }; + + let builder = self.conn.get_database_backend(); + let res = self + .conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?; + + res.try_get("", "has_index") + } +} + +pub(crate) async fn has_table(conn: &C, table: T) -> Result +where + C: ConnectionTrait, + T: AsRef, +{ + let stmt = match conn.get_database_backend() { + DbBackend::MySql => MySql.has_table(table), + DbBackend::Postgres => Postgres.has_table(table), + DbBackend::Sqlite => Sqlite.has_table(table), + }; + + let builder = conn.get_database_backend(); + let res = conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?; + + res.try_get("", "has_table") +} diff --git a/extensions/pagetop-seaorm/src/db/migration/migrator.rs b/extensions/pagetop-seaorm/src/db/migration/migrator.rs new file mode 100644 index 00000000..06611412 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration/migrator.rs @@ -0,0 +1,593 @@ +use futures::Future; +use std::collections::HashSet; +use std::fmt::Display; +use std::pin::Pin; +use std::time::SystemTime; + +use pagetop::trace::info; + +use sea_orm::sea_query::{ + self, extension::postgres::Type, Alias, Expr, ForeignKey, IntoIden, JoinType, Order, Query, + SelectStatement, SimpleExpr, Table, +}; +use sea_orm::{ + ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden, + DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement, + TransactionTrait, +}; +use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; + +use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Status of migration +pub enum MigrationStatus { + /// Not yet applied + Pending, + /// Applied + Applied, +} + +impl Display for MigrationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let status = match self { + MigrationStatus::Pending => "Pending", + MigrationStatus::Applied => "Applied", + }; + write!(f, "{status}") + } +} + +pub struct Migration { + migration: Box, + status: MigrationStatus, +} + +impl Migration { + /// Get migration name from MigrationName trait implementation + pub fn name(&self) -> &str { + self.migration.name() + } + + /// Get migration status + pub fn status(&self) -> MigrationStatus { + self.status + } +} + +/// Performing migrations on a database +#[async_trait::async_trait] +pub trait MigratorTrait: Send { + /// Vector of migrations in time sequence + fn migrations() -> Vec>; + + /// Name of the migration table, it is `seaql_migrations` by default + fn migration_table_name() -> DynIden { + seaql_migrations::Entity.into_iden() + } + + /// Get list of migrations wrapped in `Migration` struct + fn get_migration_files() -> Vec { + Self::migrations() + .into_iter() + .map(|migration| Migration { + migration, + status: MigrationStatus::Pending, + }) + .collect() + } + + /// Get list of applied migrations from database + async fn get_migration_models(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + let stmt = Query::select() + .table_name(Self::migration_table_name()) + .columns(seaql_migrations::Column::iter().map(IntoIden::into_iden)) + .order_by(seaql_migrations::Column::Version, Order::Asc) + .to_owned(); + let builder = db.get_database_backend(); + seaql_migrations::Model::find_by_statement(builder.build(&stmt)) + .all(db) + .await + } + + /// Get list of migrations with status + async fn get_migration_with_status(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + let mut migration_files = Self::get_migration_files(); + let migration_models = Self::get_migration_models(db).await?; + + let migration_in_db: HashSet = migration_models + .into_iter() + .map(|model| model.version) + .collect(); + let migration_in_fs: HashSet = migration_files + .iter() + .map(|file| file.migration.name().to_string()) + .collect(); + + let pending_migrations = &migration_in_fs - &migration_in_db; + for migration_file in migration_files.iter_mut() { + if !pending_migrations.contains(migration_file.migration.name()) { + migration_file.status = MigrationStatus::Applied; + } + } + /* + let missing_migrations_in_fs = &migration_in_db - &migration_in_fs; + let errors: Vec = missing_migrations_in_fs + .iter() + .map(|missing_migration| { + format!("Migration file of version '{missing_migration}' is missing, this migration has been applied but its file is missing") + }).collect(); + + if !errors.is_empty() { + Err(DbErr::Custom(errors.join("\n"))) + } else { */ + Ok(migration_files) + /* } */ + } + + /// Get list of pending migrations + async fn get_pending_migrations(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + Ok(Self::get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Pending) + .collect()) + } + + /// Get list of applied migrations + async fn get_applied_migrations(db: &C) -> Result, DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + Ok(Self::get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Applied) + .collect()) + } + + /// Create migration table `seaql_migrations` in the database + async fn install(db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + let builder = db.get_database_backend(); + let table_name = Self::migration_table_name(); + let schema = Schema::new(builder); + let mut stmt = schema + .create_table_from_entity(seaql_migrations::Entity) + .table_name(table_name); + stmt.if_not_exists(); + db.execute(builder.build(&stmt)).await.map(|_| ()) + } + + /// Check the status of all migrations + async fn status(db: &C) -> Result<(), DbErr> + where + C: ConnectionTrait, + { + Self::install(db).await?; + + info!("Checking migration status"); + + for Migration { migration, status } in Self::get_migration_with_status(db).await? { + info!("Migration '{}'... {}", migration.name(), status); + } + + Ok(()) + } + + /// Drop all tables from the database, then reapply all migrations + async fn fresh<'c, C>(db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_fresh::(manager).await }) + }) + .await + } + + /// Rollback all applied migrations, then reapply all migrations + async fn refresh<'c, C>(db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { + exec_down::(manager, None).await?; + exec_up::(manager, None).await + }) + }) + .await + } + + /// Rollback all applied migrations + async fn reset<'c, C>(db: C) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_down::(manager, None).await }) + }) + .await + } + + /// Apply pending migrations + async fn up<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_up::(manager, steps).await }) + }) + .await + } + + /// Rollback applied migrations + async fn down<'c, C>(db: C, steps: Option) -> Result<(), DbErr> + where + C: IntoSchemaManagerConnection<'c>, + { + exec_with_connection::<'_, _, _>(db, move |manager| { + Box::pin(async move { exec_down::(manager, steps).await }) + }) + .await + } +} + +async fn exec_with_connection<'c, C, F>(db: C, f: F) -> Result<(), DbErr> +where + C: IntoSchemaManagerConnection<'c>, + F: for<'b> Fn( + &'b SchemaManager<'_>, + ) -> Pin> + Send + 'b>>, +{ + let db = db.into_schema_manager_connection(); + + match db.get_database_backend() { + DbBackend::Postgres => { + let transaction = db.begin().await?; + let manager = SchemaManager::new(&transaction); + f(&manager).await?; + transaction.commit().await + } + DbBackend::MySql | DbBackend::Sqlite => { + let manager = SchemaManager::new(db); + f(&manager).await + } + } +} + +async fn exec_fresh(manager: &SchemaManager<'_>) -> Result<(), DbErr> +where + M: MigratorTrait + ?Sized, +{ + let db = manager.get_connection(); + + M::install(db).await?; + let db_backend = db.get_database_backend(); + + // Temporarily disable the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Disabling foreign key check"); + db.execute(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = OFF".to_owned(), + )) + .await?; + info!("Foreign key check disabled"); + } + + // Drop all foreign keys + if db_backend == DbBackend::MySql { + info!("Dropping all foreign keys"); + let stmt = query_mysql_foreign_keys(db); + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows.into_iter() { + let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?; + let table_name: String = row.try_get("", "TABLE_NAME")?; + info!( + "Dropping foreign key '{}' from table '{}'", + constraint_name, table_name + ); + let mut stmt = ForeignKey::drop(); + stmt.table(Alias::new(table_name.as_str())) + .name(constraint_name.as_str()); + db.execute(db_backend.build(&stmt)).await?; + info!("Foreign key '{}' has been dropped", constraint_name); + } + info!("All foreign keys dropped"); + } + + // Drop all tables + let stmt = query_tables(db).await; + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows.into_iter() { + let table_name: String = row.try_get("", "table_name")?; + info!("Dropping table '{}'", table_name); + let mut stmt = Table::drop(); + stmt.table(Alias::new(table_name.as_str())) + .if_exists() + .cascade(); + db.execute(db_backend.build(&stmt)).await?; + info!("Table '{}' has been dropped", table_name); + } + + // Drop all types + if db_backend == DbBackend::Postgres { + info!("Dropping all types"); + let stmt = query_pg_types(db); + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows { + let type_name: String = row.try_get("", "typname")?; + info!("Dropping type '{}'", type_name); + let mut stmt = Type::drop(); + stmt.name(Alias::new(&type_name)); + db.execute(db_backend.build(&stmt)).await?; + info!("Type '{}' has been dropped", type_name); + } + } + + // Restore the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Restoring foreign key check"); + db.execute(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = ON".to_owned(), + )) + .await?; + info!("Foreign key check restored"); + } + + // Reapply all migrations + exec_up::(manager, None).await +} + +async fn exec_up(manager: &SchemaManager<'_>, mut steps: Option) -> Result<(), DbErr> +where + M: MigratorTrait + ?Sized, +{ + let db = manager.get_connection(); + + M::install(db).await?; + /* + if let Some(steps) = steps { + info!("Applying {} pending migrations", steps); + } else { + info!("Applying all pending migrations"); + } + */ + let migrations = M::get_pending_migrations(db).await?.into_iter(); + /* + if migrations.len() == 0 { + info!("No pending migrations"); + } + */ + for Migration { migration, .. } in migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + info!("Applying migration '{}'", migration.name()); + migration.up(manager).await?; + info!("Migration '{}' has been applied", migration.name()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!"); + seaql_migrations::Entity::insert(seaql_migrations::ActiveModel { + version: ActiveValue::Set(migration.name().to_owned()), + applied_at: ActiveValue::Set(now.as_secs() as i64), + }) + .table_name(M::migration_table_name()) + .exec(db) + .await?; + } + + Ok(()) +} + +async fn exec_down(manager: &SchemaManager<'_>, mut steps: Option) -> Result<(), DbErr> +where + M: MigratorTrait + ?Sized, +{ + let db = manager.get_connection(); + + M::install(db).await?; + + if let Some(steps) = steps { + info!("Rolling back {} applied migrations", steps); + } else { + info!("Rolling back all applied migrations"); + } + + let migrations = M::get_applied_migrations(db).await?.into_iter().rev(); + if migrations.len() == 0 { + info!("No applied migrations"); + } + for Migration { migration, .. } in migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + info!("Rolling back migration '{}'", migration.name()); + migration.down(manager).await?; + info!("Migration '{}' has been rollbacked", migration.name()); + seaql_migrations::Entity::delete_many() + .filter(Expr::col(seaql_migrations::Column::Version).eq(migration.name())) + .table_name(M::migration_table_name()) + .exec(db) + .await?; + } + + Ok(()) +} + +async fn query_tables(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + match db.get_database_backend() { + DbBackend::MySql => MySql.query_tables(), + DbBackend::Postgres => Postgres.query_tables(), + DbBackend::Sqlite => Sqlite.query_tables(), + } +} + +fn get_current_schema(db: &C) -> SimpleExpr +where + C: ConnectionTrait, +{ + match db.get_database_backend() { + DbBackend::MySql => MySql::get_current_schema(), + DbBackend::Postgres => Postgres::get_current_schema(), + DbBackend::Sqlite => unimplemented!(), + } +} + +#[derive(DeriveIden)] +enum InformationSchema { + #[sea_orm(iden = "information_schema")] + Schema, + #[sea_orm(iden = "TABLE_NAME")] + TableName, + #[sea_orm(iden = "CONSTRAINT_NAME")] + ConstraintName, + TableConstraints, + TableSchema, + ConstraintType, +} + +fn query_mysql_foreign_keys(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + let mut stmt = Query::select(); + stmt.columns([ + InformationSchema::TableName, + InformationSchema::ConstraintName, + ]) + .from(( + InformationSchema::Schema, + InformationSchema::TableConstraints, + )) + .cond_where( + Condition::all() + .add(Expr::expr(get_current_schema(db)).equals(( + InformationSchema::TableConstraints, + InformationSchema::TableSchema, + ))) + .add( + Expr::col(( + InformationSchema::TableConstraints, + InformationSchema::ConstraintType, + )) + .eq("FOREIGN KEY"), + ), + ); + stmt +} + +#[derive(DeriveIden)] +enum PgType { + Table, + Typname, + Typnamespace, + Typelem, +} + +#[derive(DeriveIden)] +enum PgNamespace { + Table, + Oid, + Nspname, +} + +fn query_pg_types(db: &C) -> SelectStatement +where + C: ConnectionTrait, +{ + let mut stmt = Query::select(); + stmt.column(PgType::Typname) + .from(PgType::Table) + .join( + JoinType::LeftJoin, + PgNamespace::Table, + Expr::col((PgNamespace::Table, PgNamespace::Oid)) + .equals((PgType::Table, PgType::Typnamespace)), + ) + .cond_where( + Condition::all() + .add( + Expr::expr(get_current_schema(db)) + .equals((PgNamespace::Table, PgNamespace::Nspname)), + ) + .add(Expr::col((PgType::Table, PgType::Typelem)).eq(0)), + ); + stmt +} + +trait QueryTable { + type Statement; + + fn table_name(self, table_name: DynIden) -> Self::Statement; +} + +impl QueryTable for SelectStatement { + type Statement = SelectStatement; + + fn table_name(mut self, table_name: DynIden) -> SelectStatement { + self.from(table_name); + self + } +} + +impl QueryTable for sea_query::TableCreateStatement { + type Statement = sea_query::TableCreateStatement; + + fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement { + self.table(table_name); + self + } +} + +impl QueryTable for sea_orm::Insert +where + A: ActiveModelTrait, +{ + type Statement = sea_orm::Insert; + + fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert { + sea_orm::QueryTrait::query(&mut self).into_table(table_name); + self + } +} + +impl QueryTable for sea_orm::DeleteMany +where + E: EntityTrait, +{ + type Statement = sea_orm::DeleteMany; + + fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany { + sea_orm::QueryTrait::query(&mut self).from_table(table_name); + self + } +} diff --git a/extensions/pagetop-seaorm/src/db/migration/prelude.rs b/extensions/pagetop-seaorm/src/db/migration/prelude.rs new file mode 100644 index 00000000..5556a094 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration/prelude.rs @@ -0,0 +1,13 @@ +//pub use super::cli; + +pub use super::connection::IntoSchemaManagerConnection; +pub use super::connection::SchemaManagerConnection; +pub use super::manager::SchemaManager; +pub use super::migrator::MigratorTrait; +pub use super::{MigrationName, MigrationTrait}; +pub use async_trait; +pub use sea_orm; +pub use sea_orm::sea_query; +pub use sea_orm::sea_query::*; +pub use sea_orm::DeriveIden; +pub use sea_orm::DeriveMigrationName; diff --git a/extensions/pagetop-seaorm/src/db/migration/schema.rs b/extensions/pagetop-seaorm/src/db/migration/schema.rs new file mode 100644 index 00000000..f250eae0 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration/schema.rs @@ -0,0 +1,608 @@ +//! Adapted from +//! +//! # Database Table Schema Helpers +//! +//! This module defines functions and helpers for creating database table +//! schemas using the `sea-orm` and `sea-query` libraries. +//! +//! # Example +//! +//! The following example shows how the user migration file should be and using +//! the schema helpers to create the Db fields. +//! +//! ```rust +//! use sea_orm_migration::{prelude::*, schema::*}; +//! +//! #[derive(DeriveMigrationName)] +//! pub struct Migration; +//! +//! #[async_trait::async_trait] +//! impl MigrationTrait for Migration { +//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! let table = table_auto(Users::Table) +//! .col(pk_auto(Users::Id)) +//! .col(uuid(Users::Pid)) +//! .col(string_uniq(Users::Email)) +//! .col(string(Users::Password)) +//! .col(string(Users::Name)) +//! .col(string_null(Users::ResetToken)) +//! .col(timestamp_null(Users::ResetSentAt)) +//! .to_owned(); +//! manager.create_table(table).await?; +//! Ok(()) +//! } +//! +//! async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! manager +//! .drop_table(Table::drop().table(Users::Table).to_owned()) +//! .await +//! } +//! } +//! +//! #[derive(Iden)] +//! pub enum Users { +//! Table, +//! Id, +//! Pid, +//! Email, +//! Name, +//! Password, +//! ResetToken, +//! ResetSentAt, +//! } +//! ``` + +use crate::prelude::Iden; +use sea_orm::sea_query::{ + self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement, +}; + +#[derive(Iden)] +enum GeneralIds { + CreatedAt, + UpdatedAt, +} + +/// Wrapping table schema creation. +pub fn table_auto(name: T) -> TableCreateStatement { + timestamps(Table::create().table(name).if_not_exists().take()) +} + +/// Create a primary key column with auto-increment feature. +pub fn pk_auto(name: T) -> ColumnDef { + integer(name).auto_increment().primary_key().take() +} + +pub fn char_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).char_len(length).not_null().take() +} + +pub fn char_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).char_len(length).null().take() +} + +pub fn char_len_uniq(col: T, length: u32) -> ColumnDef { + char_len(col, length).unique_key().take() +} + +pub fn char(col: T) -> ColumnDef { + ColumnDef::new(col).char().not_null().take() +} + +pub fn char_null(col: T) -> ColumnDef { + ColumnDef::new(col).char().null().take() +} + +pub fn char_uniq(col: T) -> ColumnDef { + char(col).unique_key().take() +} + +pub fn string_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).string_len(length).not_null().take() +} + +pub fn string_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).string_len(length).null().take() +} + +pub fn string_len_uniq(col: T, length: u32) -> ColumnDef { + string_len(col, length).unique_key().take() +} + +pub fn string(col: T) -> ColumnDef { + ColumnDef::new(col).string().not_null().take() +} + +pub fn string_null(col: T) -> ColumnDef { + ColumnDef::new(col).string().null().take() +} + +pub fn string_uniq(col: T) -> ColumnDef { + string(col).unique_key().take() +} + +pub fn text(col: T) -> ColumnDef { + ColumnDef::new(col).text().not_null().take() +} + +pub fn text_null(col: T) -> ColumnDef { + ColumnDef::new(col).text().null().take() +} + +pub fn text_uniq(col: T) -> ColumnDef { + text(col).unique_key().take() +} + +pub fn tiny_integer(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_integer().not_null().take() +} + +pub fn tiny_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_integer().null().take() +} + +pub fn tiny_integer_uniq(col: T) -> ColumnDef { + tiny_integer(col).unique_key().take() +} + +pub fn small_integer(col: T) -> ColumnDef { + ColumnDef::new(col).small_integer().not_null().take() +} + +pub fn small_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).small_integer().null().take() +} + +pub fn small_integer_uniq(col: T) -> ColumnDef { + small_integer(col).unique_key().take() +} + +pub fn integer(col: T) -> ColumnDef { + ColumnDef::new(col).integer().not_null().take() +} + +pub fn integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).integer().null().take() +} + +pub fn integer_uniq(col: T) -> ColumnDef { + integer(col).unique_key().take() +} + +pub fn big_integer(col: T) -> ColumnDef { + ColumnDef::new(col).big_integer().not_null().take() +} + +pub fn big_integer_null(col: T) -> ColumnDef { + ColumnDef::new(col).big_integer().null().take() +} + +pub fn big_integer_uniq(col: T) -> ColumnDef { + big_integer(col).unique_key().take() +} + +pub fn tiny_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_unsigned().not_null().take() +} + +pub fn tiny_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).tiny_unsigned().null().take() +} + +pub fn tiny_unsigned_uniq(col: T) -> ColumnDef { + tiny_unsigned(col).unique_key().take() +} + +pub fn small_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).small_unsigned().not_null().take() +} + +pub fn small_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).small_unsigned().null().take() +} + +pub fn small_unsigned_uniq(col: T) -> ColumnDef { + small_unsigned(col).unique_key().take() +} + +pub fn unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).unsigned().not_null().take() +} + +pub fn unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).unsigned().null().take() +} + +pub fn unsigned_uniq(col: T) -> ColumnDef { + unsigned(col).unique_key().take() +} + +pub fn big_unsigned(col: T) -> ColumnDef { + ColumnDef::new(col).big_unsigned().not_null().take() +} + +pub fn big_unsigned_null(col: T) -> ColumnDef { + ColumnDef::new(col).big_unsigned().null().take() +} + +pub fn big_unsigned_uniq(col: T) -> ColumnDef { + big_unsigned(col).unique_key().take() +} + +pub fn float(col: T) -> ColumnDef { + ColumnDef::new(col).float().not_null().take() +} + +pub fn float_null(col: T) -> ColumnDef { + ColumnDef::new(col).float().null().take() +} + +pub fn float_uniq(col: T) -> ColumnDef { + float(col).unique_key().take() +} + +pub fn double(col: T) -> ColumnDef { + ColumnDef::new(col).double().not_null().take() +} + +pub fn double_null(col: T) -> ColumnDef { + ColumnDef::new(col).double().null().take() +} + +pub fn double_uniq(col: T) -> ColumnDef { + double(col).unique_key().take() +} + +pub fn decimal_len(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .decimal_len(precision, scale) + .not_null() + .take() +} + +pub fn decimal_len_null(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .decimal_len(precision, scale) + .null() + .take() +} + +pub fn decimal_len_uniq(col: T, precision: u32, scale: u32) -> ColumnDef { + decimal_len(col, precision, scale).unique_key().take() +} + +pub fn decimal(col: T) -> ColumnDef { + ColumnDef::new(col).decimal().not_null().take() +} + +pub fn decimal_null(col: T) -> ColumnDef { + ColumnDef::new(col).decimal().null().take() +} + +pub fn decimal_uniq(col: T) -> ColumnDef { + decimal(col).unique_key().take() +} + +pub fn date_time(col: T) -> ColumnDef { + ColumnDef::new(col).date_time().not_null().take() +} + +pub fn date_time_null(col: T) -> ColumnDef { + ColumnDef::new(col).date_time().null().take() +} + +pub fn date_time_uniq(col: T) -> ColumnDef { + date_time(col).unique_key().take() +} + +pub fn interval( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + ColumnDef::new(col) + .interval(fields, precision) + .not_null() + .take() +} + +pub fn interval_null( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + ColumnDef::new(col) + .interval(fields, precision) + .null() + .take() +} + +pub fn interval_uniq( + col: T, + fields: Option, + precision: Option, +) -> ColumnDef { + interval(col, fields, precision).unique_key().take() +} + +pub fn timestamp(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp().not_null().take() +} + +pub fn timestamp_null(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp().null().take() +} + +pub fn timestamp_uniq(col: T) -> ColumnDef { + timestamp(col).unique_key().take() +} + +pub fn timestamp_with_time_zone(col: T) -> ColumnDef { + ColumnDef::new(col) + .timestamp_with_time_zone() + .not_null() + .take() +} + +pub fn timestamp_with_time_zone_null(col: T) -> ColumnDef { + ColumnDef::new(col).timestamp_with_time_zone().null().take() +} + +pub fn timestamp_with_time_zone_uniq(col: T) -> ColumnDef { + timestamp_with_time_zone(col).unique_key().take() +} + +pub fn time(col: T) -> ColumnDef { + ColumnDef::new(col).time().not_null().take() +} + +pub fn time_null(col: T) -> ColumnDef { + ColumnDef::new(col).time().null().take() +} + +pub fn time_uniq(col: T) -> ColumnDef { + time(col).unique_key().take() +} + +pub fn date(col: T) -> ColumnDef { + ColumnDef::new(col).date().not_null().take() +} + +pub fn date_null(col: T) -> ColumnDef { + ColumnDef::new(col).date().null().take() +} + +pub fn date_uniq(col: T) -> ColumnDef { + date(col).unique_key().take() +} + +pub fn year(col: T) -> ColumnDef { + ColumnDef::new(col).year().not_null().take() +} + +pub fn year_null(col: T) -> ColumnDef { + ColumnDef::new(col).year().null().take() +} + +pub fn year_uniq(col: T) -> ColumnDef { + year(col).unique_key().take() +} + +pub fn binary_len(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).binary_len(length).not_null().take() +} + +pub fn binary_len_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).binary_len(length).null().take() +} + +pub fn binary_len_uniq(col: T, length: u32) -> ColumnDef { + binary_len(col, length).unique_key().take() +} + +pub fn binary(col: T) -> ColumnDef { + ColumnDef::new(col).binary().not_null().take() +} + +pub fn binary_null(col: T) -> ColumnDef { + ColumnDef::new(col).binary().null().take() +} + +pub fn binary_uniq(col: T) -> ColumnDef { + binary(col).unique_key().take() +} + +pub fn var_binary(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).var_binary(length).not_null().take() +} + +pub fn var_binary_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).var_binary(length).null().take() +} + +pub fn var_binary_uniq(col: T, length: u32) -> ColumnDef { + var_binary(col, length).unique_key().take() +} + +pub fn bit(col: T, length: Option) -> ColumnDef { + ColumnDef::new(col).bit(length).not_null().take() +} + +pub fn bit_null(col: T, length: Option) -> ColumnDef { + ColumnDef::new(col).bit(length).null().take() +} + +pub fn bit_uniq(col: T, length: Option) -> ColumnDef { + bit(col, length).unique_key().take() +} + +pub fn varbit(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).varbit(length).not_null().take() +} + +pub fn varbit_null(col: T, length: u32) -> ColumnDef { + ColumnDef::new(col).varbit(length).null().take() +} + +pub fn varbit_uniq(col: T, length: u32) -> ColumnDef { + varbit(col, length).unique_key().take() +} + +pub fn blob(col: T) -> ColumnDef { + ColumnDef::new(col).blob().not_null().take() +} + +pub fn blob_null(col: T) -> ColumnDef { + ColumnDef::new(col).blob().null().take() +} + +pub fn blob_uniq(col: T) -> ColumnDef { + blob(col).unique_key().take() +} + +pub fn boolean(col: T) -> ColumnDef { + ColumnDef::new(col).boolean().not_null().take() +} + +pub fn boolean_null(col: T) -> ColumnDef { + ColumnDef::new(col).boolean().null().take() +} + +pub fn boolean_uniq(col: T) -> ColumnDef { + boolean(col).unique_key().take() +} + +pub fn money_len(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .money_len(precision, scale) + .not_null() + .take() +} + +pub fn money_len_null(col: T, precision: u32, scale: u32) -> ColumnDef { + ColumnDef::new(col) + .money_len(precision, scale) + .null() + .take() +} + +pub fn money_len_uniq(col: T, precision: u32, scale: u32) -> ColumnDef { + money_len(col, precision, scale).unique_key().take() +} + +pub fn money(col: T) -> ColumnDef { + ColumnDef::new(col).money().not_null().take() +} + +pub fn money_null(col: T) -> ColumnDef { + ColumnDef::new(col).money().null().take() +} + +pub fn money_uniq(col: T) -> ColumnDef { + money(col).unique_key().take() +} + +pub fn json(col: T) -> ColumnDef { + ColumnDef::new(col).json().not_null().take() +} + +pub fn json_null(col: T) -> ColumnDef { + ColumnDef::new(col).json().null().take() +} + +pub fn json_uniq(col: T) -> ColumnDef { + json(col).unique_key().take() +} + +pub fn json_binary(col: T) -> ColumnDef { + ColumnDef::new(col).json_binary().not_null().take() +} + +pub fn json_binary_null(col: T) -> ColumnDef { + ColumnDef::new(col).json_binary().null().take() +} + +pub fn json_binary_uniq(col: T) -> ColumnDef { + json_binary(col).unique_key().take() +} + +pub fn uuid(col: T) -> ColumnDef { + ColumnDef::new(col).uuid().not_null().take() +} + +pub fn uuid_null(col: T) -> ColumnDef { + ColumnDef::new(col).uuid().null().take() +} + +pub fn uuid_uniq(col: T) -> ColumnDef { + uuid(col).unique_key().take() +} + +pub fn custom(col: T, name: T) -> ColumnDef { + ColumnDef::new(col).custom(name).not_null().take() +} + +pub fn custom_null(col: T, name: T) -> ColumnDef { + ColumnDef::new(col).custom(name).null().take() +} + +pub fn enumeration(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + ColumnDef::new(col) + .enumeration(name, variants) + .not_null() + .take() +} + +pub fn enumeration_null(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + ColumnDef::new(col) + .enumeration(name, variants) + .null() + .take() +} + +pub fn enumeration_uniq(col: T, name: N, variants: V) -> ColumnDef +where + T: IntoIden, + N: IntoIden, + S: IntoIden, + V: IntoIterator, +{ + enumeration(col, name, variants).unique_key().take() +} + +pub fn array(col: T, elem_type: ColumnType) -> ColumnDef { + ColumnDef::new(col).array(elem_type).not_null().take() +} + +pub fn array_null(col: T, elem_type: ColumnType) -> ColumnDef { + ColumnDef::new(col).array(elem_type).null().take() +} + +pub fn array_uniq(col: T, elem_type: ColumnType) -> ColumnDef { + array(col, elem_type).unique_key().take() +} + +/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. +pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { + let mut t = t; + t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) + .col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp())) + .take() +} + +/// Create an Alias. +pub fn name>(name: T) -> Alias { + Alias::new(name) +} diff --git a/extensions/pagetop-seaorm/src/db/migration/seaql_migrations.rs b/extensions/pagetop-seaorm/src/db/migration/seaql_migrations.rs new file mode 100644 index 00000000..51da9300 --- /dev/null +++ b/extensions/pagetop-seaorm/src/db/migration/seaql_migrations.rs @@ -0,0 +1,15 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +// One should override the name of migration table via `MigratorTrait::migration_table_name` method +#[sea_orm(table_name = "seaql_migrations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub version: String, + pub applied_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs new file mode 100644 index 00000000..b9928727 --- /dev/null +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -0,0 +1,29 @@ +use pagetop::prelude::*; + +pub mod config; +pub mod db; + +/// Preludio de la extensión. +pub mod prelude { + pub use crate::db::*; + pub use crate::install_migrations; +} + +include_locales!(LOCALES_SEAORM); + +/// Extensión que integra SeaORM como framework de base de datos para aplicaciones PageTop. +pub struct SeaORM; + +impl Extension for SeaORM { + fn name(&self) -> L10n { + L10n::t("extension_name", &LOCALES_SEAORM) + } + + fn description(&self) -> L10n { + L10n::t("extension_description", &LOCALES_SEAORM) + } + + fn initialize(&self) { + std::sync::LazyLock::force(&db::DBCONN); + } +} diff --git a/extensions/pagetop-seaorm/src/locale/en-US/extension.ftl b/extensions/pagetop-seaorm/src/locale/en-US/extension.ftl new file mode 100644 index 00000000..f80d7aa4 --- /dev/null +++ b/extensions/pagetop-seaorm/src/locale/en-US/extension.ftl @@ -0,0 +1,2 @@ +extension_name = SeaORM support +extension_description = Provides SeaORM-based access to relational databases. diff --git a/extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl b/extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl new file mode 100644 index 00000000..e48e4653 --- /dev/null +++ b/extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl @@ -0,0 +1,2 @@ +extension_name = Soporte a SeaORM +extension_description = Proporciona acceso basado en SeaORM a bases de datos relacionales. diff --git a/tools/changelog.sh b/tools/changelog.sh index 36fd8f9b..fc2a8a0c 100755 --- a/tools/changelog.sh +++ b/tools/changelog.sh @@ -65,6 +65,7 @@ case "$CRATE" in # Extensions --exclude-path "extensions/pagetop-aliner/**/*" --exclude-path "extensions/pagetop-bootsier/**/*" + --exclude-path "extensions/pagetop-seaorm/**/*" ) ;; pagetop-aliner) @@ -75,6 +76,10 @@ case "$CRATE" in CHANGELOG_FILE="extensions/pagetop-bootsier/CHANGELOG.md" PATH_FLAGS=(--include-path "extensions/pagetop-bootsier/**/*") ;; + pagetop-seaorm) + CHANGELOG_FILE="extensions/pagetop-seaorm/CHANGELOG.md" + PATH_FLAGS=(--include-path "extensions/pagetop-seaorm/**/*") + ;; *) echo "Error: unsupported crate '$CRATE'" >&2 exit 1 @@ -120,7 +125,9 @@ read -r -p "Do you want to proceed with the release of $CRATE? [y/N] " REPLY echo "" if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then echo "Aborting release process." >&2 - git restore --worktree -- . + if [[ -n "${PAGETOP_RESTORE_TREE:-}" ]]; then + git restore --worktree -- . + fi exit 1 fi diff --git a/tools/release.sh b/tools/release.sh index 92257b32..5234c849 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -43,6 +43,8 @@ cd "$(dirname "$0")/.." || exit 1 # ------------------------------------------------------------------------------ # DRY-RUN (por defecto) o ejecución real con --execute # ------------------------------------------------------------------------------ +export PAGETOP_RESTORE_TREE=1 + if [[ "$EXECUTE" != "--execute" ]]; then echo "Running dry-run (default mode). Add --execute to publish" cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL" From bd8a34341d225e903a1c699401f21d2f4e4374fe Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 9 May 2026 13:33:20 +0200 Subject: [PATCH 06/29] =?UTF-8?q?=F0=9F=9A=A7=20Retoques=20en=20documentac?= =?UTF-8?q?i=C3=B3n=20y=20c=C3=B3digo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-aliner/src/lib.rs | 5 +- extensions/pagetop-seaorm/src/config.rs | 22 ++--- extensions/pagetop-seaorm/src/lib.rs | 113 +++++++++++++++++++++++- 3 files changed, 119 insertions(+), 21 deletions(-) diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 7a29eebc..e88a9142 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -85,9 +85,10 @@ use pagetop::prelude::*; include_locales!(LOCALES_ALINER); -/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML. +/// Implementa el tema. /// -/// Define un tema mínimo útil para: +/// Define un tema mínimo que muestra esquemáticamente la composición de las páginas HTML; útil +/// para: /// /// - Comprobar el funcionamiento de temas, plantillas y regiones. /// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos. diff --git a/extensions/pagetop-seaorm/src/config.rs b/extensions/pagetop-seaorm/src/config.rs index 5c706659..bec565b3 100644 --- a/extensions/pagetop-seaorm/src/config.rs +++ b/extensions/pagetop-seaorm/src/config.rs @@ -16,8 +16,7 @@ //! Uso: //! //! ```rust -//! use pagetop_seaorm::config; -//! +//! # use pagetop_seaorm::config; //! assert_eq!(config::SETTINGS.database.db_host, "localhost"); //! ``` //! @@ -30,12 +29,12 @@ use serde::Deserialize; include_config!(SETTINGS: Settings => [ // [database] - "database.db_type" => "", - "database.db_name" => "", - "database.db_user" => "", - "database.db_pass" => "", - "database.db_host" => "localhost", - "database.db_port" => 0, + "database.db_type" => "", + "database.db_name" => "", + "database.db_user" => "", + "database.db_pass" => "", + "database.db_host" => "localhost", + "database.db_port" => 0, "database.max_pool_size" => 5, ]); @@ -49,24 +48,17 @@ pub struct Settings { /// Sección `[database]` de la configuración. Forma parte de [`Settings`]. pub struct Database { /// Tipo de base de datos: *"mysql"*, *"postgres"* ó *"sqlite"*. - /// Por defecto: *""*. pub db_type: String, /// Nombre (para mysql/postgres) o referencia (para sqlite) de la base de datos. - /// Por defecto: *""*. pub db_name: String, /// Usuario de conexión a la base de datos (para mysql/postgres). - /// Por defecto: *""*. pub db_user: String, /// Contraseña para la conexión a la base de datos (para mysql/postgres). - /// Por defecto: *""*. pub db_pass: String, /// Servidor de conexión a la base de datos (para mysql/postgres). - /// Por defecto: *"localhost"*. pub db_host: String, /// Puerto de conexión a la base de datos, normalmente 3306 (para mysql) ó 5432 (para postgres). - /// Por defecto: *0*. pub db_port: u16, /// Número máximo de conexiones habilitadas. - /// Por defecto: *5*. pub max_pool_size: u32, } diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index b9928727..64578cb8 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -1,17 +1,122 @@ +/*! +
+ +

PageTop SeaORM

+ +

Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales.

+ +[![Doc API](https://img.shields.io/docsrs/pagetop-seaorm?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-seaorm) +[![Crates.io](https://img.shields.io/crates/v/pagetop-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm) +[![Descargas](https://img.shields.io/crates/d/pagetop-seaorm.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm) +[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia) + +
+ +## 🧭 Sobre PageTop + +[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web +clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y +configurables, basadas en HTML, CSS y JavaScript. + + +## ⚡️ Guía rápida + +**Añade la dependencia** a tu `Cargo.toml` activando el motor de base de datos que necesites: + +```toml +[dependencies] +pagetop-seaorm = { version = "...", features = ["sqlite"] } +``` + +Las *features* disponibles son `mysql`, `postgres` y `sqlite`. + +**Configura la conexión** en el archivo de configuración de la aplicación: + +```toml +[database] +db_type = "sqlite" +db_name = "my_app.db" +max_pool_size = 5 +``` + +Para MySQL o PostgreSQL añade también `db_user`, `db_pass`, `db_host` y `db_port`. + +**Declara la extensión** en tu aplicación o en la extensión que la requiera: + +```rust,no_run use pagetop::prelude::*; +struct MyApp; + +impl Extension for MyApp { + fn dependencies(&self) -> Vec { + vec![ + &pagetop_seaorm::SeaORM, + ] + } + + fn initialize(&self) { + install_migrations!(m20240101_000001_create_users_table); + } +} + +#[pagetop::main] +async fn main() -> std::io::Result<()> { + Application::prepare(&MyApp).run()?.await +} +``` + +**Escribe las migraciones** usando la API de SeaORM: + +```rust,no_run +use pagetop_seaorm::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto(Users::Table) + .col(pk_auto(Users::Id)) + .col(string_uniq(Users::Email)) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum Users { + Table, + Id, + Email, +} +``` +*/ + +#![doc( + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" +)] + +use pagetop::prelude::*; + +include_locales!(LOCALES_SEAORM); + pub mod config; + pub mod db; -/// Preludio de la extensión. +/// *Prelude* de la extensión. pub mod prelude { + pub use crate::config::*; pub use crate::db::*; pub use crate::install_migrations; } -include_locales!(LOCALES_SEAORM); - -/// Extensión que integra SeaORM como framework de base de datos para aplicaciones PageTop. +/// Implementa la extensión. pub struct SeaORM; impl Extension for SeaORM { From a0805ed0fb00c1f9ebad77ac50b3361f5afaa098 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 10 May 2026 00:31:33 +0200 Subject: [PATCH 07/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(bootsier):=20Elimin?= =?UTF-8?q?a=20`prelude`=20para=20usar=20`theme`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/form-controls.rs | 2 +- examples/navbar-menus.rs | 2 +- extensions/pagetop-bootsier/src/lib.rs | 6 - .../src/theme/attrs/border.rs | 2 +- .../src/theme/attrs/breakpoint.rs | 2 +- .../src/theme/attrs/button.rs | 4 +- .../pagetop-bootsier/src/theme/attrs/color.rs | 10 +- .../src/theme/attrs/layout.rs | 2 +- .../src/theme/attrs/rounded.rs | 4 +- .../pagetop-bootsier/src/theme/button.rs | 5 +- .../src/theme/classes/border.rs | 28 ++-- .../src/theme/classes/color.rs | 14 +- .../src/theme/classes/layout.rs | 6 +- .../src/theme/classes/rounded.rs | 26 +--- .../pagetop-bootsier/src/theme/container.rs | 10 -- .../src/theme/container/component.rs | 20 ++- .../pagetop-bootsier/src/theme/dropdown.rs | 19 +-- .../src/theme/dropdown/component.rs | 29 +++- .../src/theme/dropdown/props.rs | 2 +- extensions/pagetop-bootsier/src/theme/form.rs | 32 ----- .../pagetop-bootsier/src/theme/form/check.rs | 4 +- .../src/theme/form/checkbox.rs | 2 +- .../src/theme/form/component.rs | 38 ++++-- .../src/theme/form/fieldset.rs | 2 +- .../pagetop-bootsier/src/theme/form/hidden.rs | 2 +- .../pagetop-bootsier/src/theme/form/input.rs | 2 +- .../pagetop-bootsier/src/theme/form/props.rs | 4 +- .../pagetop-bootsier/src/theme/form/radio.rs | 4 +- .../pagetop-bootsier/src/theme/form/range.rs | 2 +- .../pagetop-bootsier/src/theme/form/select.rs | 6 +- .../src/theme/form/textarea.rs | 2 +- extensions/pagetop-bootsier/src/theme/icon.rs | 2 +- .../src/theme/image/component.rs | 12 +- extensions/pagetop-bootsier/src/theme/nav.rs | 22 +-- .../src/theme/nav/component.rs | 30 ++++- .../pagetop-bootsier/src/theme/nav/item.rs | 2 +- .../pagetop-bootsier/src/theme/navbar.rs | 122 +---------------- .../src/theme/navbar/brand.rs | 2 +- .../src/theme/navbar/component.rs | 126 +++++++++++++++++- .../pagetop-bootsier/src/theme/navbar/item.rs | 2 +- .../src/theme/navbar/props.rs | 2 +- .../pagetop-bootsier/src/theme/offcanvas.rs | 22 +-- .../src/theme/offcanvas/component.rs | 26 +++- 43 files changed, 315 insertions(+), 348 deletions(-) diff --git a/examples/form-controls.rs b/examples/form-controls.rs index e49844e8..4a6fc6c0 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use pagetop_bootsier::prelude::*; +use pagetop_bootsier::theme::*; include_locales!(LOC from "examples/locale"); diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index a0d85f3b..38918aed 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use pagetop_bootsier::prelude::*; +use pagetop_bootsier::theme::*; include_locales!(LOC from "examples/locale"); diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index b07b42d5..ca2a80c8 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -96,12 +96,6 @@ pub mod config; pub mod theme; -/// *Prelude* del tema. -pub mod prelude { - pub use crate::config::*; - pub use crate::theme::*; -} - /// Plantillas que Bootsier añade. #[derive(AutoDefault)] pub enum BootsierTemplate { diff --git a/extensions/pagetop-bootsier/src/theme/attrs/border.rs b/extensions/pagetop-bootsier/src/theme/attrs/border.rs index b46a5c6b..af66db78 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/border.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/border.rs @@ -58,7 +58,7 @@ impl BorderColor { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary"); /// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle"); /// assert_eq!(BorderColor::Black.to_class(), "border-black"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs b/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs index 992f8525..ec2c8568 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs @@ -70,7 +70,7 @@ impl BreakPoint { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// let bp = BreakPoint::MD; /// assert_eq!(bp.class_with("col", ""), "col-md"); /// assert_eq!(bp.class_with("col", "6"), "col-md-6"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/button.rs b/extensions/pagetop-bootsier/src/theme/attrs/button.rs index dc74fbea..01e0dd57 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/button.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/button.rs @@ -76,7 +76,7 @@ impl ButtonColor { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!( /// ButtonColor::Background(Color::Primary).to_class(), /// "btn-primary" @@ -132,7 +132,7 @@ impl ButtonSize { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(ButtonSize::Small.to_class(), "btn-sm"); /// assert_eq!(ButtonSize::Large.to_class(), "btn-lg"); /// assert_eq!(ButtonSize::Default.to_class(), ""); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/color.rs b/extensions/pagetop-bootsier/src/theme/attrs/color.rs index c408aadc..eb9f57d4 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/color.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/color.rs @@ -44,7 +44,7 @@ impl Color { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(Color::Primary.to_class(), "primary"); /// assert_eq!(Color::Danger.to_class(), "danger"); /// ``` @@ -124,7 +124,7 @@ impl Opacity { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100"); /// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50"); /// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25"); @@ -156,7 +156,7 @@ impl Opacity { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(Opacity::Opaque.to_class(), "opacity-100"); /// assert_eq!(Opacity::Half.to_class(), "opacity-50"); /// assert_eq!(Opacity::Default.to_class(), ""); @@ -237,7 +237,7 @@ impl ColorBg { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(ColorBg::Body.to_class(), "bg-body"); /// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary"); /// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle"); @@ -321,7 +321,7 @@ impl ColorText { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(ColorText::Body.to_class(), "text-body"); /// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary"); /// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/layout.rs b/extensions/pagetop-bootsier/src/theme/attrs/layout.rs index a1255dc0..81b07834 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/layout.rs @@ -61,7 +61,7 @@ impl ScaleSize { /// # Ejemplo /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(ScaleSize::Auto.class_with("border"), "border"); /// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0"); /// assert_eq!(ScaleSize::Three.class_with("p"), "p-3"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs b/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs index 69976142..2a959767 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs @@ -71,7 +71,7 @@ impl RoundedRadius { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2"); /// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0"); /// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3"); @@ -103,7 +103,7 @@ impl RoundedRadius { /// # Ejemplos /// /// ```rust - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// assert_eq!(RoundedRadius::Default.to_class(), "rounded"); /// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0"); /// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3"); diff --git a/extensions/pagetop-bootsier/src/theme/button.rs b/extensions/pagetop-bootsier/src/theme/button.rs index 48eb7283..e494c1df 100644 --- a/extensions/pagetop-bootsier/src/theme/button.rs +++ b/extensions/pagetop-bootsier/src/theme/button.rs @@ -19,8 +19,9 @@ use crate::theme::{ButtonAction, ButtonColor, ButtonSize}; /// # Ejemplo /// /// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// /// let save = Button::submit(L10n::n("Save")) /// .with_color(ButtonColor::Background(Color::Primary)); /// diff --git a/extensions/pagetop-bootsier/src/theme/classes/border.rs b/extensions/pagetop-bootsier/src/theme/classes/border.rs index 8a6bd6c4..bfa65522 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/border.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/border.rs @@ -26,45 +26,33 @@ use crate::theme::attrs::{BorderColor, Opacity, ScaleSize, Side}; /// /// # Ejemplos /// -/// **Borde global:** /// ```rust -/// # use pagetop_bootsier::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// // Borde global. /// let b = classes::Border::with(ScaleSize::Two); /// assert_eq!(b.to_class(), "border-2"); -/// ``` /// -/// **Aditivo (solo borde superior):** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Aditivo (sólo borde superior): /// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One); /// assert_eq!(b.to_class(), "border-top-1"); -/// ``` /// -/// **Sustractivo (borde global menos el superior):** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Sustractivo (borde global menos el superior): /// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero); /// assert_eq!(b.to_class(), "border border-top-0"); -/// ``` /// -/// **Ancho por lado (lado lógico inicial a 2 y final a 4):** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Ancho por lado (lado lógico inicial a 2 y final a 4): /// let b = classes::Border::default() /// .with_side(Side::Start, ScaleSize::Two) /// .with_side(Side::End, ScaleSize::Four); /// assert_eq!(b.to_class(), "border-end-4 border-start-2"); -/// ``` /// -/// **Combinado (ejemplo completo):** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Combinado (ejemplo completo): /// let b = classes::Border::new() // Borde por defecto. /// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior. /// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final. /// .with_color(BorderColor::Theme(Color::Primary)) /// .with_opacity(Opacity::Half); -/// /// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50"); /// ``` #[rustfmt::skip] @@ -158,7 +146,7 @@ impl Border { /// # Ejemplos /// /// ```rust -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// // Convertir explícitamente con `From::from`: /// let b = classes::Border::from(ScaleSize::Two); /// assert_eq!(b.to_class(), "border-2"); diff --git a/extensions/pagetop-bootsier/src/theme/classes/color.rs b/extensions/pagetop-bootsier/src/theme/classes/color.rs index 4776ca9f..10638a52 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/color.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/color.rs @@ -9,7 +9,8 @@ use crate::theme::attrs::{ColorBg, ColorText, Opacity}; /// # Ejemplos /// /// ``` -/// # use pagetop_bootsier::prelude::*; +/// use pagetop_bootsier::theme::*; +/// /// // Sin clases. /// let s = classes::Background::new(); /// assert_eq!(s.to_class(), ""); @@ -90,7 +91,7 @@ impl From<(ColorBg, Opacity)> for Background { /// # Ejemplo /// /// ``` - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into(); /// assert_eq!(s.to_class(), "bg-white bg-opacity-25"); /// ``` @@ -105,7 +106,7 @@ impl From for Background { /// # Ejemplo /// /// ``` - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// let s: classes::Background = ColorBg::Black.into(); /// assert_eq!(s.to_class(), "bg-black"); /// ``` @@ -121,7 +122,8 @@ impl From for Background { /// # Ejemplos /// /// ``` -/// # use pagetop_bootsier::prelude::*; +/// use pagetop_bootsier::theme::*; +/// /// // Sin clases. /// let s = classes::Text::new(); /// assert_eq!(s.to_class(), ""); @@ -202,7 +204,7 @@ impl From<(ColorText, Opacity)> for Text { /// # Ejemplo /// /// ``` - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into(); /// assert_eq!(s.to_class(), "text-danger text-opacity-100"); /// ``` @@ -218,7 +220,7 @@ impl From for Text { /// # Ejemplo /// /// ``` - /// # use pagetop_bootsier::prelude::*; + /// # use pagetop_bootsier::theme::*; /// let s: classes::Text = ColorText::Black.into(); /// assert_eq!(s.to_class(), "text-black"); /// ``` diff --git a/extensions/pagetop-bootsier/src/theme/classes/layout.rs b/extensions/pagetop-bootsier/src/theme/classes/layout.rs index 2a927acc..ee403a88 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/layout.rs @@ -10,7 +10,8 @@ use crate::theme::BreakPoint; /// # Ejemplos /// /// ```rust -/// # use pagetop_bootsier::prelude::*; +/// use pagetop_bootsier::theme::*; +/// /// let m = classes::Margin::with(Side::Top, ScaleSize::Three); /// assert_eq!(m.to_class(), "mt-3"); /// @@ -97,7 +98,8 @@ impl Margin { /// # Ejemplos /// /// ```rust -/// # use pagetop_bootsier::prelude::*; +/// use pagetop_bootsier::theme::*; +/// /// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two); /// assert_eq!(p.to_class(), "px-2"); /// diff --git a/extensions/pagetop-bootsier/src/theme/classes/rounded.rs b/extensions/pagetop-bootsier/src/theme/classes/rounded.rs index 16213e01..9fcb544e 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/rounded.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/rounded.rs @@ -14,42 +14,30 @@ use crate::theme::attrs::RoundedRadius; /// /// # Ejemplos /// -/// **Radio global:** /// ```rust -/// # use pagetop_bootsier::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// // Radio global: /// let r = classes::Rounded::with(RoundedRadius::Default); /// assert_eq!(r.to_class(), "rounded"); -/// ``` /// -/// **Sin redondeo:** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Sin redondeo: /// let r = classes::Rounded::new(); /// assert_eq!(r.to_class(), ""); -/// ``` /// -/// **Radio en las esquinas de un lado lógico:** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Radio en las esquinas de un lado lógico: /// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2); /// assert_eq!(r.to_class(), "rounded-end-2"); -/// ``` /// -/// **Radio en una esquina concreta:** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Radio en una esquina concreta: /// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3); /// assert_eq!(r.to_class(), "rounded-top-start-3"); -/// ``` /// -/// **Combinado (ejemplo completo):** -/// ```rust -/// # use pagetop_bootsier::prelude::*; +/// // Combinado (ejemplo completo): /// let r = classes::Rounded::new() /// .with_top(RoundedRadius::Default) // Añade redondeo arriba. /// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta. /// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina. -/// /// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"); /// ``` #[rustfmt::skip] diff --git a/extensions/pagetop-bootsier/src/theme/container.rs b/extensions/pagetop-bootsier/src/theme/container.rs index a860110f..707fdc4f 100644 --- a/extensions/pagetop-bootsier/src/theme/container.rs +++ b/extensions/pagetop-bootsier/src/theme/container.rs @@ -6,16 +6,6 @@ //! Con [`container::Width`](crate::theme::container::Width) se puede definir el ancho y el //! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el //! fondo, texto, borde o esquinas redondeadas. -//! -//! # Ejemplo -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let main = Container::main() -//! .with_id("main-page") -//! .with_width(container::Width::From(BreakPoint::LG)); -//! ``` mod props; pub use props::{Kind, Width}; diff --git a/extensions/pagetop-bootsier/src/theme/container/component.rs b/extensions/pagetop-bootsier/src/theme/container/component.rs index 0635ad18..9e4e33c0 100644 --- a/extensions/pagetop-bootsier/src/theme/container/component.rs +++ b/extensions/pagetop-bootsier/src/theme/container/component.rs @@ -1,11 +1,23 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; -/// Componente para crear un **contenedor de componentes**. +/// Componente para crear un **contenedor de componentes** ([`container`]). /// -/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza -/// si existen componentes hijos (*children*). +/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de +/// los tipos definidos en [`container::Kind`]. +/// +/// Si no contiene elementos, el componente **no se renderiza**. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop_bootsier::theme::*; +/// +/// let main = Container::main() +/// .with_id("main-page") +/// .with_width(container::Width::From(BreakPoint::LG)); +/// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Container { #[getters(skip)] diff --git a/extensions/pagetop-bootsier/src/theme/dropdown.rs b/extensions/pagetop-bootsier/src/theme/dropdown.rs index 213756c7..c8e27870 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown.rs @@ -1,4 +1,4 @@ -//! Definiciones para crear menús desplegables [`Dropdown`]. +//! Definiciones para crear menús desplegables ([`Dropdown`]). //! //! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del //! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de @@ -6,23 +6,6 @@ //! //! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y //! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n). -//! -//! # Ejemplo -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let dd = Dropdown::new() -//! .with_title(L10n::n("Menu")) -//! .with_button_color(ButtonColor::Background(Color::Secondary)) -//! .with_auto_close(dropdown::AutoClose::ClickableInside) -//! .with_direction(dropdown::Direction::Dropend) -//! .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into())) -//! .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into())) -//! .with_item(dropdown::Item::divider()) -//! .with_item(dropdown::Item::header(L10n::n("User session"))) -//! .with_item(dropdown::Item::button(L10n::n("Sign out"))); -//! ``` mod props; pub use props::{AutoClose, Direction, MenuAlign, MenuPosition}; diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index 833cf40b..ca15c635 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -1,14 +1,14 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; use crate::LOCALES_BOOTSIER; -/// Componente para crear un **menú desplegable**. +/// Componente para crear un **menú desplegable** ([`dropdown`]). /// /// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split)) -/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra/oculta según la -/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de -/// apertura, alineación o política de cierre. +/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra u oculta según +/// la interacción del usuario. Admite variaciones para el tamaño y el color del botón, también para +/// la dirección de apertura, alineación o política de cierre. /// /// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de /// elementos sin ningún botón para interactuar. @@ -17,8 +17,25 @@ use crate::LOCALES_BOOTSIER; /// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el /// resto de propiedades no afectarán a su representación en [`Nav`]. /// -/// Ver ejemplo en el módulo [`dropdown`]. /// Si no contiene elementos, el componente **no se renderiza**. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// let dd = Dropdown::new() +/// .with_title(L10n::n("Menu")) +/// .with_button_color(ButtonColor::Background(Color::Secondary)) +/// .with_auto_close(dropdown::AutoClose::ClickableInside) +/// .with_direction(dropdown::Direction::Dropend) +/// .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into())) +/// .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into())) +/// .with_item(dropdown::Item::divider()) +/// .with_item(dropdown::Item::header(L10n::n("User session"))) +/// .with_item(dropdown::Item::button(L10n::n("Sign out"))); +/// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Dropdown { #[getters(skip)] diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/props.rs b/extensions/pagetop-bootsier/src/theme/dropdown/props.rs index fd315508..fb11783d 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/props.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/props.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; // **< AutoClose >********************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/form.rs b/extensions/pagetop-bootsier/src/theme/form.rs index 634dc48a..82c603ef 100644 --- a/extensions/pagetop-bootsier/src/theme/form.rs +++ b/extensions/pagetop-bootsier/src/theme/form.rs @@ -1,36 +1,4 @@ //! Definiciones para crear formularios ([`Form`]). -//! -//! # Ejemplo -//! -//! ```rust -//! use pagetop::prelude::*; -//! use pagetop_bootsier::prelude::*; -//! -//! let form_login = Form::new() -//! .with_id("login") -//! .with_action("/login") -//! .with_child( -//! form::input::Field::email() -//! .with_name("email") -//! .with_label(L10n::n("Email")) -//! .with_required(true), -//! ) -//! .with_child( -//! form::input::Field::password() -//! .with_name("password") -//! .with_label(L10n::n("Password")) -//! .with_required(true), -//! ) -//! .with_child( -//! form::Checkbox::check() -//! .with_name("remember") -//! .with_label(L10n::n("Remember me")), -//! ) -//! .with_child( -//! Button::submit(L10n::n("Sign in")) -//! .with_color(ButtonColor::Background(Color::Primary)), -//! ); -//! ``` mod props; pub use props::{Autocomplete, AutofillField, CheckboxKind, Method}; diff --git a/extensions/pagetop-bootsier/src/theme/form/check.rs b/extensions/pagetop-bootsier/src/theme/form/check.rs index 824ed92a..434b7e6c 100644 --- a/extensions/pagetop-bootsier/src/theme/form/check.rs +++ b/extensions/pagetop-bootsier/src/theme/form/check.rs @@ -17,7 +17,7 @@ use pagetop::prelude::*; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] @@ -82,7 +82,7 @@ impl Item { /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let interests = form::check::Field::new() /// .with_name("interests") /// .with_label(L10n::n("Areas of interest")) diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs index 6206d983..60d7120d 100644 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs @@ -17,7 +17,7 @@ use crate::LOCALES_BOOTSIER; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let accept_terms = form::Checkbox::check() // También sirve new() o default(). /// .with_name("terms_accepted") /// .with_label(L10n::n("I accept the terms and conditions")) diff --git a/extensions/pagetop-bootsier/src/theme/form/component.rs b/extensions/pagetop-bootsier/src/theme/form/component.rs index e4c22a56..80efa941 100644 --- a/extensions/pagetop-bootsier/src/theme/form/component.rs +++ b/extensions/pagetop-bootsier/src/theme/form/component.rs @@ -2,9 +2,9 @@ use pagetop::prelude::*; use crate::theme::form; -/// Componente para crear un **formulario**. +/// Componente para crear un **formulario** ([`form`]). /// -/// Este componente renderiza un `
` estándar con soporte para los atributos más habituales: +/// Este componente renderiza un formulario estándar con soporte para los atributos más habituales: /// /// - `id`: identificador opcional del formulario. /// - `classes`: clases CSS adicionales (p. ej. utilidades CSS). @@ -17,13 +17,33 @@ use crate::theme::form; /// # Ejemplo /// /// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; -/// let search = Form::new() -/// .with_id("search") -/// .with_action("/search") -/// .with_method(form::Method::Get) -/// .with_child(form::input::Field::search().with_name("q")); +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// let form_login = Form::new() +/// .with_id("login") +/// .with_action("/login") +/// .with_child( +/// form::input::Field::email() +/// .with_name("email") +/// .with_label(L10n::n("Email")) +/// .with_required(true), +/// ) +/// .with_child( +/// form::input::Field::password() +/// .with_name("password") +/// .with_label(L10n::n("Password")) +/// .with_required(true), +/// ) +/// .with_child( +/// form::Checkbox::check() +/// .with_name("remember") +/// .with_label(L10n::n("Remember me")), +/// ) +/// .with_child( +/// Button::submit(L10n::n("Sign in")) +/// .with_color(ButtonColor::Background(Color::Primary)), +/// ); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Form { diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs index b5d0e822..1aacba6c 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -15,7 +15,7 @@ use pagetop::prelude::*; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let personal_data = form::Fieldset::new() /// .with_legend(L10n::n("Personal data")) /// .with_description(L10n::n("Enter your full name and contact email.")) diff --git a/extensions/pagetop-bootsier/src/theme/form/hidden.rs b/extensions/pagetop-bootsier/src/theme/form/hidden.rs index 61d24547..6d367155 100644 --- a/extensions/pagetop-bootsier/src/theme/form/hidden.rs +++ b/extensions/pagetop-bootsier/src/theme/form/hidden.rs @@ -12,7 +12,7 @@ use pagetop::prelude::*; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let token = form::Hidden::new() /// .with_name("csrf_token") /// .with_value("a1b2c3d4e5"); diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index a6dc34ea..997b7c45 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -106,7 +106,7 @@ impl fmt::Display for Mode { /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let email = form::input::Field::email() /// .with_name("email") /// .with_label(L10n::n("Email address")) diff --git a/extensions/pagetop-bootsier/src/theme/form/props.rs b/extensions/pagetop-bootsier/src/theme/form/props.rs index dbd1e705..2ef88f3e 100644 --- a/extensions/pagetop-bootsier/src/theme/form/props.rs +++ b/extensions/pagetop-bootsier/src/theme/form/props.rs @@ -52,7 +52,7 @@ pub enum CheckboxKind { /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// // Correo electrónico con sugerencia semántica del navegador. /// let ac = form::Autocomplete::email(); /// @@ -244,7 +244,7 @@ impl fmt::Display for Autocomplete { /// # Ejemplo /// /// ```rust -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let ac = form::Autocomplete::token(form::AutofillField::Username); /// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress); /// let ac = form::Autocomplete::section("job", form::AutofillField::Email); diff --git a/extensions/pagetop-bootsier/src/theme/form/radio.rs b/extensions/pagetop-bootsier/src/theme/form/radio.rs index 2514c4bf..d79c4f44 100644 --- a/extensions/pagetop-bootsier/src/theme/form/radio.rs +++ b/extensions/pagetop-bootsier/src/theme/form/radio.rs @@ -16,7 +16,7 @@ use crate::LOCALES_BOOTSIER; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] @@ -76,7 +76,7 @@ impl Item { /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let plan = form::radio::Field::new() /// .with_name("plan") /// .with_label(L10n::n("Subscription plan")) diff --git a/extensions/pagetop-bootsier/src/theme/form/range.rs b/extensions/pagetop-bootsier/src/theme/form/range.rs index 40350479..45d07acf 100644 --- a/extensions/pagetop-bootsier/src/theme/form/range.rs +++ b/extensions/pagetop-bootsier/src/theme/form/range.rs @@ -10,7 +10,7 @@ use pagetop::prelude::*; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let volume = form::Range::new() /// .with_name("volume") /// .with_label(L10n::n("Volume")) diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 1fa74f09..92736586 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -20,7 +20,7 @@ use crate::LOCALES_BOOTSIER; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] @@ -76,7 +76,7 @@ impl Item { /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let group = form::select::Group::new(L10n::n("Europe")) /// .with_item(form::select::Item::new("es", L10n::n("Spanish"))) /// .with_item(form::select::Item::new("fr", L10n::n("French"))); @@ -149,7 +149,7 @@ pub enum Entry { /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let idioma = form::select::Field::new() /// .with_name("language") /// .with_label(L10n::n("Language")) diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index c545df8c..781e1d09 100644 --- a/extensions/pagetop-bootsier/src/theme/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -13,7 +13,7 @@ use crate::LOCALES_BOOTSIER; /// /// ```rust /// # use pagetop::prelude::*; -/// # use pagetop_bootsier::prelude::*; +/// # use pagetop_bootsier::theme::*; /// let descripcion = form::Textarea::new() /// .with_name("description") /// .with_label(L10n::n("Description")) diff --git a/extensions/pagetop-bootsier/src/theme/icon.rs b/extensions/pagetop-bootsier/src/theme/icon.rs index 935aa847..9ef6aeee 100644 --- a/extensions/pagetop-bootsier/src/theme/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/icon.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::theme::*; const DEFAULT_VIEWBOX: &str = "0 0 16 16"; diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index 898fb573..678ccdb3 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -1,14 +1,16 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; -/// Componente para renderizar una **imagen**. +/// Componente para renderizar una **imagen** ([`image`]). /// -/// - Ajusta su disposición según el origen definido en [`image::Source`]. -/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde** +/// A una imagen se le puede: +/// +/// - Establecer su contenido a partir del origen definido en [`image::Source`]. +/// - Configurar sus **dimensiones** ([`with_size()`](Self::with_size)), **borde** /// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas** /// ([`classes::Rounded`](crate::theme::classes::Rounded)). -/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`]. +/// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`]. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Image { #[getters(skip)] diff --git a/extensions/pagetop-bootsier/src/theme/nav.rs b/extensions/pagetop-bootsier/src/theme/nav.rs index 16b3d2d7..74e9214d 100644 --- a/extensions/pagetop-bootsier/src/theme/nav.rs +++ b/extensions/pagetop-bootsier/src/theme/nav.rs @@ -1,4 +1,4 @@ -//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación. +//! Definiciones para crear menús ([`Nav`]). //! //! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`], //! con distintos comportamientos según su finalidad, como enlaces de navegación o menús @@ -6,26 +6,6 @@ //! //! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y //! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n). -//! -//! # Ejemplo -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let nav = Nav::tabs() -//! .with_layout(nav::Layout::End) -//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) -//! .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into())) -//! .with_item(nav::Item::dropdown( -//! Dropdown::new() -//! .with_title(L10n::n("Options")) -//! .with_item(ChildOp::AddMany(vec![ -//! dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(), -//! dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(), -//! ])), -//! )) -//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into())); -//! ``` mod props; pub use props::{Kind, Layout}; diff --git a/extensions/pagetop-bootsier/src/theme/nav/component.rs b/extensions/pagetop-bootsier/src/theme/nav/component.rs index 0b7797c8..aeb0447e 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/component.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/component.rs @@ -1,15 +1,35 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; -/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]). +/// Componente para crear un **menú** ([`nav`]). /// /// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes -/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite -/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)). +/// ([`nav::Kind`]) como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). +/// También permite controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)). /// -/// Ver ejemplo en el módulo [`nav`]. /// Si no contiene elementos, el componente **no se renderiza**. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// let nav = Nav::tabs() +/// .with_layout(nav::Layout::End) +/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) +/// .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into())) +/// .with_item(nav::Item::dropdown( +/// Dropdown::new() +/// .with_title(L10n::n("Options")) +/// .with_item(ChildOp::AddMany(vec![ +/// dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(), +/// dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(), +/// ])), +/// )) +/// .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into())); +/// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Nav { #[getters(skip)] diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index 956ccdcd..ef5a6fe9 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; use crate::LOCALES_BOOTSIER; // **< ItemKind >*********************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/navbar.rs b/extensions/pagetop-bootsier/src/theme/navbar.rs index 31a16ccc..f381b1f3 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar.rs @@ -1,4 +1,4 @@ -//! Definiciones para crear barras de navegación [`Navbar`]. +//! Definiciones para crear barras de navegación ([`Navbar`]). //! //! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra //! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús @@ -6,126 +6,6 @@ //! //! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand)) //! que identifique la compañía, producto o nombre del proyecto asociado a la solución web. -//! -//! # Ejemplos -//! -//! Barra **simple**, sólo con un menú horizontal: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let navbar = Navbar::simple() -//! .with_item(navbar::Item::nav( -//! Nav::new() -//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) -//! .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into())) -//! .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into())) -//! )); -//! ``` -//! -//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let navbar = Navbar::simple_toggle() -//! .with_expand(BreakPoint::MD) -//! .with_item(navbar::Item::nav( -//! Nav::new() -//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) -//! .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into())) -//! .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into())) -//! )); -//! ``` -//! -//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let brand = navbar::Brand::new() -//! .with_title(L10n::n("PageTop")) -//! .with_route(Some(|cx| cx.route("/"))); -//! -//! let navbar = Navbar::brand_left(brand) -//! .with_item(navbar::Item::nav( -//! Nav::new() -//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) -//! .with_item(nav::Item::dropdown( -//! Dropdown::new() -//! .with_title(L10n::n("Tools")) -//! .with_item(dropdown::Item::link( -//! L10n::n("Generator"), |_| "/tools/gen".into()) -//! ) -//! .with_item(dropdown::Item::link( -//! L10n::n("Reports"), |_| "/tools/reports".into()) -//! ) -//! )) -//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into())) -//! )); -//! ``` -//! -//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let brand = navbar::Brand::new() -//! .with_title(L10n::n("Intranet")) -//! .with_route(Some(|cx| cx.route("/"))); -//! -//! let navbar = Navbar::brand_right(brand) -//! .with_expand(BreakPoint::LG) -//! .with_item(navbar::Item::nav( -//! Nav::pills() -//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into())) -//! .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into())) -//! )); -//! ``` -//! -//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let oc = Offcanvas::new() -//! .with_id("main_offcanvas") -//! .with_title(L10n::n("Main menu")) -//! .with_placement(offcanvas::Placement::Start) -//! .with_backdrop(offcanvas::Backdrop::Enabled); -//! -//! let navbar = Navbar::offcanvas(oc) -//! .with_item(navbar::Item::nav( -//! Nav::new() -//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) -//! .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into())) -//! .with_item(nav::Item::dropdown( -//! Dropdown::new() -//! .with_title(L10n::n("More")) -//! .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into())) -//! .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into())) -//! )) -//! )); -//! ``` -//! -//! Barra **fija arriba**: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let brand = navbar::Brand::new() -//! .with_title(L10n::n("Main App")) -//! .with_route(Some(|cx| cx.route("/"))); -//! -//! let navbar = Navbar::brand_left(brand) -//! .with_position(navbar::Position::FixedTop) -//! .with_item(navbar::Item::nav( -//! Nav::new() -//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into())) -//! .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into())) -//! .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into())) -//! )); -//! ``` mod props; pub use props::{Layout, Position}; diff --git a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs index 4d575e03..9e5082b6 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; /// Marca de identidad para mostrar en una barra de navegación [`Navbar`]. /// diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index 33aa80de..ccd97e90 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -1,19 +1,139 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; use crate::LOCALES_BOOTSIER; const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_OFFCANVAS: &str = "offcanvas"; -/// Componente para crear una **barra de navegación**. +/// Componente para crear una **barra de navegación** ([`navbar`]). /// /// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con /// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También /// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`]. /// -/// Ver ejemplos en el módulo [`navbar`]. /// Si no contiene elementos, el componente **no se renderiza**. +/// +/// # Ejemplos +/// +/// Barra **simple**, sólo con un menú horizontal: +/// +/// ```rust +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// let navbar = Navbar::simple() +/// .with_item(navbar::Item::nav( +/// Nav::new() +/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) +/// .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into())) +/// .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into())) +/// )); +/// ``` +/// +/// Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::theme::*; +/// let navbar = Navbar::simple_toggle() +/// .with_expand(BreakPoint::MD) +/// .with_item(navbar::Item::nav( +/// Nav::new() +/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) +/// .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into())) +/// .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into())) +/// )); +/// ``` +/// +/// Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::theme::*; +/// let brand = navbar::Brand::new() +/// .with_title(L10n::n("PageTop")) +/// .with_route(Some(|cx| cx.route("/"))); +/// +/// let navbar = Navbar::brand_left(brand) +/// .with_item(navbar::Item::nav( +/// Nav::new() +/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) +/// .with_item(nav::Item::dropdown( +/// Dropdown::new() +/// .with_title(L10n::n("Tools")) +/// .with_item(dropdown::Item::link( +/// L10n::n("Generator"), |_| "/tools/gen".into()) +/// ) +/// .with_item(dropdown::Item::link( +/// L10n::n("Reports"), |_| "/tools/reports".into()) +/// ) +/// )) +/// .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into())) +/// )); +/// ``` +/// +/// Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::theme::*; +/// let brand = navbar::Brand::new() +/// .with_title(L10n::n("Intranet")) +/// .with_route(Some(|cx| cx.route("/"))); +/// +/// let navbar = Navbar::brand_right(brand) +/// .with_expand(BreakPoint::LG) +/// .with_item(navbar::Item::nav( +/// Nav::pills() +/// .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into())) +/// .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into())) +/// )); +/// ``` +/// +/// Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::theme::*; +/// let oc = Offcanvas::new() +/// .with_id("main_offcanvas") +/// .with_title(L10n::n("Main menu")) +/// .with_placement(offcanvas::Placement::Start) +/// .with_backdrop(offcanvas::Backdrop::Enabled); +/// +/// let navbar = Navbar::offcanvas(oc) +/// .with_item(navbar::Item::nav( +/// Nav::new() +/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into())) +/// .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into())) +/// .with_item(nav::Item::dropdown( +/// Dropdown::new() +/// .with_title(L10n::n("More")) +/// .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into())) +/// .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into())) +/// )) +/// )); +/// ``` +/// +/// Barra **fija arriba**: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::theme::*; +/// let brand = navbar::Brand::new() +/// .with_title(L10n::n("Main App")) +/// .with_route(Some(|cx| cx.route("/"))); +/// +/// let navbar = Navbar::brand_left(brand) +/// .with_position(navbar::Position::FixedTop) +/// .with_item(navbar::Item::nav( +/// Nav::new() +/// .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into())) +/// .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into())) +/// .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into())) +/// )); +/// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Navbar { #[getters(skip)] diff --git a/extensions/pagetop-bootsier/src/theme/navbar/item.rs b/extensions/pagetop-bootsier/src/theme/navbar/item.rs index caba4e7d..9b48adf1 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/item.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/item.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; /// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar). /// diff --git a/extensions/pagetop-bootsier/src/theme/navbar/props.rs b/extensions/pagetop-bootsier/src/theme/navbar/props.rs index 1ac248b7..e0d64916 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/props.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/props.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; // **< Layout >************************************************************************************* diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas.rs b/extensions/pagetop-bootsier/src/theme/offcanvas.rs index 166893aa..63ecbc66 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas.rs @@ -1,24 +1,4 @@ -//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`]. -//! -//! # Ejemplo -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let panel = Offcanvas::new() -//! .with_id("offcanvas_example") -//! .with_title(L10n::n("Offcanvas title")) -//! .with_placement(offcanvas::Placement::End) -//! .with_backdrop(offcanvas::Backdrop::Enabled) -//! .with_body_scroll(offcanvas::BodyScroll::Enabled) -//! .with_visibility(offcanvas::Visibility::Default) -//! .with_child(Dropdown::new() -//! .with_title(L10n::n("Menu")) -//! .with_item(dropdown::Item::label(L10n::n("Label"))) -//! .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into())) -//! .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into())) -//! ); -//! ``` +//! Definiciones para crear paneles laterales deslizantes ([`Offcanvas`]). mod props; pub use props::{Backdrop, BodyScroll, Placement, Visibility}; diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs index 88aad443..764627e4 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -1,9 +1,9 @@ use pagetop::prelude::*; -use crate::prelude::*; +use crate::theme::*; use crate::LOCALES_BOOTSIER; -/// Componente para crear un **panel lateral deslizante** con contenidos adicionales. +/// Componente para crear un **panel lateral deslizante** ([`offcanvas`]). /// /// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes /// características principales: @@ -19,8 +19,28 @@ use crate::LOCALES_BOOTSIER; /// - Asocia título y controles de accesibilidad a un identificador único y expone atributos /// adecuados para lectores de pantalla y navegación por teclado. /// -/// Ver ejemplo en el módulo [`offcanvas`]. /// Si no contiene elementos, el componente **no se renderiza**. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// let panel = Offcanvas::new() +/// .with_id("offcanvas_example") +/// .with_title(L10n::n("Offcanvas title")) +/// .with_placement(offcanvas::Placement::End) +/// .with_backdrop(offcanvas::Backdrop::Enabled) +/// .with_body_scroll(offcanvas::BodyScroll::Enabled) +/// .with_visibility(offcanvas::Visibility::Default) +/// .with_child(Dropdown::new() +/// .with_title(L10n::n("Menu")) +/// .with_item(dropdown::Item::label(L10n::n("Label"))) +/// .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into())) +/// .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into())) +/// ); +/// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Offcanvas { #[getters(skip)] From fa5489dbb004f9ff0c0866e3322d0146c073b3e5 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 10 May 2026 00:38:00 +0200 Subject: [PATCH 08/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(seaorm):=20Elimina?= =?UTF-8?q?=20`prelude`=20para=20usar=20`db`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-seaorm/README.md | 2 +- extensions/pagetop-seaorm/src/db/migration/schema.rs | 3 ++- extensions/pagetop-seaorm/src/lib.rs | 9 +-------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index aa76275a..a687828f 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -68,7 +68,7 @@ async fn main() -> std::io::Result<()> { **Escribe las migraciones** usando la API de SeaORM: ```rust,no_run -use pagetop_seaorm::prelude::*; +use pagetop_seaorm::db::*; #[derive(DeriveMigrationName)] pub struct Migration; diff --git a/extensions/pagetop-seaorm/src/db/migration/schema.rs b/extensions/pagetop-seaorm/src/db/migration/schema.rs index f250eae0..ae6a3c4b 100644 --- a/extensions/pagetop-seaorm/src/db/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/db/migration/schema.rs @@ -52,7 +52,8 @@ //! } //! ``` -use crate::prelude::Iden; +use crate::db::Iden; + use sea_orm::sea_query::{ self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement, }; diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index 64578cb8..2ba22db7 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -69,7 +69,7 @@ async fn main() -> std::io::Result<()> { **Escribe las migraciones** usando la API de SeaORM: ```rust,no_run -use pagetop_seaorm::prelude::*; +use pagetop_seaorm::db::*; #[derive(DeriveMigrationName)] pub struct Migration; @@ -109,13 +109,6 @@ pub mod config; pub mod db; -/// *Prelude* de la extensión. -pub mod prelude { - pub use crate::config::*; - pub use crate::db::*; - pub use crate::install_migrations; -} - /// Implementa la extensión. pub struct SeaORM; From 8c861bff0514f7ca520025513d07c8b9511e9e13 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 10 May 2026 00:43:35 +0200 Subject: [PATCH 09/29] =?UTF-8?q?=F0=9F=93=9D=20(seaorm):=20Corrige=20ejem?= =?UTF-8?q?plos=20de=20documentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-seaorm/src/db/migration/schema.rs | 3 +-- extensions/pagetop-seaorm/src/lib.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/pagetop-seaorm/src/db/migration/schema.rs b/extensions/pagetop-seaorm/src/db/migration/schema.rs index ae6a3c4b..b6f89d3a 100644 --- a/extensions/pagetop-seaorm/src/db/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/db/migration/schema.rs @@ -11,9 +11,8 @@ //! the schema helpers to create the Db fields. //! //! ```rust -//! use sea_orm_migration::{prelude::*, schema::*}; +//! use pagetop_seaorm::db::*; //! -//! #[derive(DeriveMigrationName)] //! pub struct Migration; //! //! #[async_trait::async_trait] diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index 2ba22db7..103d95eb 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -43,7 +43,7 @@ Para MySQL o PostgreSQL añade también `db_user`, `db_pass`, `db_host` y `db_po **Declara la extensión** en tu aplicación o en la extensión que la requiera: -```rust,no_run +```rust,ignore use pagetop::prelude::*; struct MyApp; @@ -71,7 +71,6 @@ async fn main() -> std::io::Result<()> { ```rust,no_run use pagetop_seaorm::db::*; -#[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] From aa931ea0527c7325ba7b4f6e1ad01b9aecd0dc68 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 10 May 2026 21:42:19 +0200 Subject: [PATCH 10/29] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20(seaorm):=20Actualiz?= =?UTF-8?q?a=20`sea-orm`=20a=201.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 353 ++++++------------ extensions/pagetop-seaorm/Cargo.toml | 4 +- extensions/pagetop-seaorm/README.md | 38 +- extensions/pagetop-seaorm/src/db.rs | 12 +- .../src/db/migration/connection.rs | 8 +- .../src/db/migration/manager.rs | 67 ++-- .../src/db/migration/migrator.rs | 66 ++-- .../src/db/migration/prelude.rs | 1 - .../pagetop-seaorm/src/db/migration/schema.rs | 9 +- 9 files changed, 227 insertions(+), 331 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2541410..8b01826c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -222,7 +222,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -273,7 +273,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -535,7 +534,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -552,7 +551,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -582,12 +581,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1036,7 +1029,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1047,7 +1040,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1080,7 +1073,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn", ] [[package]] @@ -1102,7 +1095,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn", "unicode-xid", ] @@ -1137,7 +1130,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1146,18 +1139,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "educe" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "either" version = "1.15.0" @@ -1176,26 +1157,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1322,7 +1283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -1335,7 +1296,7 @@ dependencies = [ "ignore", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "unic-langid", ] @@ -1353,7 +1314,7 @@ dependencies = [ "ignore", "intl-memoizer", "log", - "thiserror 2.0.18", + "thiserror", "unic-langid", ] @@ -1499,7 +1460,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1588,7 +1549,7 @@ checksum = "c43d815f896a3c730f0d76b8348a1700dc8d8fd6c377e4590d531bdd646574d8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1692,6 +1653,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1703,11 +1666,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] @@ -1715,9 +1678,6 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" @@ -1998,7 +1958,7 @@ checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2148,9 +2108,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -2251,12 +2211,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2302,16 +2256,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2413,7 +2357,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2436,18 +2380,18 @@ dependencies = [ [[package]] name = "ordered-float" -version = "3.9.2" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ "num-traits", ] [[package]] name = "ouroboros" -version = "0.17.2" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ "aliasable", "ouroboros_macro", @@ -2456,15 +2400,15 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.17.2" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ "heck 0.4.1", - "proc-macro-error", "proc-macro2", + "proc-macro2-diagnostics", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2533,7 +2477,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2599,12 +2543,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pastey" version = "0.2.2" @@ -2683,7 +2621,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2712,7 +2650,7 @@ checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2844,31 +2782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "syn", ] [[package]] @@ -2890,7 +2804,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2916,8 +2830,9 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "version_check", + "yansi", ] [[package]] @@ -3178,18 +3093,19 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] name = "sea-orm" -version = "1.0.1" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1fee0cf8528dbe6eda29d5798afc522a63b75e44c5b15721e6e64af9c7cc4b" +checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" dependencies = [ "async-stream", "async-trait", - "futures", + "derive_more 2.1.1", + "futures-util", "log", "ouroboros", "sea-orm-macros", @@ -3198,32 +3114,31 @@ dependencies = [ "serde", "sqlx", "strum", - "thiserror 1.0.69", + "thiserror", "tracing", "url", ] [[package]] name = "sea-orm-macros" -version = "1.0.1" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8737b566799ed0444f278d13c300c4c6f1a91782f60ff5825a591852d5502030" +checksum = "9b9a3f90e336ec74803e8eb98c61bc98754c1adfba3b4f84d946237b752b1c88" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "sea-bae", - "syn 2.0.117", + "syn", "unicode-ident", ] [[package]] name = "sea-query" -version = "0.31.1" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4fd043b8117af233e221f73e3ea8dfbc8e8c3c928017c474296db45c649105c" +checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" dependencies = [ - "educe", "inherent", "ordered-float", "sea-query-derive", @@ -3231,9 +3146,9 @@ dependencies = [ [[package]] name = "sea-query-binder" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754965d4aee6145bec25d0898e5c931e6c22859789ce62fd85a42a15ed5a8ce3" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ "sea-query", "sqlx", @@ -3249,15 +3164,15 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.117", - "thiserror 2.0.18", + "syn", + "thiserror", ] [[package]] name = "sea-schema" -version = "0.15.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad52149fc81836ea7424c3425d8f6ed8ad448dd16d2e4f6a3907ba46f3f2fd78" +checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" dependencies = [ "futures", "sea-query", @@ -3273,7 +3188,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3338,7 +3253,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3466,6 +3381,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -3516,21 +3434,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -3541,67 +3449,62 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "ahash", "async-io 1.13.0", "async-std", - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "crc", "crossbeam-queue", "either", - "event-listener 2.5.3", - "futures-channel", + "event-listener 5.4.1", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.15.5", "hashlink", - "hex", "indexmap", "log", "memchr", "native-tls", "once_cell", - "paste", "percent-encoding", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror", "tracing", "url", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "async-std", "dotenvy", "either", - "heck 0.4.1", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -3613,19 +3516,18 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", - "tempfile", + "syn", "url", ] [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.11.1", "byteorder", "bytes", @@ -3655,19 +3557,19 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.11.1", "byteorder", "crc", @@ -3675,7 +3577,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -3693,16 +3594,16 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "flume", @@ -3715,10 +3616,11 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", + "thiserror", "tracing", "url", - "urlencoding", ] [[package]] @@ -3777,17 +3679,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -3807,7 +3698,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3833,33 +3724,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "thiserror-impl", ] [[package]] @@ -3870,7 +3741,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4032,7 +3903,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror", "time", "tracing-subscriber", ] @@ -4045,7 +3916,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4154,7 +4025,7 @@ checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" dependencies = [ "proc-macro-hack", "quote", - "syn 2.0.117", + "syn", "unic-langid-impl", ] @@ -4203,12 +4074,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "universal-hash" version = "0.5.1" @@ -4231,12 +4096,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4378,7 +4237,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -4487,7 +4346,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4498,7 +4357,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4718,7 +4577,7 @@ dependencies = [ "heck 0.5.0", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4734,7 +4593,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4782,6 +4641,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.2" @@ -4801,7 +4666,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -4822,7 +4687,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4842,7 +4707,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -4883,7 +4748,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] diff --git a/extensions/pagetop-seaorm/Cargo.toml b/extensions/pagetop-seaorm/Cargo.toml index 404bb84b..66034137 100644 --- a/extensions/pagetop-seaorm/Cargo.toml +++ b/extensions/pagetop-seaorm/Cargo.toml @@ -28,9 +28,9 @@ futures = "0.3" url = "2.5" [dependencies.sea-orm] -version = "~1.0" +version = "1.1" features = ["debug-print", "macros", "runtime-async-std-native-tls"] default-features = false [dependencies.sea-schema] -version = "~0.15" +version = "0.16" diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index a687828f..0a1de4f5 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -42,7 +42,7 @@ Para MySQL o PostgreSQL añade también `db_user`, `db_pass`, `db_host` y `db_po **Declara la extensión** en tu aplicación o en la extensión que la requiera: -```rust,no_run +```rust,ignore use pagetop::prelude::*; struct MyApp; @@ -70,7 +70,6 @@ async fn main() -> std::io::Result<()> { ```rust,no_run use pagetop_seaorm::db::*; -#[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] @@ -111,27 +110,22 @@ Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/S usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas, columnas, índices y claves externas). -También incorpora código adaptado de las siguientes fuentes: +El módulo de migraciones (`src/db/migration/`) incorpora una adaptación de +[sea-orm-migration](https://crates.io/crates/sea-orm-migration). El código que se integra procede de +la versión [**1.1.20**](https://github.com/SeaQL/sea-orm/tree/1.1.20/sea-orm-migration) en lugar de +usarlo como dependencia ya que su paradigma de CLI no es compatible con el ciclo de vida de las +extensiones de PageTop, donde las migraciones deben ejecutarse durante la inicialización de cada +extensión. Los ficheros adaptados del original son: -* [**sea-orm-migration (v1.0.0)**](https://github.com/SeaQL/sea-orm/tree/1.0.0/sea-orm-migration): - El módulo de migraciones (`src/db/migration/`) es una versión personalizada de - [sea-orm-migration](https://crates.io/crates/sea-orm-migration). Se integra directamente en lugar - de usarlo como dependencia porque su paradigma de CLI no es compatible con el ciclo de vida de las - extensiones de PageTop, donde las migraciones deben ejecutarse durante la inicialización de cada - extensión. Los ficheros adaptados del original son: - - | Original en `sea-orm-migration` | Observaciones | - |---------------------------------|-----------------------------------------| - | `lib.rs` | Excluye módulos y exportaciones del CLI | - | `connection.rs` | Integración completa | - | `manager.rs` | Integración completa | - | `migrator.rs` | Omite la gestión de errores del CLI | - | `prelude.rs` | Excluye exportaciones del CLI | - | `seaql_migrations.rs` | Integración completa | - -* [**loco-rs/loco**](https://github.com/loco-rs/loco/blob/master/src/schema.rs): El módulo - `src/db/migration/schema.rs`, con funciones de ayuda para definir columnas de tablas de forma - ergonómica, está adaptado del fichero `src/schema.rs` del proyecto [Loco](https://loco.rs/). +| Archivos | Observaciones | +|----------------------------|--------------------------------------------------------------| +| `lib.rs` en `migration.rs` | Excluye módulos y exportaciones del CLI | +| `connection.rs` | Integración completa | +| `manager.rs` | Adapta *features* propias | +| `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | +| `prelude.rs` | Excluye exportaciones del CLI | +| `schema.rs` | Integración ajustada con cambios menores | +| `seaql_migrations.rs` | Integración completa | ## 🚧 Advertencia diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index 70b8e57c..d1188a35 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -11,17 +11,7 @@ use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; mod dbconn; pub(crate) use dbconn::{run_now, DBCONN}; -// The migration module is a customized version of the sea_orm_migration module (v1.0.0) -// https://github.com/SeaQL/sea-orm/tree/1.0.0/sea-orm-migration to avoid errors caused by the -// package paradigm of PageTop. Files integrated from original: -// -// lib.rs => db/migration.rs . . . . . . . . . . . . . . (excluding some modules and exports) -// connection.rs => db/migration/connection.rs . . . . . . . . . . . . . . (full integration) -// manager.rs => db/migration/manager.rs . . . . . . . . . . . . . . . . . (full integration) -// migrator.rs => db/migration/migrator.rs . . . . . . . . . . . .(omitting error management) -// prelude.rs => db/migration/prelude.rs . . . . . . . . . . . . . . . . . . . (avoiding CLI) -// seaql_migrations.rs => db/migration/seaql_migrations.rs . . . . . . . . (full integration) -// +// Adaptación de `sea-orm-migration` (ver §Créditos en README.md). mod migration; pub use migration::prelude::*; pub use migration::schema::*; diff --git a/extensions/pagetop-seaorm/src/db/migration/connection.rs b/extensions/pagetop-seaorm/src/db/migration/connection.rs index 116185e4..7c937a37 100644 --- a/extensions/pagetop-seaorm/src/db/migration/connection.rs +++ b/extensions/pagetop-seaorm/src/db/migration/connection.rs @@ -11,7 +11,7 @@ pub enum SchemaManagerConnection<'c> { } #[async_trait::async_trait] -impl<'c> ConnectionTrait for SchemaManagerConnection<'c> { +impl ConnectionTrait for SchemaManagerConnection<'_> { fn get_database_backend(&self) -> DbBackend { match self { SchemaManagerConnection::Connection(conn) => conn.get_database_backend(), @@ -56,7 +56,7 @@ impl<'c> ConnectionTrait for SchemaManagerConnection<'c> { } #[async_trait::async_trait] -impl<'c> TransactionTrait for SchemaManagerConnection<'c> { +impl TransactionTrait for SchemaManagerConnection<'_> { async fn begin(&self) -> Result { match self { SchemaManagerConnection::Connection(conn) => conn.begin().await, @@ -86,7 +86,7 @@ impl<'c> TransactionTrait for SchemaManagerConnection<'c> { ) -> Pin> + Send + 'a>> + Send, T: Send, - E: std::error::Error + Send, + E: std::fmt::Display + std::fmt::Debug + Send, { match self { SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await, @@ -106,7 +106,7 @@ impl<'c> TransactionTrait for SchemaManagerConnection<'c> { ) -> Pin> + Send + 'a>> + Send, T: Send, - E: std::error::Error + Send, + E: std::fmt::Display + std::fmt::Debug + Send, { match self { SchemaManagerConnection::Connection(conn) => { diff --git a/extensions/pagetop-seaorm/src/db/migration/manager.rs b/extensions/pagetop-seaorm/src/db/migration/manager.rs index d1cc3b6a..3f962bdf 100644 --- a/extensions/pagetop-seaorm/src/db/migration/manager.rs +++ b/extensions/pagetop-seaorm/src/db/migration/manager.rs @@ -2,11 +2,12 @@ use super::{IntoSchemaManagerConnection, SchemaManagerConnection}; use sea_orm::sea_query::{ extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, - TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, - TableTruncateStatement, + SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement, + TableRenameStatement, TableTruncateStatement, }; use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder}; -use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; +#[allow(unused_imports)] +use sea_schema::probe::SchemaProbe; /// Helper struct for writing migration scripts in migration file pub struct SchemaManager<'c> { @@ -41,7 +42,7 @@ impl<'c> SchemaManager<'c> { } /// Schema Creation -impl<'c> SchemaManager<'c> { +impl SchemaManager<'_> { pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> { self.exec_stmt(stmt).await } @@ -60,7 +61,7 @@ impl<'c> SchemaManager<'c> { } /// Schema Mutation -impl<'c> SchemaManager<'c> { +impl SchemaManager<'_> { pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> { self.exec_stmt(stmt).await } @@ -95,7 +96,7 @@ impl<'c> SchemaManager<'c> { } /// Schema Inspection. -impl<'c> SchemaManager<'c> { +impl SchemaManager<'_> { pub async fn has_table(&self, table: T) -> Result where T: AsRef, @@ -103,42 +104,54 @@ impl<'c> SchemaManager<'c> { has_table(&self.conn, table).await } - pub async fn has_column(&self, table: T, column: C) -> Result + pub async fn has_column(&self, _table: T, _column: C) -> Result where T: AsRef, C: AsRef, { - let stmt = match self.conn.get_database_backend() { - DbBackend::MySql => MySql.has_column(table, column), - DbBackend::Postgres => Postgres.has_column(table, column), - DbBackend::Sqlite => Sqlite.has_column(table, column), + let _stmt: SelectStatement = match self.conn.get_database_backend() { + #[cfg(feature = "mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.has_column(_table, _column), + #[cfg(feature = "postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.has_column(_table, _column), + #[cfg(feature = "sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_column(_table, _column), + #[allow(unreachable_patterns)] + other => panic!("{other:?} feature is off"), }; + #[allow(unreachable_code)] let builder = self.conn.get_database_backend(); let res = self .conn - .query_one(builder.build(&stmt)) + .query_one(builder.build(&_stmt)) .await? .ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?; res.try_get("", "has_column") } - pub async fn has_index(&self, table: T, index: I) -> Result + pub async fn has_index(&self, _table: T, _index: I) -> Result where T: AsRef, I: AsRef, { - let stmt = match self.conn.get_database_backend() { - DbBackend::MySql => MySql.has_index(table, index), - DbBackend::Postgres => Postgres.has_index(table, index), - DbBackend::Sqlite => Sqlite.has_index(table, index), + let _stmt: SelectStatement = match self.conn.get_database_backend() { + #[cfg(feature = "mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.has_index(_table, _index), + #[cfg(feature = "postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.has_index(_table, _index), + #[cfg(feature = "sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_index(_table, _index), + #[allow(unreachable_patterns)] + other => panic!("{other:?} feature is off"), }; + #[allow(unreachable_code)] let builder = self.conn.get_database_backend(); let res = self .conn - .query_one(builder.build(&stmt)) + .query_one(builder.build(&_stmt)) .await? .ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?; @@ -146,20 +159,26 @@ impl<'c> SchemaManager<'c> { } } -pub(crate) async fn has_table(conn: &C, table: T) -> Result +pub(crate) async fn has_table(conn: &C, _table: T) -> Result where C: ConnectionTrait, T: AsRef, { - let stmt = match conn.get_database_backend() { - DbBackend::MySql => MySql.has_table(table), - DbBackend::Postgres => Postgres.has_table(table), - DbBackend::Sqlite => Sqlite.has_table(table), + let _stmt: SelectStatement = match conn.get_database_backend() { + #[cfg(feature = "mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.has_table(_table), + #[cfg(feature = "postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.has_table(_table), + #[cfg(feature = "sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_table(_table), + #[allow(unreachable_patterns)] + other => panic!("{other:?} feature is off"), }; + #[allow(unreachable_code)] let builder = conn.get_database_backend(); let res = conn - .query_one(builder.build(&stmt)) + .query_one(builder.build(&_stmt)) .await? .ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?; diff --git a/extensions/pagetop-seaorm/src/db/migration/migrator.rs b/extensions/pagetop-seaorm/src/db/migration/migrator.rs index 06611412..45ecdbac 100644 --- a/extensions/pagetop-seaorm/src/db/migration/migrator.rs +++ b/extensions/pagetop-seaorm/src/db/migration/migrator.rs @@ -7,7 +7,7 @@ use std::time::SystemTime; use pagetop::trace::info; use sea_orm::sea_query::{ - self, extension::postgres::Type, Alias, Expr, ForeignKey, IntoIden, JoinType, Order, Query, + self, extension::postgres::Type, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, SelectStatement, SimpleExpr, Table, }; use sea_orm::{ @@ -15,7 +15,8 @@ use sea_orm::{ DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement, TransactionTrait, }; -use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; +#[allow(unused_imports)] +use sea_schema::probe::SchemaProbe; use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager}; @@ -445,9 +446,14 @@ where C: ConnectionTrait, { match db.get_database_backend() { - DbBackend::MySql => MySql.query_tables(), - DbBackend::Postgres => Postgres.query_tables(), - DbBackend::Sqlite => Sqlite.query_tables(), + #[cfg(feature = "mysql")] + DbBackend::MySql => sea_schema::mysql::MySql.query_tables(), + #[cfg(feature = "postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres.query_tables(), + #[cfg(feature = "sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite.query_tables(), + #[allow(unreachable_patterns)] + other => panic!("{other:?} feature is off"), } } @@ -456,9 +462,14 @@ where C: ConnectionTrait, { match db.get_database_backend() { - DbBackend::MySql => MySql::get_current_schema(), - DbBackend::Postgres => Postgres::get_current_schema(), - DbBackend::Sqlite => unimplemented!(), + #[cfg(feature = "mysql")] + DbBackend::MySql => sea_schema::mysql::MySql::get_current_schema(), + #[cfg(feature = "postgres")] + DbBackend::Postgres => sea_schema::postgres::Postgres::get_current_schema(), + #[cfg(feature = "sqlite")] + DbBackend::Sqlite => sea_schema::sqlite::Sqlite::get_current_schema(), + #[allow(unreachable_patterns)] + other => panic!("{other:?} feature is off"), } } @@ -490,7 +501,7 @@ where )) .cond_where( Condition::all() - .add(Expr::expr(get_current_schema(db)).equals(( + .add(get_current_schema(db).equals(( InformationSchema::TableConstraints, InformationSchema::TableSchema, ))) @@ -508,11 +519,20 @@ where #[derive(DeriveIden)] enum PgType { Table, + Oid, Typname, Typnamespace, Typelem, } +#[derive(DeriveIden)] +enum PgDepend { + Table, + Objid, + Deptype, + Refclassid, +} + #[derive(DeriveIden)] enum PgNamespace { Table, @@ -524,24 +544,28 @@ fn query_pg_types(db: &C) -> SelectStatement where C: ConnectionTrait, { - let mut stmt = Query::select(); - stmt.column(PgType::Typname) + Query::select() + .column(PgType::Typname) .from(PgType::Table) - .join( - JoinType::LeftJoin, + .left_join( PgNamespace::Table, Expr::col((PgNamespace::Table, PgNamespace::Oid)) .equals((PgType::Table, PgType::Typnamespace)), ) - .cond_where( - Condition::all() - .add( - Expr::expr(get_current_schema(db)) - .equals((PgNamespace::Table, PgNamespace::Nspname)), + .left_join( + PgDepend::Table, + Expr::col((PgDepend::Table, PgDepend::Objid)) + .equals((PgType::Table, PgType::Oid)) + .and( + Expr::col((PgDepend::Table, PgDepend::Refclassid)) + .eq(Expr::cust("'pg_extension'::regclass::oid")), ) - .add(Expr::col((PgType::Table, PgType::Typelem)).eq(0)), - ); - stmt + .and(Expr::col((PgDepend::Table, PgDepend::Deptype)).eq(Expr::cust("'e'"))), + ) + .and_where(get_current_schema(db).equals((PgNamespace::Table, PgNamespace::Nspname))) + .and_where(Expr::col((PgType::Table, PgType::Typelem)).eq(0)) + .and_where(Expr::col((PgDepend::Table, PgDepend::Objid)).is_null()) + .take() } trait QueryTable { diff --git a/extensions/pagetop-seaorm/src/db/migration/prelude.rs b/extensions/pagetop-seaorm/src/db/migration/prelude.rs index 5556a094..e2389faa 100644 --- a/extensions/pagetop-seaorm/src/db/migration/prelude.rs +++ b/extensions/pagetop-seaorm/src/db/migration/prelude.rs @@ -10,4 +10,3 @@ pub use sea_orm; pub use sea_orm::sea_query; pub use sea_orm::sea_query::*; pub use sea_orm::DeriveIden; -pub use sea_orm::DeriveMigrationName; diff --git a/extensions/pagetop-seaorm/src/db/migration/schema.rs b/extensions/pagetop-seaorm/src/db/migration/schema.rs index b6f89d3a..90910277 100644 --- a/extensions/pagetop-seaorm/src/db/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/db/migration/schema.rs @@ -73,6 +73,11 @@ pub fn pk_auto(name: T) -> ColumnDef { integer(name).auto_increment().primary_key().take() } +/// Create a UUID primary key +pub fn pk_uuid(name: T) -> ColumnDef { + uuid(name).primary_key().take() +} + pub fn char_len(col: T, length: u32) -> ColumnDef { ColumnDef::new(col).char_len(length).not_null().take() } @@ -538,11 +543,11 @@ pub fn uuid_uniq(col: T) -> ColumnDef { uuid(col).unique_key().take() } -pub fn custom(col: T, name: T) -> ColumnDef { +pub fn custom(col: T, name: N) -> ColumnDef { ColumnDef::new(col).custom(name).not_null().take() } -pub fn custom_null(col: T, name: T) -> ColumnDef { +pub fn custom_null(col: T, name: N) -> ColumnDef { ColumnDef::new(col).custom(name).null().take() } From 796ae5ce811918c8648a754ad2741b3788bf3208 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 11 May 2026 15:10:49 +0200 Subject: [PATCH 11/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(seaorm):=20Revisa?= =?UTF-8?q?=20y=20mejora=20la=20API=20p=C3=BAblica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-seaorm/README.md | 21 +++--- extensions/pagetop-seaorm/src/config.rs | 6 +- extensions/pagetop-seaorm/src/db.rs | 72 +++++++++++++++++-- extensions/pagetop-seaorm/src/db/dbconn.rs | 17 ++--- extensions/pagetop-seaorm/src/db/migration.rs | 4 +- extensions/pagetop-seaorm/src/lib.rs | 3 +- 6 files changed, 92 insertions(+), 31 deletions(-) diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index 0a1de4f5..fff02d6d 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -38,7 +38,8 @@ db_name = "my_app.db" max_pool_size = 5 ``` -Para MySQL o PostgreSQL añade también `db_user`, `db_pass`, `db_host` y `db_port`. +Para MySQL o PostgreSQL añade también `db_user`, `db_pass` y `db_host`. El campo `db_port` es +opcional; si se omite se usa el puerto predeterminado del motor. **Declara la extensión** en tu aplicación o en la extensión que la requiera: @@ -117,15 +118,15 @@ usarlo como dependencia ya que su paradigma de CLI no es compatible con el ciclo extensiones de PageTop, donde las migraciones deben ejecutarse durante la inicialización de cada extensión. Los ficheros adaptados del original son: -| Archivos | Observaciones | -|----------------------------|--------------------------------------------------------------| -| `lib.rs` en `migration.rs` | Excluye módulos y exportaciones del CLI | -| `connection.rs` | Integración completa | -| `manager.rs` | Adapta *features* propias | -| `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | -| `prelude.rs` | Excluye exportaciones del CLI | -| `schema.rs` | Integración ajustada con cambios menores | -| `seaql_migrations.rs` | Integración completa | +| Archivos | Observaciones | +|-----------------------|--------------------------------------------------------------------------| +| `lib.rs` | Incluido en `migration.rs`, descarta módulos y exportaciones del CLI | +| `connection.rs` | Integración completa | +| `manager.rs` | Adapta *features* propias | +| `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | +| `prelude.rs` | Excluye exportaciones del CLI | +| `schema.rs` | Integra con ajustes, original de [loco](https://github.com/loco-rs/loco) | +| `seaql_migrations.rs` | Integración completa | ## 🚧 Advertencia diff --git a/extensions/pagetop-seaorm/src/config.rs b/extensions/pagetop-seaorm/src/config.rs index bec565b3..b1276c50 100644 --- a/extensions/pagetop-seaorm/src/config.rs +++ b/extensions/pagetop-seaorm/src/config.rs @@ -34,7 +34,6 @@ include_config!(SETTINGS: Settings => [ "database.db_user" => "", "database.db_pass" => "", "database.db_host" => "localhost", - "database.db_port" => 0, "database.max_pool_size" => 5, ]); @@ -57,8 +56,9 @@ pub struct Database { pub db_pass: String, /// Servidor de conexión a la base de datos (para mysql/postgres). pub db_host: String, - /// Puerto de conexión a la base de datos, normalmente 3306 (para mysql) ó 5432 (para postgres). - pub db_port: u16, + /// Puerto de conexión a la base de datos (para mysql/postgres). Si es `None` se usa el puerto + /// predeterminado para el motor: 3306 para MySQL y 5432 para PostgreSQL. + pub db_port: Option, /// Número máximo de conexiones habilitadas. pub max_pool_size: u32, } diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index d1188a35..f954d9ce 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -1,7 +1,7 @@ use pagetop::core::TypeInfo; use pagetop::trace; -pub use url::Url as DbUri; +pub(crate) use url::Url as DbUri; pub use sea_orm::error::{DbErr, RuntimeErr}; pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult}; @@ -16,7 +16,30 @@ mod migration; pub use migration::prelude::*; pub use migration::schema::*; -pub async fn query(stmt: &mut Q) -> Result, DbErr> { +/// Ejecuta una consulta para devolver todas las filas resultantes. +/// +/// Acepta cualquier tipo que implemente [`QueryStatementWriter`] (p. ej. [`SelectStatement`]) y +/// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada +/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con +/// [`QueryResult::try_get`]. +/// +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// +/// async fn example() -> Result<(), DbErr> { +/// let mut stmt = Query::select() +/// .column(Asterisk) +/// .from(Alias::new("users")) +/// .to_owned(); +/// let rows = fetch_all(&mut stmt).await?; +/// for row in rows { +/// let name: String = row.try_get("", "name")?; +/// println!("{name}"); +/// } +/// Ok(()) +/// } +/// ``` +pub async fn fetch_all(stmt: &mut Q) -> Result, DbErr> { let dbconn = &*DBCONN; let dbbackend = dbconn.get_database_backend(); dbconn @@ -31,7 +54,30 @@ pub async fn query(stmt: &mut Q) -> Result(stmt: &mut Q) -> Result, DbErr> { +/// Ejecuta una consulta y devuelve sólo la primera fila, si existe. +/// +/// Funciona igual que [`fetch_all`] pero detiene la ejecución tras la primera fila y devuelve +/// `None` si la consulta no produce resultados. +/// +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// +/// async fn example() -> Result<(), DbErr> { +/// let mut stmt = Query::select() +/// .column(Asterisk) +/// .from(Alias::new("users")) +/// .and_where(Expr::col(Alias::new("id")).eq(1)) +/// .to_owned(); +/// if let Some(row) = fetch_one(&mut stmt).await? { +/// let name: String = row.try_get("", "name")?; +/// println!("{name}"); +/// } +/// Ok(()) +/// } +/// ``` +pub async fn fetch_one( + stmt: &mut Q, +) -> Result, DbErr> { let dbconn = &*DBCONN; let dbbackend = dbconn.get_database_backend(); dbconn @@ -46,11 +92,27 @@ pub async fn exec(stmt: &mut Q) -> Result Result { +/// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de +/// la operación. +/// +/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe como +/// cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El +/// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado. +/// +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// +/// async fn example() -> Result<(), DbErr> { +/// let result = execute("DELETE FROM sessions WHERE expired = 1").await?; +/// println!("Filas eliminadas: {}", result.rows_affected()); +/// Ok(()) +/// } +/// ``` +pub async fn execute(stmt: impl Into) -> Result { let dbconn = &*DBCONN; let dbbackend = dbconn.get_database_backend(); dbconn - .execute(Statement::from_string(dbbackend, stmt)) + .execute(Statement::from_string(dbbackend, stmt.into())) .await } diff --git a/extensions/pagetop-seaorm/src/db/dbconn.rs b/extensions/pagetop-seaorm/src/db/dbconn.rs index bd227956..e4881c08 100644 --- a/extensions/pagetop-seaorm/src/db/dbconn.rs +++ b/extensions/pagetop-seaorm/src/db/dbconn.rs @@ -35,10 +35,8 @@ pub static DBCONN: LazyLock = LazyLock::new(|| { tmp_uri .set_password(Some(config::SETTINGS.database.db_pass.as_str())) .unwrap(); - if config::SETTINGS.database.db_port != 0 { - tmp_uri - .set_port(Some(config::SETTINGS.database.db_port)) - .unwrap(); + if let Some(port) = config::SETTINGS.database.db_port { + tmp_uri.set_port(Some(port)).unwrap(); } tmp_uri } @@ -51,13 +49,10 @@ pub static DBCONN: LazyLock = LazyLock::new(|| { .as_str(), ) .unwrap(), - _ => { - trace::error!( - "Unrecognized database type \"{}\"", - &config::SETTINGS.database.db_type - ); - DbUri::parse("").unwrap() - } + _ => panic!( + "Unrecognized database type \"{}\"", + config::SETTINGS.database.db_type + ), }; run_now(Database::connect::({ diff --git a/extensions/pagetop-seaorm/src/db/migration.rs b/extensions/pagetop-seaorm/src/db/migration.rs index 29314bf6..0c32c10a 100644 --- a/extensions/pagetop-seaorm/src/db/migration.rs +++ b/extensions/pagetop-seaorm/src/db/migration.rs @@ -28,6 +28,8 @@ pub trait MigrationTrait: MigrationName + Send + Sync { /// Define actions to perform when rolling back the migration async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { - Err(DbErr::Migration("We Don't Do That Here".to_owned())) + Err(DbErr::Migration( + "Rollback not implemented for this migration".to_owned(), + )) } } diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index 103d95eb..0b686cb2 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -39,7 +39,8 @@ db_name = "my_app.db" max_pool_size = 5 ``` -Para MySQL o PostgreSQL añade también `db_user`, `db_pass`, `db_host` y `db_port`. +Para MySQL o PostgreSQL añade también `db_user`, `db_pass` y `db_host`. El campo `db_port` es +opcional; si se omite se usa el puerto predeterminado del motor. **Declara la extensión** en tu aplicación o en la extensión que la requiera: From 026448e51108ef76d077c3d81ea7dbf5e7c2fc06 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 15 May 2026 00:22:55 +0200 Subject: [PATCH 12/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(seaorm):=20Separa?= =?UTF-8?q?=20m=C3=B3dulo=20`migration`=20de=20`db`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `db::*` queda como API de consultas (connection, fetch_*). - `migration::*` sube a primer nivel con su propia documentación. - `DBCONN` y `run_now` se trasladan a la raíz de la extensión. - Actualiza README.md y docs para reflejar la nueva estructura. --- extensions/pagetop-seaorm/README.md | 6 +- extensions/pagetop-seaorm/src/db.rs | 130 +++++---------- extensions/pagetop-seaorm/src/db/dbconn.rs | 64 ------- extensions/pagetop-seaorm/src/db/migration.rs | 35 ---- .../src/db/migration/prelude.rs | 12 -- extensions/pagetop-seaorm/src/lib.rs | 67 +++++++- extensions/pagetop-seaorm/src/migration.rs | 156 ++++++++++++++++++ .../src/{db => }/migration/connection.rs | 0 .../src/{db => }/migration/manager.rs | 0 .../src/{db => }/migration/migrator.rs | 0 .../src/{db => }/migration/schema.rs | 26 ++- .../{db => }/migration/seaql_migrations.rs | 0 12 files changed, 279 insertions(+), 217 deletions(-) delete mode 100644 extensions/pagetop-seaorm/src/db/dbconn.rs delete mode 100644 extensions/pagetop-seaorm/src/db/migration.rs delete mode 100644 extensions/pagetop-seaorm/src/db/migration/prelude.rs create mode 100644 extensions/pagetop-seaorm/src/migration.rs rename extensions/pagetop-seaorm/src/{db => }/migration/connection.rs (100%) rename extensions/pagetop-seaorm/src/{db => }/migration/manager.rs (100%) rename extensions/pagetop-seaorm/src/{db => }/migration/migrator.rs (100%) rename extensions/pagetop-seaorm/src/{db => }/migration/schema.rs (96%) rename extensions/pagetop-seaorm/src/{db => }/migration/seaql_migrations.rs (100%) diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index fff02d6d..42738cd0 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -69,7 +69,7 @@ async fn main() -> std::io::Result<()> { **Escribe las migraciones** usando la API de SeaORM: ```rust,no_run -use pagetop_seaorm::db::*; +use pagetop_seaorm::migration::*; pub struct Migration; @@ -111,7 +111,7 @@ Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/S usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas, columnas, índices y claves externas). -El módulo de migraciones (`src/db/migration/`) incorpora una adaptación de +El módulo de migraciones (`src/migration/`) incorpora una adaptación de [sea-orm-migration](https://crates.io/crates/sea-orm-migration). El código que se integra procede de la versión [**1.1.20**](https://github.com/SeaQL/sea-orm/tree/1.1.20/sea-orm-migration) en lugar de usarlo como dependencia ya que su paradigma de CLI no es compatible con el ciclo de vida de las @@ -124,7 +124,7 @@ extensión. Los ficheros adaptados del original son: | `connection.rs` | Integración completa | | `manager.rs` | Adapta *features* propias | | `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | -| `prelude.rs` | Excluye exportaciones del CLI | +| `prelude.rs` | Absorbido en `migration.rs`, descarta exportaciones del CLI | | `schema.rs` | Integra con ajustes, original de [loco](https://github.com/loco-rs/loco) | | `seaql_migrations.rs` | Integración completa | diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index f954d9ce..2b47c399 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -1,30 +1,52 @@ -use pagetop::core::TypeInfo; -use pagetop::trace; +//! API completa de SeaORM para operaciones con la base de datos. +//! +//! Re-exporta el *prelude* de SeaORM (entidades, traits, tipos de valor, macros de derivación…) +//! y expone tres funciones de consulta propias. Con una sola importación tienes todo lo necesario +//! para definir entidades y realizar operaciones CRUD: +//! +//! ```rust,ignore +//! use pagetop_seaorm::db::*; +//! ``` +//! +//! Para definir el esquema de la base de datos o escribir migraciones usa además +//! [`crate::migration`]. -pub(crate) use url::Url as DbUri; +pub use sea_orm::prelude::*; -pub use sea_orm::error::{DbErr, RuntimeErr}; -pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult}; +use sea_orm::sea_query::{ + MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder, +}; +use sea_orm::{DatabaseBackend, ExecResult, Statement}; -use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; - -mod dbconn; -pub(crate) use dbconn::{run_now, DBCONN}; - -// Adaptación de `sea-orm-migration` (ver §Créditos en README.md). -mod migration; -pub use migration::prelude::*; -pub use migration::schema::*; +/// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades. +/// +/// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades +/// de SeaORM. El coste de esta llamada es prácticamente nulo: sólo devuelve una referencia a un +/// valor inicializado una sola vez al arrancar la aplicación. +/// +/// ```rust,no_run +/// use pagetop_seaorm::db::*; +/// +/// // Consultas tipadas con el sistema de entidades de SeaORM: +/// // let users = User::find().all(connection()).await?; +/// // let user = User::find_by_id(1).one(connection()).await?; +/// // User::insert(model).exec(connection()).await?; +/// let _conn = connection(); +/// ``` +pub fn connection() -> &'static DatabaseConnection { + &super::DBCONN +} /// Ejecuta una consulta para devolver todas las filas resultantes. /// -/// Acepta cualquier tipo que implemente [`QueryStatementWriter`] (p. ej. [`SelectStatement`]) y +/// Acepta cualquier tipo que implemente [`crate::migration::QueryStatementWriter`] (p. ej. [`crate::migration::SelectStatement`]) y /// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada /// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con /// [`QueryResult::try_get`]. /// /// ```rust,no_run /// use pagetop_seaorm::db::*; +/// use pagetop_seaorm::migration::*; /// /// async fn example() -> Result<(), DbErr> { /// let mut stmt = Query::select() @@ -40,7 +62,7 @@ pub use migration::schema::*; /// } /// ``` pub async fn fetch_all(stmt: &mut Q) -> Result, DbErr> { - let dbconn = &*DBCONN; + let dbconn = &*super::DBCONN; let dbbackend = dbconn.get_database_backend(); dbconn .query_all(Statement::from_string( @@ -61,6 +83,7 @@ pub async fn fetch_all(stmt: &mut Q) -> Result Result<(), DbErr> { /// let mut stmt = Query::select() @@ -78,7 +101,7 @@ pub async fn fetch_all(stmt: &mut Q) -> Result( stmt: &mut Q, ) -> Result, DbErr> { - let dbconn = &*DBCONN; + let dbconn = &*super::DBCONN; let dbbackend = dbconn.get_database_backend(); dbconn .query_one(Statement::from_string( @@ -95,8 +118,8 @@ pub async fn fetch_one( /// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de /// la operación. /// -/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe como -/// cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El +/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe +/// como cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El /// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado. /// /// ```rust,no_run @@ -109,76 +132,9 @@ pub async fn fetch_one( /// } /// ``` pub async fn execute(stmt: impl Into) -> Result { - let dbconn = &*DBCONN; + let dbconn = &*super::DBCONN; let dbbackend = dbconn.get_database_backend(); dbconn .execute(Statement::from_string(dbbackend, stmt.into())) .await } - -pub trait MigratorBase { - fn run_up(); - - fn run_down(); -} - -#[rustfmt::skip] -impl MigratorBase for M { - fn run_up() { - if let Err(e) = run_now(Self::up(SchemaManagerConnection::Connection(&DBCONN), None)) { - trace::error!("Migration upgrade failed ({})", e); - }; - } - - fn run_down() { - if let Err(e) = run_now(Self::down(SchemaManagerConnection::Connection(&DBCONN), None)) { - trace::error!("Migration downgrade failed ({})", e); - }; - } -} - -impl MigrationName for M { - fn name(&self) -> &str { - TypeInfo::NameTo(-2).of::() - } -} - -pub type MigrationItem = Box; - -#[macro_export] -macro_rules! install_migrations { - ( $($migration_module:ident),+ $(,)? ) => {{ - use $crate::db::{MigrationItem, MigratorBase, MigratorTrait}; - - struct Migrator; - impl MigratorTrait for Migrator { - fn migrations() -> Vec { - let mut m = Vec::::new(); - $( - m.push(Box::new(migration::$migration_module::Migration)); - )* - m - } - } - Migrator::run_up(); - }}; -} - -#[macro_export] -macro_rules! uninstall_migrations { - ( $($migration_module:ident),+ $(,)? ) => {{ - use $crate::db::{MigrationItem, MigratorBase, MigratorTrait}; - - struct Migrator; - impl MigratorTrait for Migrator { - fn migrations() -> Vec { - let mut m = Vec::::new(); - $( - m.push(Box::new(migration::$migration_module::Migration)); - )* - m - } - } - Migrator::run_down(); - }}; -} diff --git a/extensions/pagetop-seaorm/src/db/dbconn.rs b/extensions/pagetop-seaorm/src/db/dbconn.rs deleted file mode 100644 index e4881c08..00000000 --- a/extensions/pagetop-seaorm/src/db/dbconn.rs +++ /dev/null @@ -1,64 +0,0 @@ -use pagetop::trace; - -use crate::config; -use crate::db::{DbConn, DbUri}; - -use std::sync::LazyLock; - -use sea_orm::{ConnectOptions, Database}; - -pub use futures::executor::block_on as run_now; - -pub static DBCONN: LazyLock = LazyLock::new(|| { - trace::info!( - "Connecting to database \"{}\" using a pool of {} connections", - &config::SETTINGS.database.db_name, - &config::SETTINGS.database.max_pool_size - ); - - let db_uri = match config::SETTINGS.database.db_type.as_str() { - "mysql" | "postgres" => { - let mut tmp_uri = DbUri::parse( - format!( - "{}://{}/{}", - &config::SETTINGS.database.db_type, - &config::SETTINGS.database.db_host, - &config::SETTINGS.database.db_name - ) - .as_str(), - ) - .unwrap(); - tmp_uri - .set_username(config::SETTINGS.database.db_user.as_str()) - .unwrap(); - // https://github.com/launchbadge/sqlx/issues/1624 - tmp_uri - .set_password(Some(config::SETTINGS.database.db_pass.as_str())) - .unwrap(); - if let Some(port) = config::SETTINGS.database.db_port { - tmp_uri.set_port(Some(port)).unwrap(); - } - tmp_uri - } - "sqlite" => DbUri::parse( - format!( - "{}://{}", - &config::SETTINGS.database.db_type, - &config::SETTINGS.database.db_name - ) - .as_str(), - ) - .unwrap(), - _ => panic!( - "Unrecognized database type \"{}\"", - config::SETTINGS.database.db_type - ), - }; - - run_now(Database::connect::({ - let mut db_opt = ConnectOptions::new(db_uri.to_string()); - db_opt.max_connections(config::SETTINGS.database.max_pool_size); - db_opt - })) - .unwrap_or_else(|_| panic!("Failed to connect to database")) -}); diff --git a/extensions/pagetop-seaorm/src/db/migration.rs b/extensions/pagetop-seaorm/src/db/migration.rs deleted file mode 100644 index 0c32c10a..00000000 --- a/extensions/pagetop-seaorm/src/db/migration.rs +++ /dev/null @@ -1,35 +0,0 @@ -//pub mod cli; -pub mod connection; -pub mod manager; -pub mod migrator; -pub mod prelude; -pub mod schema; -pub mod seaql_migrations; -//pub mod util; - -pub use connection::*; -pub use manager::*; -//pub use migrator::*; - -pub use async_trait; -//pub use sea_orm; -//pub use sea_orm::sea_query; -use sea_orm::DbErr; - -pub trait MigrationName { - fn name(&self) -> &str; -} - -/// The migration definition -#[async_trait::async_trait] -pub trait MigrationTrait: MigrationName + Send + Sync { - /// Define actions to perform when applying the migration - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>; - - /// Define actions to perform when rolling back the migration - async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { - Err(DbErr::Migration( - "Rollback not implemented for this migration".to_owned(), - )) - } -} diff --git a/extensions/pagetop-seaorm/src/db/migration/prelude.rs b/extensions/pagetop-seaorm/src/db/migration/prelude.rs deleted file mode 100644 index e2389faa..00000000 --- a/extensions/pagetop-seaorm/src/db/migration/prelude.rs +++ /dev/null @@ -1,12 +0,0 @@ -//pub use super::cli; - -pub use super::connection::IntoSchemaManagerConnection; -pub use super::connection::SchemaManagerConnection; -pub use super::manager::SchemaManager; -pub use super::migrator::MigratorTrait; -pub use super::{MigrationName, MigrationTrait}; -pub use async_trait; -pub use sea_orm; -pub use sea_orm::sea_query; -pub use sea_orm::sea_query::*; -pub use sea_orm::DeriveIden; diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index 0b686cb2..8af917ab 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -70,7 +70,7 @@ async fn main() -> std::io::Result<()> { **Escribe las migraciones** usando la API de SeaORM: ```rust,no_run -use pagetop_seaorm::db::*; +use pagetop_seaorm::migration::*; pub struct Migration; @@ -103,12 +103,75 @@ enum Users { use pagetop::prelude::*; +use sea_orm::{ConnectOptions, Database, DatabaseConnection}; +use url::Url; + +use std::sync::LazyLock; + include_locales!(LOCALES_SEAORM); pub mod config; pub mod db; +pub mod migration; + +pub(crate) use futures::executor::block_on as run_now; + +pub(crate) static DBCONN: LazyLock = LazyLock::new(|| { + trace::info!( + "Connecting to database \"{}\" using a pool of {} connections", + &config::SETTINGS.database.db_name, + &config::SETTINGS.database.max_pool_size + ); + + let db_uri = match config::SETTINGS.database.db_type.as_str() { + "mysql" | "postgres" => { + let mut tmp_uri = Url::parse( + format!( + "{}://{}/{}", + &config::SETTINGS.database.db_type, + &config::SETTINGS.database.db_host, + &config::SETTINGS.database.db_name + ) + .as_str(), + ) + .unwrap(); + tmp_uri + .set_username(config::SETTINGS.database.db_user.as_str()) + .unwrap(); + // https://github.com/launchbadge/sqlx/issues/1624 + tmp_uri + .set_password(Some(config::SETTINGS.database.db_pass.as_str())) + .unwrap(); + if let Some(port) = config::SETTINGS.database.db_port { + tmp_uri.set_port(Some(port)).unwrap(); + } + tmp_uri + } + "sqlite" => Url::parse( + format!( + "{}://{}", + &config::SETTINGS.database.db_type, + &config::SETTINGS.database.db_name + ) + .as_str(), + ) + .unwrap(), + _ => panic!( + "Unrecognized database type \"{}\"", + config::SETTINGS.database.db_type + ), + }; + + run_now(Database::connect::({ + let mut db_opt = ConnectOptions::new(db_uri.to_string()); + db_opt.max_connections(config::SETTINGS.database.max_pool_size); + db_opt + })) + .unwrap_or_else(|_| panic!("Failed to connect to database")) +}); + /// Implementa la extensión. pub struct SeaORM; @@ -122,6 +185,6 @@ impl Extension for SeaORM { } fn initialize(&self) { - std::sync::LazyLock::force(&db::DBCONN); + std::sync::LazyLock::force(&DBCONN); } } diff --git a/extensions/pagetop-seaorm/src/migration.rs b/extensions/pagetop-seaorm/src/migration.rs new file mode 100644 index 00000000..14b8f85b --- /dev/null +++ b/extensions/pagetop-seaorm/src/migration.rs @@ -0,0 +1,156 @@ +//! API para definir y ejecutar migraciones de base de datos. +//! +//! Re-exporta los tipos de SeaORM necesarios para escribir migraciones y ofrece las macros +//! [`crate::install_migrations`] y [`crate::uninstall_migrations`] para aplicarlas o revertirlas al +//! arrancar la extensión. +//! +//! ```rust,ignore +//! use pagetop_seaorm::db::*; +//! use pagetop_seaorm::migration::*; +//! ``` + +// **< Adaptación de `sea-orm-migration` (ver §Créditos en README.md) >***************************** + +//pub mod cli; +pub mod connection; +pub mod manager; +pub mod migrator; +//pub mod prelude; +pub mod schema; +pub mod seaql_migrations; +//pub mod util; + +pub use connection::*; +pub use manager::*; +//pub use migrator::*; + +pub use async_trait; +//pub use sea_orm; +//pub use sea_orm::sea_query; +pub use sea_orm::DbErr; + +pub trait MigrationName { + fn name(&self) -> &str; +} + +/// The migration definition +#[async_trait::async_trait] +pub trait MigrationTrait: MigrationName + Send + Sync { + /// Define actions to perform when applying the migration + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>; + + /// Define actions to perform when rolling back the migration + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Err(DbErr::Migration( + "Rollback not implemented for this migration".to_owned(), + )) + } +} + +// ************************************************************************************************* + +pub use migrator::MigratorTrait; +pub use schema::*; +pub use sea_orm::sea_query::*; +pub use sea_orm::DeriveIden; + +use pagetop::core::TypeInfo; +use pagetop::trace; + +impl MigrationName for M { + fn name(&self) -> &str { + TypeInfo::NameTo(-2).of::() + } +} + +pub type MigrationItem = Box; + +pub trait MigratorBase { + fn run_up(); + + fn run_down(); +} + +#[rustfmt::skip] +impl MigratorBase for M { + fn run_up() { + if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) { + trace::error!("Migration upgrade failed ({})", e); + }; + } + + fn run_down() { + if let Err(e) = super::run_now(Self::down(SchemaManagerConnection::Connection(&super::DBCONN), None)) { + trace::error!("Migration downgrade failed ({})", e); + }; + } +} + +/// Aplica las migraciones pendientes al arrancar una extensión. +/// +/// Recibe uno o más módulos de migración y ejecuta el método `up` de los que aún no estén +/// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde +/// [`Extension::initialize`](pagetop::core::extension::Extension::initialize). +/// +/// ```rust,ignore +/// impl Extension for MyExt { +/// fn initialize(&self) { +/// install_migrations!( +/// m20240101_000001_create_users_table, +/// m20240115_000002_add_email_index, +/// ); +/// } +/// } +/// ``` +#[macro_export] +macro_rules! install_migrations { + ( $($migration_module:ident),+ $(,)? ) => {{ + use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait}; + + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut m = Vec::::new(); + $( + m.push(Box::new(migration::$migration_module::Migration)); + )* + m + } + } + Migrator::run_up(); + }}; +} + +/// Revierte las migraciones de una extensión en orden inverso al de su aplicación. +/// +/// Ejecuta el método `down` de cada migración indicada. Si alguna no implementa `down`, +/// detiene el proceso con un error. Complementario a [`crate::install_migrations`]. +/// +/// ```rust,ignore +/// impl Extension for MyExt { +/// fn uninitialize(&self) { +/// uninstall_migrations!( +/// m20240101_000001_create_users_table, +/// m20240115_000002_add_email_index, +/// ); +/// } +/// } +/// ``` +#[macro_export] +macro_rules! uninstall_migrations { + ( $($migration_module:ident),+ $(,)? ) => {{ + use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait}; + + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut m = Vec::::new(); + $( + m.push(Box::new(migration::$migration_module::Migration)); + )* + m + } + } + Migrator::run_down(); + }}; +} diff --git a/extensions/pagetop-seaorm/src/db/migration/connection.rs b/extensions/pagetop-seaorm/src/migration/connection.rs similarity index 100% rename from extensions/pagetop-seaorm/src/db/migration/connection.rs rename to extensions/pagetop-seaorm/src/migration/connection.rs diff --git a/extensions/pagetop-seaorm/src/db/migration/manager.rs b/extensions/pagetop-seaorm/src/migration/manager.rs similarity index 100% rename from extensions/pagetop-seaorm/src/db/migration/manager.rs rename to extensions/pagetop-seaorm/src/migration/manager.rs diff --git a/extensions/pagetop-seaorm/src/db/migration/migrator.rs b/extensions/pagetop-seaorm/src/migration/migrator.rs similarity index 100% rename from extensions/pagetop-seaorm/src/db/migration/migrator.rs rename to extensions/pagetop-seaorm/src/migration/migrator.rs diff --git a/extensions/pagetop-seaorm/src/db/migration/schema.rs b/extensions/pagetop-seaorm/src/migration/schema.rs similarity index 96% rename from extensions/pagetop-seaorm/src/db/migration/schema.rs rename to extensions/pagetop-seaorm/src/migration/schema.rs index 90910277..4dea5ee2 100644 --- a/extensions/pagetop-seaorm/src/db/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/migration/schema.rs @@ -1,17 +1,16 @@ -//! Adapted from +//! Adaptación de //! -//! # Database Table Schema Helpers +//! # Ayudantes de esquema de base de datos //! -//! This module defines functions and helpers for creating database table -//! schemas using the `sea-orm` and `sea-query` libraries. +//! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`. //! -//! # Example +//! # Ejemplo //! -//! The following example shows how the user migration file should be and using -//! the schema helpers to create the Db fields. +//! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes +//! de esquema. //! //! ```rust -//! use pagetop_seaorm::db::*; +//! use pagetop_seaorm::migration::*; //! //! pub struct Migration; //! @@ -38,7 +37,7 @@ //! } //! } //! -//! #[derive(Iden)] +//! #[derive(DeriveIden)] //! pub enum Users { //! Table, //! Id, @@ -51,10 +50,9 @@ //! } //! ``` -use crate::db::Iden; - use sea_orm::sea_query::{ - self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement, + self, Alias, ColumnDef, ColumnType, Expr, Iden, IntoIden, PgInterval, Table, + TableCreateStatement, }; #[derive(Iden)] @@ -599,7 +597,7 @@ pub fn array_uniq(col: T, elem_type: ColumnType) -> ColumnDef { array(col, elem_type).unique_key().take() } -/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. +/// Añade las columnas de timestamp (`CreatedAt` y `UpdatedAt`) a una tabla existente. pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { let mut t = t; t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) @@ -607,7 +605,7 @@ pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { .take() } -/// Create an Alias. +/// Crea un alias. pub fn name>(name: T) -> Alias { Alias::new(name) } diff --git a/extensions/pagetop-seaorm/src/db/migration/seaql_migrations.rs b/extensions/pagetop-seaorm/src/migration/seaql_migrations.rs similarity index 100% rename from extensions/pagetop-seaorm/src/db/migration/seaql_migrations.rs rename to extensions/pagetop-seaorm/src/migration/seaql_migrations.rs From 9c58d5e1d6d48e0ec931f458afd4c466dc9db4f3 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 30 May 2026 22:30:58 +0200 Subject: [PATCH 13/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(pagetop):=20Migra?= =?UTF-8?q?=20de=20actix-web=20a=20Axum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sustituye el módulo `service` por `web` y adapta toda la API al modelo de Axum: router inmutable, extractores via `FromRequestParts` y servicios Tower para archivos estáticos. - `HttpRequest` pasa a ser un tipo propio, mínimo y clonable. - `configure_services` pasa a `configure_routes`. - `EmbeddedFilesService` pasa a `ServeEmbedded`. - Elimina `session_lifetime` de `Server` (va a `pagetop-auth`). - Actualiza tests y ejemplos a la nueva API. --- Cargo.toml | 157 +++++++----- extensions/pagetop-aliner/Cargo.toml | 5 +- extensions/pagetop-bootsier/Cargo.toml | 5 +- extensions/pagetop-seaorm/Cargo.toml | 19 +- helpers/pagetop-build/Cargo.toml | 4 +- helpers/pagetop-macros/Cargo.toml | 10 +- helpers/pagetop-minimal/Cargo.toml | 8 +- helpers/pagetop-statics/Cargo.toml | 18 +- src/app.rs | 147 +++++------ src/config.rs | 44 ++-- src/core.rs | 40 ++- src/global.rs | 11 +- src/html.rs | 2 +- src/lib.rs | 16 +- src/prelude.rs | 8 +- src/response.rs | 2 - src/service.rs | 128 ---------- src/util.rs | 38 ++- src/web.rs | 340 +++++++++++++++++++++++++ 19 files changed, 612 insertions(+), 390 deletions(-) delete mode 100644 src/service.rs create mode 100644 src/web.rs diff --git a/Cargo.toml b/Cargo.toml index 48bc600c..269f752f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,63 +1,3 @@ -[package] -name = "pagetop" -version = "0.5.0" -edition = "2021" - -description = """ - Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables. -""" -categories = ["web-programming::http-server"] -keywords = ["pagetop", "web", "framework", "frontend", "ssr"] - -repository.workspace = true -homepage.workspace = true -license.workspace = true -authors.workspace = true - -[dependencies] -chrono = "0.4" -colored = "3.1" -config = { version = "0.15", default-features = false, features = ["toml"] } -figlet-rs = "1.0" -getter-methods = "2.0" -itoa = "1.0" -indexmap = "2.14" -parking_lot = "0.12" -substring = "1.4" -terminal_size = "0.4" - -tracing = "0.1" -tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } -tracing-actix-web = "0.7" - -fluent-templates = "0.14" -unic-langid = { version = "0.9", features = ["macros"] } - -actix-web = { workspace = true, default-features = true } -actix-session = { version = "0.11", features = ["cookie-session"] } -actix-web-files = { package = "actix-files", version = "0.6" } - -serde.workspace = true - -pagetop-macros.workspace = true -pagetop-minimal.workspace = true -pagetop-statics.workspace = true - -[features] -default = [] -testing = [] - -[dev-dependencies] -tempfile = "3.27" -serde_json = "1.0" -pagetop-aliner.workspace = true -pagetop-bootsier.workspace = true - -[build-dependencies] -pagetop-build.workspace = true - - [workspace] resolver = "2" members = [ @@ -75,12 +15,45 @@ members = [ [workspace.package] repository = "https://git.cillero.es/manuelcillero/pagetop" homepage = "https://pagetop.cillero.es" +edition = "2024" license = "MIT OR Apache-2.0" authors = ["Manuel Cillero "] [workspace.dependencies] -actix-web = { version = "4.13", default-features = false } +async-trait = "0.1" +axum = { version = "0.8" } +change-detection = "1.2" +chrono = "0.4" +colored = "3.1" +concat-string = "1.0" +config = { version = "0.15", default-features = false, features = ["toml"] } +figlet-rs = "1.0" +fluent-templates = "0.14" +getter-methods = "2.0" +grass = "0.13" +indexmap = "2.14" +indoc = "2.0" +itoa = "1.0" +mime_guess = "2.0" +parking_lot = "0.12" +pastey = "0.2" +path-slash = "0.2" +proc-macro2 = "1.0" +proc-macro2-diagnostics = { version = "0.10", default-features = false } +quote = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +tempfile = "3.27" +terminal_size = "0.4" +tokio = { version = "1", features = ["full"] } +tower = { version = "0.5", features = ["util"] } +tower-http = { version = "0.6", features = ["fs"] } +tracing = "0.1" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } +unic-langid = { version = "0.9", features = ["macros"] } +url = "2.5" # Helpers pagetop-build = { version = "0.3", path = "helpers/pagetop-build" } pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" } @@ -92,3 +65,65 @@ pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" } pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" } # PageTop pagetop = { version = "0.5", path = "." } + +[workspace.dependencies.sea-orm] +version = "1.1" +features = ["debug-print", "macros", "runtime-tokio-native-tls"] +default-features = false + +[workspace.dependencies.sea-schema] +version = "0.16" + + +[package] +name = "pagetop" +version = "0.5.0" + +description = """ + Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables. +""" +categories = ["web-programming::http-server"] +keywords = ["pagetop", "web", "framework", "frontend", "ssr"] + +repository.workspace = true +homepage.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[features] +default = [] +testing = [] + +[dependencies] +axum.workspace = true +chrono.workspace = true +colored.workspace = true +config.workspace = true +figlet-rs.workspace = true +fluent-templates.workspace = true +getter-methods.workspace = true +indexmap.workspace = true +itoa.workspace = true +parking_lot.workspace = true +pagetop-macros.workspace = true +pagetop-minimal.workspace = true +pagetop-statics.workspace = true +serde.workspace = true +terminal_size.workspace = true +tokio.workspace = true +tower.workspace = true +tower-http.workspace = true +tracing.workspace = true +tracing-appender.workspace = true +tracing-subscriber.workspace = true +unic-langid.workspace = true + +[dev-dependencies] +pagetop-aliner.workspace = true +pagetop-bootsier.workspace = true +serde_json.workspace = true +tempfile.workspace = true + +[build-dependencies] +pagetop-build.workspace = true diff --git a/extensions/pagetop-aliner/Cargo.toml b/extensions/pagetop-aliner/Cargo.toml index 00deda3e..d2828b3f 100644 --- a/extensions/pagetop-aliner/Cargo.toml +++ b/extensions/pagetop-aliner/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-aliner" version = "0.1.0" -edition = "2021" description = """ Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML @@ -11,11 +10,15 @@ keywords = ["pagetop", "theme", "css"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true [dependencies] pagetop.workspace = true +[dev-dependencies] +tokio.workspace = true + [build-dependencies] pagetop-build.workspace = true diff --git a/extensions/pagetop-bootsier/Cargo.toml b/extensions/pagetop-bootsier/Cargo.toml index 6e6fc66b..44b6d248 100644 --- a/extensions/pagetop-bootsier/Cargo.toml +++ b/extensions/pagetop-bootsier/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-bootsier" version = "0.1.1" -edition = "2021" description = """ Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles. @@ -11,6 +10,7 @@ keywords = ["pagetop", "theme", "bootstrap", "css", "js"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -18,5 +18,8 @@ authors.workspace = true pagetop.workspace = true serde.workspace = true +[dev-dependencies] +tokio.workspace = true + [build-dependencies] pagetop-build.workspace = true diff --git a/extensions/pagetop-seaorm/Cargo.toml b/extensions/pagetop-seaorm/Cargo.toml index 66034137..6e2b6fc7 100644 --- a/extensions/pagetop-seaorm/Cargo.toml +++ b/extensions/pagetop-seaorm/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-seaorm" version = "0.0.4" -edition = "2021" description = """ Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales. @@ -11,6 +10,7 @@ keywords = ["pagetop", "database", "sql", "orm", "ssr"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -20,17 +20,10 @@ postgres = ["sea-orm/sqlx-postgres"] sqlite = ["sea-orm/sqlx-sqlite"] [dependencies] +async-trait.workspace = true pagetop.workspace = true +sea-orm.workspace = true +sea-schema.workspace = true serde.workspace = true - -async-trait = "0.1" -futures = "0.3" -url = "2.5" - -[dependencies.sea-orm] -version = "1.1" -features = ["debug-print", "macros", "runtime-async-std-native-tls"] -default-features = false - -[dependencies.sea-schema] -version = "0.16" +tokio.workspace = true +url.workspace = true diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml index aa37e1af..a06bc9ca 100644 --- a/helpers/pagetop-build/Cargo.toml +++ b/helpers/pagetop-build/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-build" version = "0.3.2" -edition = "2021" description = """ Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el @@ -12,9 +11,10 @@ keywords = ["pagetop", "build", "assets", "resources", "static"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -grass = "0.13" +grass.workspace = true pagetop-statics.workspace = true diff --git a/helpers/pagetop-macros/Cargo.toml b/helpers/pagetop-macros/Cargo.toml index b34d2ec1..13b5d387 100644 --- a/helpers/pagetop-macros/Cargo.toml +++ b/helpers/pagetop-macros/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-macros" version = "0.3.0" -edition = "2021" description = """ Una colección de macros que mejoran la experiencia de desarrollo con PageTop. @@ -11,6 +10,7 @@ keywords = ["pagetop", "macros", "proc-macros", "codegen"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -18,7 +18,7 @@ authors.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1.0" -proc-macro2-diagnostics = { version = "0.10", default-features = false } -quote = "1.0" -syn = { version = "2.0", features = ["full", "extra-traits"] } +proc-macro2.workspace = true +proc-macro2-diagnostics.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/helpers/pagetop-minimal/Cargo.toml b/helpers/pagetop-minimal/Cargo.toml index 39b7d10d..dfb37a9d 100644 --- a/helpers/pagetop-minimal/Cargo.toml +++ b/helpers/pagetop-minimal/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-minimal" version = "0.1.0" -edition = "2021" description = """ Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones @@ -12,10 +11,11 @@ keywords = ["pagetop", "build", "assets", "resources", "static"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true [dependencies] -concat-string = "1.0" -indoc = "2.0" -pastey = "0.2" +concat-string.workspace = true +indoc.workspace = true +pastey.workspace = true diff --git a/helpers/pagetop-statics/Cargo.toml b/helpers/pagetop-statics/Cargo.toml index da967c31..503511eb 100644 --- a/helpers/pagetop-statics/Cargo.toml +++ b/helpers/pagetop-statics/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "pagetop-statics" version = "0.1.3" -edition = "2021" description = """ Librería para automatizar la recopilación de recursos estáticos en PageTop. @@ -11,6 +10,7 @@ keywords = ["pagetop", "build", "static", "resources", "file"] repository.workspace = true homepage.workspace = true +edition.workspace = true license.workspace = true authors.workspace = true @@ -19,15 +19,11 @@ default = ["change-detection"] sort = [] [dependencies] -change-detection = { version = "1.2", optional = true } -mime_guess = "2.0" -path-slash = "0.2" - -actix-web.workspace = true -derive_more = "0.99.17" -futures-util = { version = "0.3", default-features = false, features = ["std"] } +change-detection = { workspace = true, optional = true } +mime_guess.workspace = true +path-slash.workspace = true [build-dependencies] -change-detection = { version = "1.2", optional = true } -mime_guess = "2.0" -path-slash = "0.2" +change-detection = { workspace = true, optional = true } +mime_guess.workspace = true +path-slash.workspace = true diff --git a/src/app.rs b/src/app.rs index 6a266edc..d24a03d1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,24 +6,20 @@ use crate::core::{extension, extension::ExtensionRef}; use crate::html::Markup; use crate::locale::Locale; use crate::response::page::{ErrorPage, ResultPage}; -use crate::service::HttpRequest; -use crate::{global, service, trace, PAGETOP_VERSION}; - -use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; -use actix_session::storage::CookieSessionStore; -use actix_session::SessionMiddleware; - -use substring::Substring; +use crate::web::{HttpRequest, Router}; +use crate::{PAGETOP_VERSION, global, trace}; +use std::future::Future; use std::io::Error; use std::sync::LazyLock; /// Punto de entrada de una aplicación PageTop. /// -/// No almacena datos, **encapsula** el inicio completo de configuración y puesta en marcha. Para -/// instanciarla se puede usar [`new()`](Application::new) o [`prepare()`](Application::prepare). -/// Después sólo hay que llamar a [`run()`](Application::run) para ejecutar la aplicación (o a -/// [`test()`](Application::test) si se está preparando un entorno de pruebas). +/// No almacena datos, **encapsula** el inicio completo de la configuración y puesta en marcha de la +/// aplicación. Para instanciarla se puede usar [`new()`](Application::new) o +/// [`prepare()`](Application::prepare). Después sólo hay que llamar a [`run()`](Application::run) +/// para ejecutar la aplicación (o a [`test()`](Application::test) si se está preparando un entorno +/// de pruebas). pub struct Application; impl Default for Application { @@ -33,24 +29,24 @@ impl Default for Application { } impl Application { - /// Crea una instancia de la aplicación. + /// Crea una instancia mínima de la aplicación, sin extensión raíz. + /// + /// Útil para verificar que el servidor arranca correctamente. Para una aplicación real, usa + /// [`prepare()`](Application::prepare) con una extensión raíz. pub fn new() -> Self { Self::internal_prepare(None) } /// Prepara una instancia de la aplicación a partir de una extensión raíz. /// - /// Esa extensión suele declarar: - /// - /// - Sus propias dependencias (que se habilitarán automáticamente). - /// - Una lista de extensiones que deben deshabilitarse si estuvieran activadas. - /// - /// Esto simplifica el arranque en escenarios complejos. + /// Las dependencias se habilitan en orden: primero las que no dependen de ninguna otra, luego + /// las que dependen de extensiones ya habilitadas, y así sucesivamente hasta dejar habilitada + /// la extensión raíz. pub fn prepare(root_extension: ExtensionRef) -> Self { Self::internal_prepare(Some(root_extension)) } - /// Método interno para preparar la aplicación, opcionalmente con una extensión. + // Secuencia de arranque común a new() y prepare(). fn internal_prepare(root_extension: Option) -> Self { // Al arrancar muestra una cabecera para la aplicación. Self::show_banner(); @@ -73,10 +69,10 @@ impl Application { Self } - /// Muestra una cabecera para la aplicación basada en la configuración. + // Muestra la cabecera de arranque si está habilitada en la configuración. fn show_banner() { use colored::Colorize; - use terminal_size::{terminal_size, Width}; + use terminal_size::{Width, terminal_size}; if global::SETTINGS.app.startup_banner != global::StartupBanner::Off { // Nombre de la aplicación, ajustado al ancho del terminal si es necesario. @@ -85,8 +81,8 @@ impl Application { if let Some((Width(term_width), _)) = terminal_size() { if term_width >= 80 { let maxlen: usize = ((term_width / 10) - 2).into(); - let mut app = app_name.substring(0, maxlen).to_string(); - if app_name.len() > maxlen { + let mut app: String = app_name.chars().take(maxlen).collect(); + if app_name.chars().count() > maxlen { app = format!("{app}..."); } if let Some(ff) = figfont::FIGFONT.convert(&app) { @@ -103,7 +99,7 @@ impl Application { // Descripción de la aplicación. if !global::SETTINGS.app.description.is_empty() { println!("{}", global::SETTINGS.app.description.cyan()); - }; + } // Versión de PageTop. println!( @@ -114,72 +110,55 @@ impl Application { } } + // Construye el router con las rutas de todas las extensiones habilitadas. + fn build_router() -> Router { + let router = extension::all::configure_routes(Router::new()); + router.fallback(route_not_found) + } + /// Arranca el servidor web de la aplicación. /// - /// Devuelve [`std::io::Error`] si el *socket* no puede enlazarse (por puerto en uso, permisos, - /// etc.). - pub fn run(self) -> Result { - // Genera clave secreta para firmar y verificar cookies. - let secret_key = service::cookie::Key::generate(); - - // Prepara el servidor web. - Ok(service::HttpServer::new(move || { - Self::service_app() - .wrap(tracing_actix_web::TracingLogger::default()) - .wrap( - SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) - .session_lifecycle(match global::SETTINGS.server.session_lifetime { - 0 => SessionLifecycle::BrowserSession(BrowserSession::default()), - _ => SessionLifecycle::PersistentSession( - PersistentSession::default().session_ttl( - service::cookie::time::Duration::seconds( - global::SETTINGS.server.session_lifetime, - ), - ), - ), - }) - .build(), - ) - }) - .bind(format!( + /// Enlaza el puerto del servidor web de forma síncrona (puede fallar con [`std::io::Error`] si + /// el puerto ya está en uso o el proceso carece de permisos) y devuelve un [`Future`] que + /// ejecuta el bucle de atención de peticiones. El patrón habitual es: + /// + /// ```rust,no_run + /// use pagetop::prelude::*; + /// + /// struct MyApp; + /// + /// impl Extension for MyApp {} + /// + /// #[pagetop::main] + /// async fn main() -> std::io::Result<()> { + /// Application::prepare(&MyApp).run()?.await + /// } + /// ``` + pub fn run(self) -> Result>, Error> { + let addr = format!( "{}:{}", - &global::SETTINGS.server.bind_address, - &global::SETTINGS.server.bind_port - ))? - .run()) + global::SETTINGS.server.bind_address, + global::SETTINGS.server.bind_port + ); + + // Enlaza el puerto de forma síncrona para detectar errores antes del *await*. + let std_listener = std::net::TcpListener::bind(&addr)?; + std_listener.set_nonblocking(true)?; + + let router = Self::build_router(); + + Ok(async move { + let listener = tokio::net::TcpListener::from_std(std_listener)?; + axum::serve(listener, router).await + }) } - /// Prepara el servidor web de la aplicación para pruebas. - pub fn test( - self, - ) -> service::App< - impl service::Factory< - service::Request, - Config = (), - Response = service::Response, - Error = service::Error, - InitError = (), - >, - > { - Self::service_app() - } - - /// Configura el servicio web de la aplicación. - fn service_app() -> service::App< - impl service::Factory< - service::Request, - Config = (), - Response = service::Response, - Error = service::Error, - InitError = (), - >, - > { - service::App::new() - .configure(extension::all::configure_services) - .default_service(service::web::route().to(service_not_found)) + /// Devuelve el servidor web configurado para usarlo en pruebas de integración. + pub fn test(self) -> Router { + Self::build_router() } } -async fn service_not_found(request: HttpRequest) -> ResultPage { +async fn route_not_found(request: HttpRequest) -> ResultPage { Err(ErrorPage::NotFound(request)) } diff --git a/src/config.rs b/src/config.rs index 9b7b43d2..9c687a0f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,11 +7,11 @@ //! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada //! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente. //! -//! //! # Orden de carga //! //! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del -//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación. +//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación. Puedes +//! cambiar esta ubicación mediante la variable de entorno `CONFIG_DIR`. //! //! PageTop carga en este orden, y siempre de forma opcional, los siguientes archivos TOML: //! @@ -42,7 +42,6 @@ //! Los archivos se combinan en el orden anterior, cada archivo sobrescribe a los anteriores en caso //! de conflicto. //! -//! //! # Cómo añadir opciones de configuración a tu código //! //! Añade [*serde*](https://docs.rs/serde) en tu archivo *Cargo.toml* con la *feature* `derive`: @@ -91,7 +90,6 @@ //! //! Las estructuras de configuración son de **sólo lectura** durante la ejecución. //! -//! //! # Usando tus opciones de configuración //! //! ```rust,ignore @@ -131,9 +129,14 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into()); let config_dir = util::resolve_absolute_dir(&dir).unwrap_or_else(|_| PathBuf::from(&dir)); - // Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Si no está definida, se usa - // por defecto DEFAULT_RUN_MODE (p. ej. PAGETOP_RUN_MODE=production). - let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into()); + // Modo de ejecución. Con la *feature* `testing` activa (usada por `cargo ts` y `cargo tw`), se + // fija en "test" en tiempo de compilación, sin manipular el entorno. En caso contrario se lee + // de PAGETOP_RUN_MODE, o se usa DEFAULT_RUN_MODE si la variable no está definida. + let rm = if cfg!(feature = "testing") { + "test".to_string() + } else { + env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into()) + }; Config::builder() // 1. Configuración común para todos los entornos (common.toml). @@ -158,7 +161,7 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// Hay que añadir en nuestra librería el siguiente código: /// /// ```rust,ignore -/// include_config!(SETTINGS: Settings => [ +/// include_config!(SETTINGS_NAME: SettingsType => [ /// "ruta.clave" => valor, /// // ... /// ]); @@ -168,8 +171,8 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// /// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los /// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio. -/// * **`Settings_Type`** es la referencia a la estructura que define los tipos para deserializar la -/// configuración. Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`). +/// * **`SettingsType`** es la estructura que define los tipos para deserializar la configuración. +/// Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`). /// * **Lista de pares** con las claves TOML que requieran valores por defecto. Siguen la notación /// `"seccion.subclave"` para coincidir con el árbol TOML. /// @@ -211,7 +214,7 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p. ej. `[blog]`) para /// evitar colisiones con otras librerías. /// -/// * **Solo lectura**. La variable generada es inmutable durante toda la vida del programa. Para +/// * **Sólo lectura**. La variable generada es inmutable durante toda la vida del programa. Para /// configurar distintos entornos (*dev*, *staging*, *prod*) usa los archivos TOML descritos en la /// documentación de [`config`](crate::config). /// @@ -220,8 +223,8 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// /// # Requisitos /// -/// * Dependencia `serde` con la *feature* `derive`. -/// * Las claves deben coincidir con los campos (*snake case*) de tu estructura `Settings_Type`. +/// * Las claves deben coincidir con los campos (*snake case*) de la estructura de ajustes. +/// * Añade `serde` con la *feature* `derive` en *Cargo.toml*: /// /// ```toml /// [dependencies] @@ -229,10 +232,10 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( /// ``` #[macro_export] macro_rules! include_config { - ( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { + ( $SETTINGS_NAME:ident : $settings_type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { #[doc = concat!( "Ajustes de configuración y **valores por defecto** para ", - "[`", stringify!($Settings_Type), "`]." + "[`", stringify!($settings_type), "`]." )] #[doc = ""] #[doc = "Valores predeterminados que se aplican en ausencia de configuración:"] @@ -241,17 +244,18 @@ macro_rules! include_config { #[doc = concat!($k, " = ", stringify!($v))] )* #[doc = "```"] - pub static $SETTINGS_NAME: std::sync::LazyLock<$Settings_Type> = + pub static $SETTINGS_NAME: std::sync::LazyLock<$settings_type> = std::sync::LazyLock::new(|| { let mut settings = $crate::config::CONFIG_VALUES.clone(); $( - settings = settings.set_default($k, $v).unwrap(); + settings = settings.set_default($k, $v) + .expect(concat!("Failed to set default for key ", $k)); )* settings .build() - .expect(concat!("Failed to build config for ", stringify!($Settings_Type))) - .try_deserialize::<$Settings_Type>() - .expect(concat!("Error parsing settings for ", stringify!($Settings_Type))) + .expect(concat!("Failed to build config for ", stringify!($settings_type))) + .try_deserialize::<$settings_type>() + .expect(concat!("Error parsing settings for ", stringify!($settings_type))) }); }; } diff --git a/src/core.rs b/src/core.rs index 8a47848e..03e32a94 100644 --- a/src/core.rs +++ b/src/core.rs @@ -30,28 +30,22 @@ impl TypeInfo { } } - /// Extrae un rango de segmentos de `type_name` (tokens separados por `::`). - /// - /// Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta: - /// - /// * Los índices positivos cuentan **desde la izquierda**, empezando en `0`. - /// * Los índices negativos cuentan **desde la derecha**, `-1` es el último. - /// * Si `end` es `None`, el corte llega hasta el último segmento. - /// * Si la selección resulta vacía por índices desordenados o segmento inexistente, se devuelve - /// la cadena vacía. - /// - /// Ejemplos (con `type_name = "alloc::vec::Vec"`): - /// - /// | Llamada | Resultado | - /// |------------------------------|--------------------------| - /// | `partial(..., 0, None)` | `"alloc::vec::Vec"` | - /// | `partial(..., 1, None)` | `"vec::Vec"` | - /// | `partial(..., -1, None)` | `"Vec"` | - /// | `partial(..., 0, Some(-2))` | `"alloc::vec"` | - /// | `partial(..., -5, None)` | `"alloc::vec::Vec"` | - /// - /// La porción devuelta vive tanto como `'static` porque `type_name` es `'static` y sólo se - /// presta. + // Extrae un rango de segmentos de `type_name` (tokens separados por `::`). + // + // Los argumentos `start` y `end` identifican los índices de los segmentos teniendo en cuenta: + // + // * Los índices positivos cuentan desde la izquierda, empezando en 0. + // * Los índices negativos cuentan desde la derecha; -1 es el último. + // * Si `end` es `None`, el corte llega hasta el último segmento. + // * Si la selección resulta vacía por índices desordenados o segmento inexistente, devuelve "". + // + // Ejemplos con type_name = "alloc::vec::Vec": + // + // partial(..., 0, None) => "alloc::vec::Vec" + // partial(..., 1, None) => "vec::Vec" + // partial(..., -1, None) => "Vec" + // partial(..., 0, Some(-2)) => "alloc::vec" + // partial(..., -5, None) => "alloc::vec::Vec" fn partial(type_name: &'static str, start: isize, end: Option) -> &'static str { let maxlen = type_name.len(); @@ -59,7 +53,7 @@ impl TypeInfo { let mut segments = Vec::new(); let mut segment_start = 0; // Posición inicial del segmento actual. let mut angle_brackets = 0; // Profundidad dentro de '<...>'. - let mut previous_char = '\0'; // Se inicializa a carácter nulo, no hay aún carácter previo. + let mut previous_char = '\0'; // Control, ningún carácter previo aún. for (idx, c) in type_name.char_indices() { match c { diff --git a/src/global.rs b/src/global.rs index 8bf753e3..953dfb6d 100644 --- a/src/global.rs +++ b/src/global.rs @@ -39,9 +39,8 @@ include_config!(SETTINGS: Settings => [ "log.format" => "Full", // [server] - "server.bind_address" => "localhost", - "server.bind_port" => 8080, - "server.session_lifetime" => 604_800, + "server.bind_address" => "localhost", + "server.bind_port" => 8080, ]); // **< Settings >*********************************************************************************** @@ -116,7 +115,7 @@ pub struct Log { pub enabled: bool, /// Opciones, o combinación de opciones separadas por comas, para filtrar las trazas: *"Error"*, /// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*. - /// Ejemplo: *"Error,actix_server::builder=Info,tracing_actix_web=Debug"*. + /// Ejemplo: *"Error,tower_http=Debug,axum::rejection=trace"*. pub tracing: String, /// Muestra los mensajes de traza en el terminal (*"Stdout"*) o los vuelca en archivos con /// rotación: *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*. @@ -136,8 +135,4 @@ pub struct Server { pub bind_address: String, /// Puerto de escucha del servidor web. pub bind_port: u16, - /// Duración de la cookie de sesión en segundos (p. ej., `604_800` para una semana). - /// - /// El valor `0` indica que la cookie permanecerá activa hasta que se cierre el navegador. - pub session_lifetime: i64, } diff --git a/src/html.rs b/src/html.rs index 21809c08..6020627d 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,7 +1,7 @@ //! HTML en código. mod maud; -pub use maud::{display, html, html_private, Escaper, Markup, PreEscaped, DOCTYPE}; +pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, display, html, html_private}; mod route; pub use route::RoutePath; diff --git a/src/lib.rs b/src/lib.rs index 0213e61e..918ecd02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,8 +53,8 @@ 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)) } } @@ -117,10 +117,10 @@ use std::ops::Deref; /// fn before_render_page_body(&self, page: &mut Page) { /// page /// .alter_assets(AssetsOp::AddStyleSheet( -/// StyleSheet::from("/css/normalize.css").with_version("8.0.1"), +/// StyleSheet::from("/pagetop/css/normalize.css").with_version("8.0.1"), /// )) /// .alter_assets(AssetsOp::AddStyleSheet( -/// StyleSheet::from("/css/basic.css").with_version(PAGETOP_VERSION), +/// StyleSheet::from("/pagetop/css/basic.css").with_version(PAGETOP_VERSION), /// )) /// .alter_assets(AssetsOp::AddStyleSheet( /// StyleSheet::from("/mytheme/styles.css").with_version(env!("CARGO_PKG_VERSION")), @@ -132,9 +132,9 @@ use std::ops::Deref; /// referencia a la versión del *crate* que lo usa. pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub use pagetop_macros::{builder_fn, html, main, test, AutoDefault}; +pub use pagetop_macros::{AutoDefault, builder_fn, html, main, test}; -pub use pagetop_statics::{resource, StaticResource}; +pub use pagetop_statics::{StaticResource, resource}; pub use getter_methods::Getters; @@ -198,8 +198,8 @@ pub mod datetime; pub mod core; // Respuestas a peticiones web en sus diferentes formatos. pub mod response; -// Gestión del servidor y servicios web. -pub mod service; +// Gestión del servidor y rutas web. +pub mod web; // Reúne acciones, componentes, extensiones y temas predefinidos. pub mod base; // Prepara y ejecuta la aplicación. diff --git a/src/prelude.rs b/src/prelude.rs index 818bfc91..32ce68b7 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -14,7 +14,7 @@ pub use crate::{AutoDefault, CowStr, Getters, StaticResources, UniqueId, Weight} pub use crate::include_config; // crate::locale pub use crate::include_locales; -// crate::service +// crate::web pub use crate::static_files_service; // crate::core::action pub use crate::actions; @@ -35,8 +35,8 @@ pub use crate::locale::*; pub use crate::datetime::*; -pub use crate::service; -pub use crate::service::{HttpMessage, HttpRequest, HttpResponse}; +pub use crate::web; +pub use crate::web::{HttpRequest, IntoResponse, Json, Path, Query, Router}; pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; @@ -45,7 +45,7 @@ pub use crate::core::component::*; pub use crate::core::extension::*; pub use crate::core::theme::*; -pub use crate::response::{json::*, page::*, redirect::*, ResponseError}; +pub use crate::response::{page::*, redirect::*}; pub use crate::base::action; pub use crate::base::component::*; diff --git a/src/response.rs b/src/response.rs index 4078d420..55150b71 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,7 +1,5 @@ //! Respuestas a las peticiones web en sus diferentes formatos. -pub use actix_web::ResponseError; - pub mod page; pub mod json; diff --git a/src/service.rs b/src/service.rs deleted file mode 100644 index 10665413..00000000 --- a/src/service.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Gestión del servidor y servicios web (con [Actix Web](https://docs.rs/actix-web)). - -pub use actix_session::Session; -pub use actix_web::body::BoxBody; -pub use actix_web::dev::Server; -pub use actix_web::dev::ServiceFactory as Factory; -pub use actix_web::dev::ServiceRequest as Request; -pub use actix_web::dev::ServiceResponse as Response; -pub use actix_web::{cookie, http, rt, web}; -pub use actix_web::{App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer}; -pub use actix_web_files::Files as ActixFiles; - -pub use pagetop_statics::ResourceFiles; - -#[doc(hidden)] -pub use actix_web::test; - -// **< static_files_service! >********************************************************************** - -/// Configura un servicio web para publicar archivos estáticos. -/// -/// La macro ofrece tres modos para configurar el servicio: -/// -/// - **Sistema de ficheros o embebido** (`[$path, $bundle]`): trata de servir los archivos desde -/// `$path`; y si es una cadena vacía, no existe o no es un directorio, entonces usará el conjunto -/// de recursos `$bundle` integrado en el binario. -/// - **Sólo embebido** (`[$bundle]`): sirve siempre desde el conjunto de recursos `$bundle` -/// integrado en el binario. -/// - **Sólo sistema de ficheros** (`$path`): sin usar corchetes, sirve únicamente desde el sistema -/// de ficheros si existe; en otro caso no registra el servicio. -/// -/// # Argumentos -/// -/// * `$scfg` - Instancia de [`ServiceConfig`](crate::service::web::ServiceConfig) donde aplicar la -/// configuración. -/// * `$path` - Ruta al directorio local con los archivos estáticos. -/// * `$bundle` - Nombre del conjunto de recursos que esta macro integra en el binario. -/// * `$route` - Ruta URL base desde la que se servirán los archivos. -/// -/// # Ejemplos -/// -/// ```rust,ignore -/// # use pagetop::prelude::*; -/// pub struct MyExtension; -/// -/// impl Extension for MyExtension { -/// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { -/// // Forma 1) Sistema de ficheros o embebido. -/// static_files_service!(scfg, ["/var/www/static", assets] => "/public"); -/// -/// // Forma 2) Siempre embebido. -/// static_files_service!(scfg, [assets] => "/public"); -/// -/// // Forma 3) Sólo sistema de ficheros (no requiere `assets`). -/// static_files_service!(scfg, "/var/www/static" => "/public"); -/// } -/// } -/// ``` -#[macro_export] -macro_rules! static_files_service { - // Forma 1: primero intenta servir desde el sistema de ficheros; si falla, sirve embebido. - ( $scfg:ident, [$path:expr, $bundle:ident] => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "Configuring static files (file system or embedded)", - mode = "fs_or_embedded", - route = $route, - ); - let _ = span.in_scope(|| { - let mut serve_embedded: bool = true; - if !::std::path::Path::new(&$path).as_os_str().is_empty() { - if let Ok(absolute) = $crate::util::resolve_absolute_dir($path) { - $scfg.service($crate::service::ActixFiles::new($route, absolute)); - serve_embedded = false; - } - } - if serve_embedded { - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - $scfg.service($crate::service::ResourceFiles::new( - $route, - []::$bundle(), - )); - } - } - }); - }}; - // Forma 2: sirve siempre embebido. - ( $scfg:ident, [$bundle:ident] => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "Configuring static files (using embedded only)", - mode = "embedded", - route = $route, - ); - let _ = span.in_scope(|| { - $crate::util::paste! { - mod [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - $scfg.service($crate::service::ResourceFiles::new( - $route, - []::$bundle(), - )); - } - }); - }}; - // Forma 3: intenta servir desde el sistema de ficheros. - ( $scfg:ident, $path:expr => $route:expr $(,)? ) => {{ - let span = $crate::trace::debug_span!( - "Configuring static files (file system only)", - mode = "fs", - route = $route, - ); - let _ = span.in_scope(|| match $crate::util::resolve_absolute_dir($path) { - Ok(absolute) => { - $scfg.service($crate::service::ActixFiles::new($route, absolute)); - } - Err(e) => { - $crate::trace::warn!( - "Static dir not found or invalid for route `{}`: {:?} ({e})", - $route, - $path, - ); - } - }); - }}; -} diff --git a/src/util.rs b/src/util.rs index 6ea4b70d..5c1fff49 100644 --- a/src/util.rs +++ b/src/util.rs @@ -58,14 +58,15 @@ pub enum NormalizeAsciiError { /// # use pagetop::util; /// assert_eq!(util::normalize_ascii(" Foo\tBAR CLi\r\n").unwrap().as_ref(), "foo bar cli"); /// ``` -pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsciiError> { +pub fn normalize_ascii(input: &str) -> Result, NormalizeAsciiError> { let bytes = input.as_bytes(); if bytes.is_empty() { return Err(NormalizeAsciiError::IsEmpty); } - let mut start = 0usize; - let mut end = 0usize; + // Primera pasada, determina si se necesita asignación y calcula los límites del contenido. + let mut start = 0; + let mut end = 0; let mut needs_alloc = false; let mut needs_alloc_ws = false; @@ -110,6 +111,7 @@ pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsci return Ok(Cow::Borrowed(slice)); } + // Segunda pasada, construye la cadena normalizada. let mut output = String::with_capacity(slice.len()); let mut prev_sep = true; @@ -132,8 +134,8 @@ pub fn normalize_ascii<'a>(input: &'a str) -> Result, NormalizeAsci /// /// - Devuelve `Some(Cow)` si la entrada es válida ASCII (normalizada a minúsculas). /// - Devuelve `Some(Cow::Borrowed(""))` si la entrada es `""` o queda vacía tras recortar. -/// - Devuelve `None` si la entrada contiene bytes non-ASCII; y emite un `trace::debug!` con el -/// campo `target`. +/// - Devuelve `None` si la entrada contiene bytes no ASCII; y emite un `trace::debug!` con el campo +/// `target`. #[inline] pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Option> { match normalize_ascii(input) { @@ -171,15 +173,25 @@ pub fn normalize_ascii_or_empty<'a>(input: &'a str, target: &'static str) -> Opt /// println!("{:#?}", util::resolve_absolute_dir("/var/www")); /// ``` pub fn resolve_absolute_dir>(path: P) -> io::Result { + resolve_absolute_dir_with_base(path, env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from)) +} + +/// Auxiliar de [`resolve_absolute_dir`] expuesta para tests. +/// +/// Permite probar la lógica de resolución inyectando el directorio base explícitamente, sin +/// modificar variables de entorno globales. No forma parte de la API pública. +#[doc(hidden)] +pub fn resolve_absolute_dir_with_base>( + path: P, + base: Option, +) -> io::Result { let path = path.as_ref(); let candidate = if path.is_absolute() { path.to_path_buf() } else { - // Directorio base CARGO_MANIFEST_DIR si está disponible; o current_dir() en su defecto. - env::var_os("CARGO_MANIFEST_DIR") - .map(PathBuf::from) - .or_else(|| env::current_dir().ok()) + // Directorio base proporcionado, o current_dir() en su defecto. + base.or_else(|| env::current_dir().ok()) .unwrap_or_else(|| PathBuf::from(".")) .join(path) }; @@ -191,10 +203,8 @@ pub fn resolve_absolute_dir>(path: P) -> io::Result { if absolute_dir.is_dir() { Ok(absolute_dir) } else { - Err({ - let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); - trace::warn!(msg); - io::Error::new(io::ErrorKind::InvalidInput, msg) - }) + let msg = format!("path \"{}\" is not a directory", absolute_dir.display()); + trace::warn!(msg); + Err(io::Error::new(io::ErrorKind::InvalidInput, msg)) } } diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 00000000..fb8a4883 --- /dev/null +++ b/src/web.rs @@ -0,0 +1,340 @@ +//! 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`]. + +use std::collections::HashMap; +use std::convert::Infallible; +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; +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; + +// Verbos HTTP para registrar rutas. +pub use axum::routing::{delete, get, patch, post, put}; + +// Servicios para archivos estáticos (disco y embebidos). +pub use pagetop_statics::StaticResource; +pub use tower_http::services::ServeDir; + +// **< HttpRequest >******************************************************************************** + +/// Representa una petición HTTP. +/// +/// Almacena los datos necesarios para negociar el idioma y renderizar las páginas de error, +/// incluyendo la URI completa y las cabeceras de la petición original. +/// +/// Puede declararse directamente como parámetro en un *handler* para pasarlo al +/// [`Context`](crate::core::component::Context) de renderizado y a las variantes de +/// [`ErrorPage`](crate::response::page::ErrorPage): +/// +/// ```rust,ignore +/// async fn my_handler(request: HttpRequest) -> ResultPage { ... } +/// ``` +#[derive(Clone, Debug)] +pub struct HttpRequest { + uri: Uri, + headers: HeaderMap, +} + +impl HttpRequest { + /// Devuelve la URI completa de la petición, incluyendo la *query string* si la hay. + pub fn uri(&self) -> &str { + self.uri + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/") + } + + /// Devuelve la ruta (*path*) de la petición, sin la *query string*. + pub fn path(&self) -> &str { + self.uri.path() + } + + /// Devuelve la cadena de consulta (*query string*) de la petición, sin el carácter `?`. + /// + /// Devuelve una cadena vacía si la petición no tiene *query string*. + pub fn query_string(&self) -> &str { + self.uri.query().unwrap_or("") + } + + /// Devuelve las cabeceras HTTP de la petición. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } +} + +impl FromRequestParts for HttpRequest { + type Rejection = Infallible; + + // Implementa el extractor de Axum para poder declarar `HttpRequest` como parámetro. + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + Ok(HttpRequest { + uri: parts.uri.clone(), + headers: parts.headers.clone(), + }) + } +} + +// **< ServeEmbedded >****************************************************************************** + +/// Permite servir 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. +/// +/// Es [`Clone`] para clonar el servicio por petición, pero internamente comparte el mapa de +/// recursos con un [`Arc`](std::sync::Arc) para evitar copias innecesarias. +#[derive(Clone)] +pub struct ServeEmbedded { + files: std::sync::Arc>, +} + +impl ServeEmbedded { + /// Crea un nuevo servicio a partir del mapa de recursos embebidos generado por `build.rs`. + pub fn new(files: HashMap<&'static str, StaticResource>) -> Self { + Self { + files: std::sync::Arc::new(files), + } + } +} + +impl tower::Service> for ServeEmbedded { + type Response = Response; + type Error = Infallible; + type Future = std::future::Ready>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + use axum::http::header; + + // Axum elimina el prefijo de montaje: la ruta restante puede o no comenzar con '/'. + let path = req.uri().path().trim_start_matches('/'); + + // Busca la ruta exacta; si es raíz o directorio, intenta index.html. + let resource = self.files.get(path).or_else(|| { + if path.is_empty() || path.ends_with('/') { + self.files.get("index.html") + } else { + None + } + }); + + let response = match resource { + Some(r) => Response::builder() + .header(header::CONTENT_TYPE, r.mime_type) + .body(Body::from(r.data)) + .unwrap(), + None => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap(), + }; + + std::future::ready(Ok(response)) + } +} + +// **< static_files_service! >********************************************************************** + +/// Configura un servicio web para publicar archivos estáticos. +/// +/// La macro añade rutas al [`Router`] de Axum pasado como primer argumento y ofrece tres modos: +/// +/// - **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. +/// +/// # 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. +/// +/// # Ejemplos +/// +/// ```rust,ignore +/// # use pagetop::prelude::*; +/// pub struct MyExtension; +/// +/// impl Extension for MyExtension { +/// fn configure_router(&self, mut router: Router) -> Router { +/// // Forma 1) Sistema de ficheros o embebido. +/// static_files_service!(router, ["/var/www/static", assets] => "/public"); +/// +/// // Forma 2) Siempre embebido. +/// static_files_service!(router, [assets] => "/public"); +/// +/// // Forma 3) Sólo sistema de ficheros (no requiere `assets`). +/// static_files_service!(router, "/var/www/static" => "/public"); +/// +/// router +/// } +/// } +/// ``` +#[macro_export] +macro_rules! static_files_service { + // 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; + } + } + if !served_from_fs { + $crate::util::paste! { + mod [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + $router = $router.nest_service( + $route, + $crate::web::ServeEmbedded::new( + []::$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 [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + $router = $router.nest_service( + $route, + $crate::web::ServeEmbedded::new( + []::$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)); + } + Err(e) => { + $crate::trace::warn!( + "Static dir not found or invalid for route `{}`: {:?} ({e})", + $route, + $path, + ); + } + } + }}; +} + +// **< Utilidades de test >************************************************************************* + +/// Utilidades para escribir pruebas de integración con PageTop sobre Axum. +#[doc(hidden)] +pub mod test { + use axum::Router; + use axum::body::Body; + use axum::http::{Method, Request}; + use axum::response::Response; + use tower::ServiceExt; + + /// Devuelve el router tal como se recibe, listo para usarse en pruebas de integración. + pub fn init_router(router: Router) -> Router { + router + } + + /// Constructor de peticiones HTTP para pruebas. + pub struct TestRequest { + method: Method, + uri: String, + } + + impl TestRequest { + /// Crea una petición GET. + pub fn get() -> Self { + Self { + method: Method::GET, + uri: "/".to_owned(), + } + } + + /// Crea una petición POST. + pub fn post() -> Self { + Self { + method: Method::POST, + uri: "/".to_owned(), + } + } + + /// Establece la URI de la petición. + pub fn uri(mut self, uri: impl Into) -> Self { + self.uri = uri.into(); + self + } + + /// Construye la petición HTTP de Axum (para enviar al router en tests de integración). + pub fn to_request(self) -> Request { + Request::builder() + .method(self.method) + .uri(self.uri) + .body(Body::empty()) + .unwrap() + } + + /// Construye un [`HttpRequest`](super::HttpRequest) listo para pasarlo a + /// [`Context::new`](crate::core::component::Context::new) en tests unitarios de componentes. + pub fn to_http_request(self) -> super::HttpRequest { + let uri = self.uri.parse().unwrap(); + super::HttpRequest { + uri, + headers: axum::http::HeaderMap::new(), + } + } + } + + /// Envía una petición al router y devuelve la respuesta. + pub async fn send_request(router: &Router, req: Request) -> Response { + router.clone().oneshot(req).await.unwrap() + } +} From 7553ed35ecd6f3f48bbac218c9e018c40d2f6100 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 30 May 2026 22:50:40 +0200 Subject: [PATCH 14/29] =?UTF-8?q?=F0=9F=8E=A8=20Aplica=20formato=20Rust=20?= =?UTF-8?q?2024=20(`rustfmt.toml`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-bootsier/src/theme/classes/layout.rs | 2 +- .../pagetop-bootsier/src/theme/dropdown/component.rs | 2 +- extensions/pagetop-bootsier/src/theme/form/checkbox.rs | 2 +- extensions/pagetop-bootsier/src/theme/form/input.rs | 2 +- extensions/pagetop-bootsier/src/theme/form/select.rs | 2 +- extensions/pagetop-bootsier/src/theme/form/textarea.rs | 2 +- extensions/pagetop-bootsier/src/theme/nav/item.rs | 2 +- extensions/pagetop-bootsier/src/theme/navbar/component.rs | 2 +- .../pagetop-bootsier/src/theme/offcanvas/component.rs | 2 +- helpers/pagetop-statics/build.rs | 2 +- helpers/pagetop-statics/src/resource_dir.rs | 2 +- helpers/pagetop-statics/src/sets.rs | 4 ++-- rustfmt.toml | 8 ++++++++ src/core/action/list.rs | 6 +++--- src/core/component/children.rs | 4 ++-- src/core/component/context.rs | 8 ++++---- src/core/component/definition.rs | 2 +- src/core/component/error.rs | 2 +- src/core/theme.rs | 4 ++-- src/core/theme/definition.rs | 4 ++-- src/core/theme/regions.rs | 2 +- src/html/assets.rs | 2 +- src/html/assets/favicon.rs | 2 +- src/html/assets/javascript.rs | 4 ++-- src/html/assets/stylesheet.rs | 4 ++-- src/html/attr.rs | 2 +- src/html/classes.rs | 2 +- src/html/logo.rs | 6 +++--- src/locale/definition.rs | 2 +- src/locale/l10n.rs | 2 +- src/locale/languages.rs | 2 +- 31 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 rustfmt.toml diff --git a/extensions/pagetop-bootsier/src/theme/classes/layout.rs b/extensions/pagetop-bootsier/src/theme/classes/layout.rs index ee403a88..1438b210 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/layout.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::attrs::{ScaleSize, Side}; use crate::theme::BreakPoint; +use crate::theme::attrs::{ScaleSize, Side}; // **< Margin >************************************************************************************* diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index ca15c635..b70fed65 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; /// Componente para crear un **menú desplegable** ([`dropdown`]). /// diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs index 60d7120d..18ab908f 100644 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; /// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*). /// diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index 997b7c45..68cce931 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -2,8 +2,8 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; use std::fmt; diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 92736586..7d51e9c9 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -2,8 +2,8 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; // **< Item >*************************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index 781e1d09..81b32783 100644 --- a/extensions/pagetop-bootsier/src/theme/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::form; use crate::LOCALES_BOOTSIER; +use crate::theme::form; /// Componente para crear un **área de texto** de formulario. /// diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index ef5a6fe9..43386baf 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; // **< ItemKind >*********************************************************************************** diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index ccd97e90..096ec87a 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_OFFCANVAS: &str = "offcanvas"; diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs index 764627e4..a2c014b8 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -use crate::theme::*; use crate::LOCALES_BOOTSIER; +use crate::theme::*; /// Componente para crear un **panel lateral deslizante** ([`offcanvas`]). /// diff --git a/helpers/pagetop-statics/build.rs b/helpers/pagetop-statics/build.rs index fcd009c9..3cbd6706 100644 --- a/helpers/pagetop-statics/build.rs +++ b/helpers/pagetop-statics/build.rs @@ -13,7 +13,7 @@ use resource_dir::resource_dir; mod sets { include!("src/sets.rs"); } -use sets::{generate_resources_sets, SplitByCount}; +use sets::{SplitByCount, generate_resources_sets}; use std::{env, path::Path}; diff --git a/helpers/pagetop-statics/src/resource_dir.rs b/helpers/pagetop-statics/src/resource_dir.rs index 805e1ed4..41c29829 100644 --- a/helpers/pagetop-statics/src/resource_dir.rs +++ b/helpers/pagetop-statics/src/resource_dir.rs @@ -1,4 +1,4 @@ -use super::sets::{generate_resources_sets, SplitByCount}; +use super::sets::{SplitByCount, generate_resources_sets}; use std::{ env, io, path::{Path, PathBuf}, diff --git a/helpers/pagetop-statics/src/sets.rs b/helpers/pagetop-statics/src/sets.rs index 1d9299df..5e09f1ff 100644 --- a/helpers/pagetop-statics/src/sets.rs +++ b/helpers/pagetop-statics/src/sets.rs @@ -5,8 +5,8 @@ use std::{ }; use super::resource::{ - collect_resources, generate_function_end, generate_function_header, generate_resource_insert, - generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME, + DEFAULT_VARIABLE_NAME, collect_resources, generate_function_end, generate_function_header, + generate_resource_insert, generate_uses, generate_variable_header, generate_variable_return, }; /// Defines the split strategie. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..5d6e629c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +edition = "2024" +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Auto" + +# Heurísticas por defecto: evitar reformateo agresivo +use_small_heuristics = "Default" diff --git a/src/core/action/list.rs b/src/core/action/list.rs index d60129c1..f47d3d6e 100644 --- a/src/core/action/list.rs +++ b/src/core/action/list.rs @@ -1,7 +1,7 @@ -use crate::core::action::{ActionBox, ActionDispatcher}; -use crate::core::AnyCast; -use crate::trace; use crate::AutoDefault; +use crate::core::AnyCast; +use crate::core::action::{ActionBox, ActionDispatcher}; +use crate::trace; use parking_lot::RwLock; diff --git a/src/core/component/children.rs b/src/core/component/children.rs index 617a783d..bfedec14 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -1,6 +1,6 @@ use crate::core::component::{Component, Context}; -use crate::html::{html, Markup}; -use crate::{builder_fn, AutoDefault, UniqueId}; +use crate::html::{Markup, html}; +use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::Mutex; diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 63b2daad..9ff7251c 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,13 +1,13 @@ +use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef}; -use crate::core::TypeInfo; -use crate::html::{html, Markup, RoutePath}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; +use crate::html::{Markup, RoutePath, html}; use crate::locale::L10n; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; -use crate::service::HttpRequest; -use crate::{builder_fn, util, CowStr}; +use crate::web::HttpRequest; +use crate::{CowStr, builder_fn, util}; use std::any::Any; use std::cell::Cell; diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index 718e3b37..b7ceaa9a 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -2,7 +2,7 @@ use crate::base::action; use crate::core::component::{ComponentError, Context, Contextual}; use crate::core::theme::ThemeRef; use crate::core::{AnyInfo, TypeInfo}; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; /// Permite clonar un componente. /// diff --git a/src/core/component/error.rs b/src/core/component/error.rs index beb7c8f2..86f9e4aa 100644 --- a/src/core/component/error.rs +++ b/src/core/component/error.rs @@ -1,4 +1,4 @@ -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::{AutoDefault, Getters}; /// Error producido durante el renderizado de un componente. diff --git a/src/core/theme.rs b/src/core/theme.rs index a8c1f3a4..43649db1 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -28,9 +28,9 @@ //! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas. use crate::core::component::Context; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::locale::L10n; -use crate::{util, AutoDefault}; +use crate::{AutoDefault, util}; // **< Region >************************************************************************************* diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 17f3b391..0b036dd4 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -3,10 +3,10 @@ use crate::core::component::{ChildOp, Component, ComponentError, Context, Contex use crate::core::extension::Extension; use crate::core::theme::{DefaultRegion, DefaultTemplate, TemplateRef}; use crate::global; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::locale::L10n; use crate::response::page::Page; -use crate::service::http::StatusCode; +use crate::web::http::StatusCode; /// Interfaz común que debe implementar cualquier tema de PageTop. /// diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index a2b71ff2..a10e3ecc 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,6 +1,6 @@ use crate::core::component::{Child, ChildOp, Children, Component}; use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef}; -use crate::{builder_fn, AutoDefault, UniqueId}; +use crate::{AutoDefault, UniqueId, builder_fn}; use parking_lot::RwLock; diff --git a/src/html/assets.rs b/src/html/assets.rs index fe5f5b7c..80cb3b26 100644 --- a/src/html/assets.rs +++ b/src/html/assets.rs @@ -3,7 +3,7 @@ pub mod javascript; pub mod stylesheet; use crate::core::component::Context; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::{AutoDefault, Weight}; /// Representación genérica de un script [`JavaScript`](crate::html::JavaScript) o una hoja de diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 1a4174bf..2a4f26ed 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -1,5 +1,5 @@ use crate::core::component::Context; -use crate::html::{html, Markup}; +use crate::html::{Markup, html}; use crate::{AutoDefault, CowStr}; /// Un **Favicon** es un recurso gráfico que usa el navegador como icono asociado al sitio. diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index 62126895..e348bebc 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -1,7 +1,7 @@ use crate::core::component::Context; use crate::html::assets::Asset; -use crate::html::{html, Markup, PreEscaped}; -use crate::{util, AutoDefault, CowStr, Weight}; +use crate::html::{Markup, PreEscaped, html}; +use crate::{AutoDefault, CowStr, Weight, util}; /// Define el origen del recurso JavaScript y cómo debe cargarse en el navegador. /// diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index 5a6d98c5..5941db9d 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -1,7 +1,7 @@ use crate::core::component::Context; use crate::html::assets::Asset; -use crate::html::{html, Markup, PreEscaped}; -use crate::{util, AutoDefault, CowStr, Weight}; +use crate::html::{Markup, PreEscaped, html}; +use crate::{AutoDefault, CowStr, Weight, util}; /// Define el origen del recurso CSS y cómo se incluye en el documento. /// diff --git a/src/html/attr.rs b/src/html/attr.rs index 61f7252c..8f25a5eb 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -1,5 +1,5 @@ use crate::locale::{L10n, LangId}; -use crate::{builder_fn, AutoDefault}; +use crate::{AutoDefault, builder_fn}; /// Valor opcional para atributos HTML. /// diff --git a/src/html/classes.rs b/src/html/classes.rs index 3465d6b3..2f665c19 100644 --- a/src/html/classes.rs +++ b/src/html/classes.rs @@ -1,4 +1,4 @@ -use crate::{builder_fn, util, AutoDefault, CowStr}; +use crate::{AutoDefault, CowStr, builder_fn, util}; use std::collections::HashSet; diff --git a/src/html/logo.rs b/src/html/logo.rs index d5dcaa0b..7746da7a 100644 --- a/src/html/logo.rs +++ b/src/html/logo.rs @@ -1,7 +1,7 @@ -use crate::core::component::Context; -use crate::html::{html, Markup}; -use crate::locale::L10n; use crate::AutoDefault; +use crate::core::component::Context; +use crate::html::{Markup, html}; +use crate::locale::L10n; /// Representación SVG del **logotipo de PageTop** para incrustar en HTML. /// diff --git a/src/locale/definition.rs b/src/locale/definition.rs index 06a07c49..bffc805c 100644 --- a/src/locale/definition.rs +++ b/src/locale/definition.rs @@ -1,7 +1,7 @@ use crate::{global, trace}; use super::languages::LANGUAGES; -use super::{langid, LanguageIdentifier}; +use super::{LanguageIdentifier, langid}; use std::sync::LazyLock; diff --git a/src/locale/l10n.rs b/src/locale/l10n.rs index af5e9535..e75e103d 100644 --- a/src/locale/l10n.rs +++ b/src/locale/l10n.rs @@ -1,5 +1,5 @@ use crate::html::{Markup, PreEscaped}; -use crate::{include_locales, AutoDefault, CowStr}; +use crate::{AutoDefault, CowStr, include_locales}; use super::{LangId, Locale}; diff --git a/src/locale/languages.rs b/src/locale/languages.rs index f1962a14..cda4483d 100644 --- a/src/locale/languages.rs +++ b/src/locale/languages.rs @@ -1,6 +1,6 @@ use crate::util; -use super::{langid, LanguageIdentifier}; +use super::{LanguageIdentifier, langid}; use std::collections::HashMap; use std::sync::LazyLock; From 019961ed77d1ec5f9674d11232e4a49812d59c54 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 31 May 2026 00:51:48 +0200 Subject: [PATCH 15/29] =?UTF-8?q?=F0=9F=9A=9A=20Actualiza=20rutas=20de=20a?= =?UTF-8?q?ssets=20est=C3=A1ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los CSS e imágenes propios de PageTop se sirven bajo `/pagetop/`, por lo que las referencias a `/css/` e `/img/` deben incluir ese prefijo. --- extensions/pagetop-aliner/src/lib.rs | 4 ++-- src/base/component/intro.rs | 2 +- src/base/theme/basic.rs | 4 ++-- static/css/intro.css | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index e88a9142..42a89e48 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -117,12 +117,12 @@ impl Extension for Aliner { impl Theme for Aliner { fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/normalize.css") + StyleSheet::from("/pagetop/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/basic.css") + StyleSheet::from("/pagetop/css/basic.css") .with_version(PAGETOP_VERSION) .with_weight(-99), )) diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index a7ccb2c4..63902f10 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -114,7 +114,7 @@ impl Component for Intro { fn prepare(&self, cx: &mut Context) -> Result { cx.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION), + StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION), )); if *self.opening() == IntroOpening::PageTop { cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx| diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 34f9088b..3e6e99e9 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -13,12 +13,12 @@ impl Extension for Basic { impl Theme for Basic { fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/normalize.css") + StyleSheet::from("/pagetop/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/basic.css") + StyleSheet::from("/pagetop/css/basic.css") .with_version(PAGETOP_VERSION) .with_weight(-99), )) diff --git a/static/css/intro.css b/static/css/intro.css index 1cc03ffc..00fe0d21 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -1,8 +1,8 @@ :root { - --intro-bg-img: url('/img/intro-header.jpg'); - --intro-bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg')); - --intro-bg-img-sm: url('/img/intro-header-sm.jpg'); - --intro-bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg')); + --intro-bg-img: url('/pagetop/img/intro-header.jpg'); + --intro-bg-img-set: image-set(url('/pagetop/img/intro-header.avif') type('image/avif'), url('/pagetop/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg')); + --intro-bg-img-sm: url('/pagetop/img/intro-header-sm.jpg'); + --intro-bg-img-sm-set: image-set(url('/pagetop/img/intro-header-sm.avif') type('image/avif'), url('/pagetop/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg')); --intro-bg-color: #7a430e; --intro-bg-block-1: #ffb84b; --intro-bg-block-2: #ffc66f; From c1afe0e70c7405d23c50b361df20f93ef0b27441 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 31 May 2026 23:38:43 +0200 Subject: [PATCH 16/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migra=20API=20p?= =?UTF-8?q?=C3=BAblica=20de=20actix-web=20a=20Axum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `configure_service` como `configure_router(Router) -> Router`. - Macro `static_files_service!` como `serve_static_files!`. - `ResultPage` eliminado; handlers devuelven `Result`. - `ErrorPage` implementa `IntoResponse` en lugar de `ResponseError`. - Registro con `OnceLock`; eliminados `drop_extensions` y `app.welcome`. - `Redirect` devuelve `Response`; docs y ejemplos actualizados. --- README.md | 2 +- examples/form-controls.rs | 6 +- examples/hello-name.rs | 9 +- examples/hello-world.rs | 6 +- examples/intro-colors.rs | 6 +- examples/navbar-menus.rs | 6 +- extensions/pagetop-aliner/README.md | 2 +- extensions/pagetop-aliner/src/lib.rs | 7 +- extensions/pagetop-bootsier/README.md | 2 +- extensions/pagetop-bootsier/src/lib.rs | 9 +- .../src/theme/image/component.rs | 2 +- helpers/pagetop-build/README.md | 4 +- helpers/pagetop-build/src/lib.rs | 15 +- src/app.rs | 4 +- src/base/extension/welcome.rs | 6 +- src/core/extension/all.rs | 117 +++------ src/core/extension/definition.rs | 111 +++++++-- src/global.rs | 34 ++- src/html/assets/favicon.rs | 16 +- src/html/assets/javascript.rs | 2 +- src/html/assets/stylesheet.rs | 2 +- src/html/route.rs | 2 +- src/lib.rs | 2 +- src/locale/request.rs | 2 +- src/prelude.rs | 6 +- src/response/json.rs | 10 +- src/response/page.rs | 12 +- src/response/page/error.rs | 61 ++--- src/response/redirect.rs | 60 +++-- src/web.rs | 225 +++++++++--------- 30 files changed, 393 insertions(+), 355 deletions(-) diff --git a/README.md b/README.md index 604c4b3c..89fade06 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ impl Extension for HelloWorld { } } -async fn hello_world(request: HttpRequest) -> ResultPage { +async fn hello_world(request: HttpRequest) -> Result { Page::new(request) .add_child(Html::with(|_| html! { h1 { "Hello World!" } })) .render() diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 4a6fc6c0..1c7f066e 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -11,12 +11,12 @@ impl Extension for FormControls { vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(form_controls)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(form_controls)) } } -async fn form_controls(request: HttpRequest) -> ResultPage { +async fn form_controls(request: HttpRequest) -> Result { Page::new(request) .with_child( Intro::default() diff --git a/examples/hello-name.rs b/examples/hello-name.rs index e2904c6f..71439c7d 100644 --- a/examples/hello-name.rs +++ b/examples/hello-name.rs @@ -3,16 +3,15 @@ use pagetop::prelude::*; struct HelloName; impl Extension for HelloName { - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/hello/{name}", service::web::get().to(hello_name)); + fn configure_router(&self, router: Router) -> Router { + router.route("/hello/{name}", web::get(hello_name)) } } async fn hello_name( request: HttpRequest, - path: service::web::Path, -) -> ResultPage { - let name = path.into_inner(); + web::Path(name): web::Path, +) -> Result { Page::new(request) .with_child(Html::with(move |_| { html! { diff --git a/examples/hello-world.rs b/examples/hello-world.rs index e6127af9..f1c40d23 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -3,12 +3,12 @@ use pagetop::prelude::*; struct HelloWorld; impl Extension for HelloWorld { - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(hello_world)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(hello_world)) } } -async fn hello_world(request: HttpRequest) -> ResultPage { +async fn hello_world(request: HttpRequest) -> Result { Page::new(request) .with_child(Html::with(|_| { html! { diff --git a/examples/intro-colors.rs b/examples/intro-colors.rs index 57ddeed4..b219c5be 100644 --- a/examples/intro-colors.rs +++ b/examples/intro-colors.rs @@ -5,12 +5,12 @@ include_locales!(LOC from "examples/locale"); struct IntroColors; impl Extension for IntroColors { - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(intro_colors)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(intro_colors)) } } -async fn intro_colors(request: HttpRequest) -> ResultPage { +async fn intro_colors(request: HttpRequest) -> Result { Page::new(request) .with_child( Intro::default() diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 38918aed..7f8ccdda 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -8,7 +8,11 @@ struct SuperMenu; impl Extension for SuperMenu { fn dependencies(&self) -> Vec { - vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier] + vec![ + &pagetop_aliner::Aliner, + &pagetop_bootsier::Bootsier, + &pagetop::base::extension::Welcome, + ] } fn initialize(&self) { diff --git a/extensions/pagetop-aliner/README.md b/extensions/pagetop-aliner/README.md index 7b772591..bf515d66 100644 --- a/extensions/pagetop-aliner/README.md +++ b/extensions/pagetop-aliner/README.md @@ -64,7 +64,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_aliner::Aliner; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Aliner) .add_child( diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index 42a89e48..dedf4e19 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_aliner::Aliner; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Aliner) .with_child( @@ -109,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 } } diff --git a/extensions/pagetop-bootsier/README.md b/extensions/pagetop-bootsier/README.md index edb0be75..f71f221e 100644 --- a/extensions/pagetop-bootsier/README.md +++ b/extensions/pagetop-bootsier/README.md @@ -64,7 +64,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_bootsier::Bootsier; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Bootsier) .add_child( diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index ca2a80c8..8c0ec847 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta: use pagetop::prelude::*; use pagetop_bootsier::Bootsier; -async fn homepage(request: HttpRequest) -> ResultPage { +async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Bootsier) .with_child( @@ -140,9 +140,10 @@ impl Extension for Bootsier { Some(&Self) } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs"); - static_files_service!(scfg, [bootsier_js] => "/bootsier/js"); + fn configure_router(&self, router: Router) -> Router { + serve_static_files!(router, [bootsier_bs] => "/bootsier/bs"); + serve_static_files!(router, [bootsier_js] => "/bootsier/js"); + router } } diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index 678ccdb3..df2c28a7 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -55,7 +55,7 @@ impl Component for Image { { (logo.render(cx)) } - }) + }); } image::Source::Responsive(source) => Some(source), image::Source::Thumbnail(source) => Some(source), diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md index c5d9c5bd..bb7d3bfa 100644 --- a/helpers/pagetop-build/README.md +++ b/helpers/pagetop-build/README.md @@ -94,7 +94,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro usen nombres diferentes. Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos -en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html) +en [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: ```rust,ignore @@ -105,7 +105,7 @@ pub struct MyExtension; impl Extension for MyExtension { // Servicio web que publica los recursos de `guides` en `/ruta/a/guides`. fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, guides => "/ruta/a/guides"); + serve_static_files!(scfg, guides => "/ruta/a/guides"); } } ``` diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index 774a4af7..f8390ee6 100644 --- a/helpers/pagetop-build/src/lib.rs +++ b/helpers/pagetop-build/src/lib.rs @@ -95,7 +95,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro usen nombres diferentes. Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos -en [`static_files_service!`](https://docs.rs/pagetop/latest/pagetop/macro.static_files_service.html) +en [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: ```rust,ignore @@ -104,9 +104,10 @@ use pagetop::prelude::*; pub struct MyExtension; impl Extension for MyExtension { - /// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`. - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - static_files_service!(scfg, guides => "/ruta/a/guides"); + /// Registra los recursos de `guides` en el router bajo `/ruta/a/guides`. + fn configure_router(&self, mut router: Router) -> Router { + serve_static_files!(router, [guides] => "/ruta/a/guides"); + router } } ``` @@ -116,10 +117,10 @@ impl Extension for MyExtension { html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" )] -use grass::{from_path, Options, OutputStyle}; -use pagetop_statics::{resource_dir, ResourceDir}; +use grass::{Options, OutputStyle, from_path}; +use pagetop_statics::{ResourceDir, resource_dir}; -use std::fs::{create_dir_all, remove_dir_all, File}; +use std::fs::{File, create_dir_all, remove_dir_all}; use std::io::Write; use std::path::Path; diff --git a/src/app.rs b/src/app.rs index d24a03d1..4a009fb7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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 { +async fn route_not_found(request: HttpRequest) -> Result { Err(ErrorPage::NotFound(request)) } diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs index b8739a40..b6d09dbe 100644 --- a/src/base/extension/welcome.rs +++ b/src/base/extension/welcome.rs @@ -20,12 +20,12 @@ impl Extension for Welcome { L10n::l("welcome_extension_description") } - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - scfg.route("/", service::web::get().to(home)); + fn configure_router(&self, router: Router) -> Router { + router.route("/", web::get(home)) } } -async fn home(request: HttpRequest) -> ResultPage { +async fn home(request: HttpRequest) -> Result { let app = &global::SETTINGS.app.name; Page::new(request) diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index b787c9a8..f9081d83 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -1,60 +1,43 @@ use crate::core::action::add_action; use crate::core::extension::ExtensionRef; use crate::core::theme::all::THEMES; -use crate::{global, service, static_files_service, trace}; +use crate::web::Router; +use crate::{global, serve_static_files, trace, web}; -use parking_lot::RwLock; +use std::sync::OnceLock; -use std::sync::LazyLock; - -// **< EXTENSIONES >******************************************************************************** - -static ENABLED_EXTENSIONS: LazyLock>> = - LazyLock::new(|| RwLock::new(Vec::new())); - -static DROPPED_EXTENSIONS: LazyLock>> = - LazyLock::new(|| RwLock::new(Vec::new())); +static EXTENSIONS: OnceLock> = OnceLock::new(); // **< REGISTRO DE LAS EXTENSIONES >**************************************************************** pub fn register_extensions(root_extension: Option) { - // Prepara la lista de extensiones habilitadas. - let mut enabled_list: Vec = Vec::new(); + // Garantiza que ocurre sólo una vez cuando los tests se ejecutan en paralelo. + EXTENSIONS.get_or_init(|| { + let mut list: Vec = Vec::new(); - // Primero añade el tema básico a la lista de extensiones habilitadas. - add_to_enabled(&mut enabled_list, &crate::base::theme::Basic); + // Primero añade el tema básico a la lista de extensiones habilitadas. + add_to_enabled(&mut list, &crate::base::theme::Basic); - // Si se proporciona una extensión raíz inicial, se añade a la lista de extensiones habilitadas. - if let Some(extension) = root_extension { - add_to_enabled(&mut enabled_list, extension); - } + // Si se proporciona la extensión raíz inicial, se añade a las extensiones habilitadas. + if let Some(extension) = root_extension { + add_to_enabled(&mut list, extension); + } - // Añade la página de bienvenida predefinida si se habilita en la configuración. - if global::SETTINGS.app.welcome { - add_to_enabled(&mut enabled_list, &crate::base::extension::Welcome); - } + // Añade la página de bienvenida si no hay extensión raíz. + if root_extension.is_none() { + add_to_enabled(&mut list, &crate::base::extension::Welcome); + } - // Guarda la lista final de extensiones habilitadas. - ENABLED_EXTENSIONS.write().append(&mut enabled_list); - - // Prepara una lista de extensiones deshabilitadas. - let mut dropped_list: Vec = Vec::new(); - - // Si se proporciona una extensión raíz, analiza su lista de dependencias. - if let Some(extension) = root_extension { - add_to_dropped(&mut dropped_list, extension); - } - - // Guarda la lista final de extensiones deshabilitadas. - DROPPED_EXTENSIONS.write().append(&mut dropped_list); + list + }); } fn add_to_enabled(list: &mut Vec, extension: ExtensionRef) { // Verifica que la extensión no esté en la lista para evitar duplicados. if !list.iter().any(|e| e.type_id() == extension.type_id()) { // Añade primero (en orden inverso) las dependencias de la extensión. - for d in extension.dependencies().iter().rev() { - add_to_enabled(list, *d); + for d in extension.dependencies().into_iter().rev() { + add_to_enabled(list, d); } // Añade la propia extensión a la lista. @@ -77,40 +60,11 @@ fn add_to_enabled(list: &mut Vec, extension: ExtensionRef) { } } -fn add_to_dropped(list: &mut Vec, extension: ExtensionRef) { - // Recorre las extensiones que la actual recomienda deshabilitar. - for d in &extension.drop_extensions() { - // Verifica que la extensión no esté ya en la lista. - if !list.iter().any(|e| e.type_id() == d.type_id()) { - // Comprueba si la extensión está habilitada. Si es así, registra una advertencia. - if ENABLED_EXTENSIONS - .read() - .iter() - .any(|e| e.type_id() == extension.type_id()) - { - trace::warn!( - "Trying to drop \"{}\" extension which is enabled", - extension.short_name() - ); - } else { - // Si la extensión no está habilitada, se añade a la lista y registra la acción. - list.push(*d); - trace::debug!("Extension \"{}\" dropped", d.short_name()); - // Añade recursivamente las dependencias de la extensión eliminada. - // De este modo, todas las dependencias se tienen en cuenta para ser deshabilitadas. - for dependency in &extension.dependencies() { - add_to_dropped(list, *dependency); - } - } - } - } -} - // **< REGISTRO DE LAS ACCIONES >******************************************************************* pub fn register_actions() { - for extension in ENABLED_EXTENSIONS.read().iter() { - for a in extension.actions().into_iter() { + for extension in EXTENSIONS.get().into_iter().flatten() { + for a in extension.actions() { add_action(a); } } @@ -120,25 +74,28 @@ pub fn register_actions() { pub fn initialize_extensions() { trace::info!("Calling application bootstrap"); - for extension in ENABLED_EXTENSIONS.read().iter() { - extension.initialize(); + for e in EXTENSIONS.get().into_iter().flatten() { + e.initialize(); } } -// **< CONFIGURA LOS SERVICIOS >******************************************************************** +// **< CONFIGURA LAS RUTAS >************************************************************************ -pub fn configure_services(scfg: &mut service::web::ServiceConfig) { +pub fn configure_routes(router: Router) -> Router { // Sólo compila durante el desarrollo, para evitar errores 400 en la traza de eventos. #[cfg(debug_assertions)] - scfg.route( - // Ruta automática lanzada por Chrome DevTools. + let router = router.route( "/.well-known/appspecific/com.chrome.devtools.json", - service::web::get().to(|| async { service::HttpResponse::NotFound().finish() }), + web::get(|| async { web::http::StatusCode::NOT_FOUND }), ); - for extension in ENABLED_EXTENSIONS.read().iter() { - extension.configure_service(scfg); - } + let router = EXTENSIONS + .get() + .into_iter() + .flatten() + .fold(router, |r, e| e.configure_router(r)); - static_files_service!(scfg, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/"); + serve_static_files!(router, [&global::SETTINGS.dev.pagetop_static_dir, assets] => "/pagetop"); + + router } diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index a5d2b723..984a5cc1 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -1,8 +1,9 @@ +use crate::actions; +use crate::core::AnyInfo; use crate::core::action::ActionBox; use crate::core::theme::ThemeRef; -use crate::core::AnyInfo; use crate::locale::L10n; -use crate::{actions, service}; +use crate::web::Router; /// Interfaz común que debe implementar cualquier extensión de PageTop. /// @@ -11,15 +12,15 @@ use crate::{actions, service}; /// /// ```rust /// # use pagetop::prelude::*; -/// pub struct Blog; +/// pub struct MyExtension; /// -/// impl Extension for Blog { +/// impl Extension for MyExtension { /// fn name(&self) -> L10n { -/// L10n::n("Blog") +/// L10n::n("My Extension") /// } /// /// fn description(&self) -> L10n { -/// L10n::n("Blog system") +/// L10n::n("Does something useful") /// } /// } /// ``` @@ -86,31 +87,95 @@ pub trait Extension: AnyInfo + Send + Sync { /// aceptar cualquier petición HTTP. fn initialize(&self) {} - /// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros - /// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig). + /// Registra rutas, servicios y capas de la extensión en el servidor web de la aplicación. /// - /// # Ejemplo + /// Recibe las rutas acumuladas hasta ese momento, añade lo que la extensión necesite y retorna + /// las rutas con las nuevas modificaciones. La implementación por defecto devuelve las rutas + /// sin cambios. /// - /// ```rust,ignore + /// # Operaciones disponibles + /// + /// | Operación | Llamada sobre `router` | + /// |------------------------------------|-------------------------------------------------| + /// | Ruta HTTP | `.route("/path", web::get(handler))` | + /// | Rutas bajo prefijo común | `.nest("/prefix", sub_router)` | + /// | Archivos estáticos | `serve_static_files!(router, [...] => "/path")` | + /// | Capa de *middleware* | `.layer(some_layer)` | + /// | Estado compartido entre *handlers* | `.with_state(my_state)` | + /// + /// # Ejemplos + /// + /// ## Rutas HTTP básicas + /// + /// ```rust /// # use pagetop::prelude::*; - /// pub struct ExtensionSample; + /// # async fn list_posts() -> &'static str { "" } + /// # async fn view_post() -> &'static str { "" } + /// # async fn create_post() -> &'static str { "" } + /// pub struct Blog; /// - /// impl Extension for ExtensionSample { - /// fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - /// scfg.route("/sample", web::get().to(route_sample)); + /// impl Extension for Blog { + /// fn configure_router(&self, router: Router) -> Router { + /// router + /// .route("/posts", web::get(list_posts)) + /// .route("/posts/{id}", web::get(view_post)) + /// .route("/posts/new", web::post(create_post)) /// } /// } /// ``` - #[allow(unused_variables)] - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {} - - /// Permite declarar extensiones destinadas a deshabilitar o desinstalar recursos de otras - /// extensiones asociadas a versiones anteriores de la aplicación. /// - /// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras - /// implementaciones. - fn drop_extensions(&self) -> Vec { - vec![] + /// ## Rutas agrupadas bajo un prefijo + /// + /// ```rust + /// # use pagetop::prelude::*; + /// # async fn dashboard() -> &'static str { "" } + /// # async fn list_users() -> &'static str { "" } + /// pub struct Admin; + /// + /// impl Extension for Admin { + /// fn configure_router(&self, router: Router) -> Router { + /// let admin = Router::new() + /// .route("/dashboard", web::get(dashboard)) + /// .route("/users", web::get(list_users)); + /// + /// router.nest("/admin", admin) + /// } + /// } + /// ``` + /// + /// ## Rutas con capa de *middleware* + /// + /// ```rust,ignore + /// # use pagetop::prelude::*; + /// pub struct Api; + /// + /// impl Extension for Api { + /// fn configure_router(&self, router: Router) -> Router { + /// router + /// .route("/api/data", web::get(get_data)) + /// .layer(auth_layer()) + /// } + /// } + /// ``` + /// + /// ## Archivos estáticos + /// + /// La macro [`serve_static_files!`](crate::serve_static_files) sombrea `router` internamente, + /// por lo que el parámetro no necesita `mut`. Sí es necesario devolverlo al final. + /// + /// ```rust,ignore + /// # use pagetop::prelude::*; + /// pub struct MyExtension; + /// + /// impl Extension for MyExtension { + /// fn configure_router(&self, router: Router) -> Router { + /// serve_static_files!(router, [assets] => "/static"); + /// router + /// } + /// } + /// ``` + fn configure_router(&self, router: Router) -> Router { + router } } diff --git a/src/global.rs b/src/global.rs index 953dfb6d..d6bdbc47 100644 --- a/src/global.rs +++ b/src/global.rs @@ -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, diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 2a4f26ed..9d0fb688 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -13,7 +13,7 @@ use crate::{AutoDefault, CowStr}; /// /// > **Nota** /// > Los archivos de los iconos deben estar disponibles en el servidor web de la aplicación. Pueden -/// > servirse usando [`static_files_service!`](crate::static_files_service). +/// > servirse usando [`serve_static_files!`](crate::serve_static_files). /// /// # Ejemplo /// @@ -165,14 +165,12 @@ impl Favicon { } } - /// Centraliza la creación de los elementos ``. - /// - /// - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.). - /// - `href`: URL del recurso. - /// - `sizes`: tamaños opcionales. - /// - `color`: color opcional (solo relevante para `mask-icon`). - /// - /// También infiere automáticamente el tipo MIME (`type`) según la extensión del archivo. + // Crea un elemento para el favicon. Infiere el tipo MIME según la extensión. + // + // - `icon_rel`: indica el tipo de recurso (`"icon"`, `"apple-touch-icon"`, etc.). + // - `href`: URL del recurso. + // - `sizes`: tamaños opcionales. + // - `color`: color opcional (solo relevante para `mask-icon`). fn add_icon_item( mut self, icon_rel: &'static str, diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index e348bebc..6af0fd55 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -39,7 +39,7 @@ enum Source { /// /// > **Nota** /// > Los archivos de los scripts deben estar disponibles en el servidor web de la aplicación. -/// > Pueden servirse usando [`static_files_service!`](crate::static_files_service). +/// > Pueden servirse usando [`serve_static_files!`](crate::serve_static_files). /// /// # Ejemplo /// diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index 5941db9d..fb71fd44 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -56,7 +56,7 @@ impl TargetMedia { /// /// > **Nota** /// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden -/// > servirse usando [`static_files_service!`](crate::static_files_service). +/// > servirse usando [`serve_static_files!`](crate::serve_static_files). /// /// # Ejemplo /// diff --git a/src/html/route.rs b/src/html/route.rs index a1efb0d8..ae694857 100644 --- a/src/html/route.rs +++ b/src/html/route.rs @@ -1,4 +1,4 @@ -use crate::{builder_fn, AutoDefault, CowStr}; +use crate::{AutoDefault, CowStr, builder_fn}; use std::fmt; diff --git a/src/lib.rs b/src/lib.rs index 918ecd02..d4712a0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ impl Extension for HelloWorld { } } -async fn hello_world(request: HttpRequest) -> ResultPage { +async fn hello_world(request: HttpRequest) -> Result { Page::new(request) .with_child(Html::with(|_| html! { h1 { "Hello World!" } })) .render() diff --git a/src/locale/request.rs b/src/locale/request.rs index 6f3af13d..53e4e032 100644 --- a/src/locale/request.rs +++ b/src/locale/request.rs @@ -1,5 +1,5 @@ use crate::global; -use crate::service::HttpRequest; +use crate::web::HttpRequest; use super::{LangId, LanguageIdentifier, Locale}; diff --git a/src/prelude.rs b/src/prelude.rs index 32ce68b7..5e6f7ec1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -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::*; diff --git a/src/response/json.rs b/src/response/json.rs index 23b8ab2c..7ef4b402 100644 --- a/src/response/json.rs +++ b/src/response/json.rs @@ -1,4 +1,4 @@ -//! Extractor y generador de respuestas JSON (reexporta [`actix_web::web::Json`]). +//! Extractor y generador de respuestas JSON (reexporta [`axum::Json`]). //! //! # Uso como extractor JSON //! @@ -11,10 +11,10 @@ //! struct NuevoUsuario { nombre: String, email: String } //! //! /// Manejador configurado para la ruta POST "/usuarios". -//! async fn crear_usuario(payload: Json) -> HttpResponse { +//! async fn crear_usuario(payload: Json) -> web::http::StatusCode { //! // `payload` ya es `NuevoUsuario`; si la deserialización falla, -//! // devolverá automáticamente 400 Bad Request con un cuerpo JSON que describe el error. -//! HttpResponse::Ok().finish() +//! // devolverá automáticamente 400 Bad Request. +//! web::http::StatusCode::OK //! } //! ``` //! @@ -36,4 +36,4 @@ //! `Json` funciona con cualquier tipo que implemente `serde::Serialize` (para respuestas) y/o //! `serde::Deserialize` (para peticiones). -pub use actix_web::web::Json; +pub use axum::Json; diff --git a/src/response/page.rs b/src/response/page.rs index d8bd4b16..2376fd39 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -16,18 +16,16 @@ mod error; pub use error::ErrorPage; -pub use actix_web::Result as ResultPage; - use crate::base::action; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; -use crate::html::{html, Markup, DOCTYPE}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Attr, AttrId}; use crate::html::{Classes, ClassesOp}; +use crate::html::{DOCTYPE, Markup, html}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; -use crate::service::HttpRequest; -use crate::{builder_fn, AutoDefault}; +use crate::web::HttpRequest; +use crate::{AutoDefault, builder_fn}; // **< ReservedRegion >***************************************************************************** @@ -227,8 +225,8 @@ impl Page { /// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos /// `lang` y `dir` en la etiqueta ``. /// 8. Compone el documento HTML completo (``, ``, ``, ``) y - /// devuelve un [`ResultPage`] con el [`Markup`] final. - pub fn render(&mut self) -> ResultPage { + /// devuelve un [`Result`] con el [`Markup`] final. + pub fn render(&mut self) -> Result { // Acciones específicas del tema antes de renderizar el . self.context.theme().before_render_page_body(self); diff --git a/src/response/page/error.rs b/src/response/page/error.rs index fd9959c2..2c92d02e 100644 --- a/src/response/page/error.rs +++ b/src/response/page/error.rs @@ -1,9 +1,7 @@ use crate::core::component::Contextual; use crate::locale::L10n; -use crate::response::ResponseError; -use crate::service::http::{header::ContentType, StatusCode}; -use crate::service::{HttpRequest, HttpResponse}; use crate::util; +use crate::web::{HttpRequest, IntoResponse, Response, http}; use super::Page; @@ -31,13 +29,9 @@ pub enum ErrorPage { } impl ErrorPage { - /// Función auxiliar para renderizar una página de error genérica usando el tema activo. - /// - /// Construye una [`Page`] a partir de la petición y un prefijo de clave basado en el código de - /// estado (`error`), del que se derivan los textos localizados `error_title`, - /// `error_alert` y `error_help`. - /// - /// Si el renderizado falla, escribe en su lugar el texto plano asociado al código de estado. + // Renderiza una página de error genérica usando el tema activo. Deriva las claves de + // localización del código de estado (`error_title`, `_alert`, `_help`). Si el + // renderizado falla, escribe el texto plano del código de estado. fn display_error_page(&self, f: &mut fmt::Formatter<'_>, request: &HttpRequest) -> fmt::Result { let mut page = Page::new(request.clone()); let code = self.status_code(); @@ -51,7 +45,19 @@ impl ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(&code.to_string()) + f.write_str(code.as_str()) + } + } + + /// Devuelve el código de estado HTTP asociado a la variante de error. + pub fn status_code(&self) -> http::StatusCode { + match self { + ErrorPage::BadRequest(_) => http::StatusCode::BAD_REQUEST, + ErrorPage::AccessDenied(_) => http::StatusCode::FORBIDDEN, + ErrorPage::NotFound(_) => http::StatusCode::NOT_FOUND, + ErrorPage::InternalError(_) => http::StatusCode::INTERNAL_SERVER_ERROR, + ErrorPage::ServiceUnavailable(_) => http::StatusCode::SERVICE_UNAVAILABLE, + ErrorPage::GatewayTimeout(_) => http::StatusCode::GATEWAY_TIMEOUT, } } } @@ -69,7 +75,7 @@ impl fmt::Display for ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(&self.status_code().to_string()) + f.write_str(self.status_code().as_str()) } } @@ -80,7 +86,7 @@ impl fmt::Display for ErrorPage { if let Ok(rendered) = page.render() { write!(f, "{}", rendered.into_string()) } else { - f.write_str(&self.status_code().to_string()) + f.write_str(self.status_code().as_str()) } } @@ -96,22 +102,17 @@ impl fmt::Display for ErrorPage { } } -impl ResponseError for ErrorPage { - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()) - .insert_header(ContentType::html()) - .body(self.to_string()) - } - - #[rustfmt::skip] - fn status_code(&self) -> StatusCode { - match self { - ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST, - ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN, - ErrorPage::NotFound(_) => StatusCode::NOT_FOUND, - ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - ErrorPage::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, - ErrorPage::GatewayTimeout(_) => StatusCode::GATEWAY_TIMEOUT, - } +/// Convierte un [`ErrorPage`] en una respuesta HTTP con el código de estado adecuado y el cuerpo +/// HTML generado por el tema activo. +impl IntoResponse for ErrorPage { + fn into_response(self) -> Response { + let status = self.status_code(); + let body = self.to_string(); + ( + status, + [(http::header::CONTENT_TYPE, "text/html; charset=utf-8")], + body, + ) + .into_response() } } diff --git a/src/response/redirect.rs b/src/response/redirect.rs index a3bec0cd..ebe470f8 100644 --- a/src/response/redirect.rs +++ b/src/response/redirect.rs @@ -18,12 +18,12 @@ //! //! - **Respuestas especiales**. -use crate::service::HttpResponse; +use crate::web::{IntoResponse, Response, http}; /// Funciones predefinidas para generar respuestas HTTP de redirección. /// -/// Ofrece atajos para construir respuestas con el código de estado apropiado, añade la cabecera -/// `Location` y la cierra con `.finish()`, evitando repetir la misma secuencia en cada controlador. +/// Ofrece atajos para construir respuestas con el código de estado apropiado y la cabecera +/// `Location`, evitando repetir la misma secuencia en cada controlador. pub struct Redirect; impl Redirect { @@ -34,10 +34,12 @@ impl Redirect { /// Emplear cuando un recurso se ha movido de forma definitiva y la URL antigua debe dejar de /// usarse. #[must_use] - pub fn moved(redirect_to_url: &str) -> HttpResponse { - HttpResponse::MovedPermanently() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn moved(redirect_to_url: &str) -> Response { + ( + http::StatusCode::MOVED_PERMANENTLY, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **permanente**. Código de estado **308**. Mantiene método y cuerpo sin cambios. @@ -45,10 +47,12 @@ impl Redirect { /// Indicada para reorganizaciones de un sitio o aplicación web en las que también existen /// métodos distintos de GET (POST, PUT, ...) que no deben degradarse a GET. #[must_use] - pub fn permanent(redirect_to_url: &str) -> HttpResponse { - HttpResponse::PermanentRedirect() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn permanent(redirect_to_url: &str) -> Response { + ( + http::StatusCode::PERMANENT_REDIRECT, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **temporal**. Código de estado **302**. El método GET (y normalmente HEAD) se @@ -57,10 +61,12 @@ impl Redirect { /// Útil cuando un recurso está fuera de servicio de forma imprevista (mantenimiento breve, /// sobrecarga, ...). #[must_use] - pub fn found(redirect_to_url: &str) -> HttpResponse { - HttpResponse::Found() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn found(redirect_to_url: &str) -> Response { + ( + http::StatusCode::FOUND, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **temporal**. Código de estado **303**. Método GET se mantiene tal cual. Los @@ -69,10 +75,12 @@ impl Redirect { /// Se usa típicamente tras un POST o PUT para aplicar el patrón *Post/Redirect/Get*, permite /// recargar la página de resultados sin volver a ejecutar la operación. #[must_use] - pub fn see_other(redirect_to_url: &str) -> HttpResponse { - HttpResponse::SeeOther() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn see_other(redirect_to_url: &str) -> Response { + ( + http::StatusCode::SEE_OTHER, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Redirección **temporal**. Código de estado **307**. Conserva método y cuerpo íntegros. @@ -80,10 +88,12 @@ impl Redirect { /// Preferible a [`found`](Self::found) cuando el sitio expone operaciones diferentes de GET que /// deben respetarse durante la redirección. #[must_use] - pub fn temporary(redirect_to_url: &str) -> HttpResponse { - HttpResponse::TemporaryRedirect() - .append_header(("Location", redirect_to_url)) - .finish() + pub fn temporary(redirect_to_url: &str) -> Response { + ( + http::StatusCode::TEMPORARY_REDIRECT, + [(http::header::LOCATION, redirect_to_url.to_owned())], + ) + .into_response() } /// Respuesta **especial**. Código de estado **304**. Se envía tras una petición condicional, @@ -92,7 +102,7 @@ impl Redirect { /// /// No es una redirección, el cliente debe reutilizar su copia local. #[must_use] - pub fn not_modified() -> HttpResponse { - HttpResponse::NotModified().finish() + pub fn not_modified() -> Response { + http::StatusCode::NOT_MODIFIED.into_response() } } diff --git a/src/web.rs b/src/web.rs index fb8a4883..63916cd3 100644 --- a/src/web.rs +++ b/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 { ... } +/// async fn my_handler(request: HttpRequest) -> Result { ... } /// ``` #[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 FromRequestParts for HttpRequest { type Rejection = Infallible; // Implementa el extractor de Axum para poder declarar `HttpRequest` como parámetro. - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + async fn from_request_parts( + parts: &mut http::request::Parts, + _state: &S, + ) -> Result { Ok(HttpRequest { uri: parts.uri.clone(), headers: parts.headers.clone(), @@ -94,13 +93,14 @@ impl FromRequestParts 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> for ServeEmbedded { - type Response = Response; +impl tower::Service> for ServeEmbedded { + type Response = http::Response; type Error = Infallible; type Future = std::future::Ready>; @@ -125,7 +125,7 @@ impl tower::Service> for ServeEmbedded { Poll::Ready(Ok(())) } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: http::Request) -> Self::Future { use axum::http::header; // Axum elimina el prefijo de montaje: la ruta restante puede o no comenzar con '/'. @@ -141,12 +141,12 @@ impl tower::Service> 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> 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> 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 [] { + include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); + } + __r = __r.nest_service( + $path, + $crate::web::ServeEmbedded::new( + []::$bundle(), + ), + ); + } + } + __r + }; + }; + // Forma 2: sirve siempre embebido. + ( $router:ident, [$bundle:ident] => $path:expr $(,)? ) => { + let $router = { + let _span = $crate::trace::debug_span!( + "serve_static_files", + mode = "embedded_only", + route = $path, + ) + .entered(); $crate::util::paste! { mod [] { include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); } - $router = $router.nest_service( - $route, + $router.nest_service( + $path, $crate::web::ServeEmbedded::new( []::$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 [] { - include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); - } - $router = $router.nest_service( - $route, - $crate::web::ServeEmbedded::new( - []::$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 { - Request::builder() + pub fn to_request(self) -> http::Request { + 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) -> Response { + pub async fn send_request(router: &Router, req: http::Request) -> http::Response { router.clone().oneshot(req).await.unwrap() } } From 7d43742a11690c6a281cd852ad07883a775cf453 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sun, 31 May 2026 23:43:10 +0200 Subject: [PATCH 17/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(macros):=20Adapta?= =?UTF-8?q?=20`main`=20y=20`test`=20a=20Tokio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `#[pagetop::main]` y `#[pagetop::test]` expanden ahora a `#[tokio::main]` y `#[tokio::test]`, eliminando la dependencia de Actix-web. --- helpers/pagetop-macros/src/lib.rs | 8 ++++---- helpers/pagetop-macros/src/maud/ast.rs | 5 ++--- helpers/pagetop-macros/src/maud/generate.rs | 4 ++-- helpers/pagetop-macros/src/smart_default/body_impl.rs | 6 +++--- helpers/pagetop-macros/src/smart_default/default_attr.rs | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 3a597bff..a0796a0e 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -39,7 +39,7 @@ mod smart_default; use proc_macro::TokenStream; use quote::{quote, quote_spanned}; -use syn::{parse_macro_input, spanned::Spanned, DeriveInput}; +use syn::{DeriveInput, parse_macro_input, spanned::Spanned}; /// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)). #[proc_macro] @@ -164,7 +164,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// documentación se mostrará la entrada del método `with_...()`. #[proc_macro_attribute] pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { - use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type}; + use syn::{FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type, parse2}; let ts: proc_macro2::TokenStream = item.clone().into(); @@ -451,7 +451,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { let mut output: TokenStream = (quote! { - #[::pagetop::service::rt::main(system = "::pagetop::service::rt::System")] + #[::tokio::main] }) .into(); @@ -472,7 +472,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { let mut output: TokenStream = (quote! { - #[::pagetop::service::rt::test(system = "::pagetop::service::rt::System")] + #[::tokio::test] }) .into(); diff --git a/helpers/pagetop-macros/src/maud/ast.rs b/helpers/pagetop-macros/src/maud/ast.rs index ebd53318..cdda2331 100644 --- a/helpers/pagetop-macros/src/maud/ast.rs +++ b/helpers/pagetop-macros/src/maud/ast.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream; use proc_macro2_diagnostics::{Diagnostic, SpanDiagnosticExt}; use quote::ToTokens; use syn::{ - braced, bracketed, + Error, Expr, Ident, Lit, LitBool, LitInt, LitStr, Local, Pat, Stmt, braced, bracketed, ext::IdentExt, parenthesized, parse::{Lookahead1, Parse, ParseStream}, @@ -14,7 +14,6 @@ use syn::{ At, Brace, Bracket, Colon, Comma, Dot, Else, Eq, FatArrow, For, If, In, Let, Match, Minus, Paren, Pound, Question, Semi, Slash, While, }, - Error, Expr, Ident, Lit, LitBool, LitInt, LitStr, Local, Pat, Stmt, }; #[derive(Debug, Clone)] @@ -1079,7 +1078,7 @@ impl ToTokens for MatchArm { pub trait DiagnosticParse: Sized { fn diagnostic_parse(input: ParseStream, diagnostics: &mut Vec) - -> syn::Result; + -> syn::Result; } impl DiagnosticParse for Box { diff --git a/helpers/pagetop-macros/src/maud/generate.rs b/helpers/pagetop-macros/src/maud/generate.rs index 19ff3d76..a3dfb36e 100644 --- a/helpers/pagetop-macros/src/maud/generate.rs +++ b/helpers/pagetop-macros/src/maud/generate.rs @@ -1,6 +1,6 @@ use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, ToTokens}; -use syn::{parse_quote, token::Brace, Expr, Local}; +use quote::{ToTokens, quote}; +use syn::{Expr, Local, parse_quote, token::Brace}; use crate::maud::{ast::*, escape}; diff --git a/helpers/pagetop-macros/src/smart_default/body_impl.rs b/helpers/pagetop-macros/src/smart_default/body_impl.rs index f7d59bb5..68e1663e 100644 --- a/helpers/pagetop-macros/src/smart_default/body_impl.rs +++ b/helpers/pagetop-macros/src/smart_default/body_impl.rs @@ -1,9 +1,9 @@ use proc_macro2::TokenStream; use quote::quote; +use syn::DeriveInput; use syn::parse::Error; use syn::spanned::Spanned; -use syn::DeriveInput; use crate::smart_default::default_attr::{ConversionStrategy, DefaultAttr}; use crate::smart_default::util::find_only; @@ -68,7 +68,7 @@ fn default_body_tt(body: &syn::Fields) -> Result<(TokenStream, String), Error> { let mut doc = String::new(); use std::fmt::Write; let body_tt = match body { - syn::Fields::Named(ref fields) => { + syn::Fields::Named(fields) => { doc.push_str(" {"); let result = { let field_assignments = fields @@ -101,7 +101,7 @@ fn default_body_tt(body: &syn::Fields) -> Result<(TokenStream, String), Error> { doc.push('}'); result } - syn::Fields::Unnamed(ref fields) => { + syn::Fields::Unnamed(fields) => { doc.push('('); let result = { let field_assignments = fields diff --git a/helpers/pagetop-macros/src/smart_default/default_attr.rs b/helpers/pagetop-macros/src/smart_default/default_attr.rs index 8487fc06..90e52f64 100644 --- a/helpers/pagetop-macros/src/smart_default/default_attr.rs +++ b/helpers/pagetop-macros/src/smart_default/default_attr.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::ToTokens; -use syn::{parse::Error, MetaNameValue}; +use syn::{MetaNameValue, parse::Error}; use crate::smart_default::util::find_only; From 87e4eac27c150d1abcd2732d3770b8f8daf2ef10 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 1 Jun 2026 01:01:24 +0200 Subject: [PATCH 18/29] =?UTF-8?q?=F0=9F=94=A5=20(statics):=20Elimina=20c?= =?UTF-8?q?=C3=B3digo=20residual=20de=20actix-web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ResourceFiles` y `UriSegmentError` quedaron sin uso al migrar de actix-web a axum/tower. --- helpers/pagetop-statics/src/lib.rs | 5 +- helpers/pagetop-statics/src/resource_files.rs | 396 ------------------ 2 files changed, 1 insertion(+), 400 deletions(-) delete mode 100644 helpers/pagetop-statics/src/resource_files.rs diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs index d2f147e3..f0135695 100644 --- a/helpers/pagetop-statics/src/lib.rs +++ b/helpers/pagetop-statics/src/lib.rs @@ -47,10 +47,7 @@ pub mod resource; pub use resource::Resource as StaticResource; mod resource_dir; -pub use resource_dir::{resource_dir, ResourceDir}; - -mod resource_files; -pub use resource_files::{ResourceFiles, UriSegmentError}; +pub use resource_dir::{ResourceDir, resource_dir}; /// Support for module based generations. Use it for large data sets (more than 128 Mb). pub mod sets; diff --git a/helpers/pagetop-statics/src/resource_files.rs b/helpers/pagetop-statics/src/resource_files.rs deleted file mode 100644 index b487bca9..00000000 --- a/helpers/pagetop-statics/src/resource_files.rs +++ /dev/null @@ -1,396 +0,0 @@ -use super::resource::Resource; -use actix_web::{ - dev::{ - always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, - ServiceRequest, ServiceResponse, - }, - error::Error, - guard::{Guard, GuardContext}, - http::{ - header::{self, ContentType}, - Method, StatusCode, - }, - HttpMessage, HttpRequest, HttpResponse, ResponseError, -}; -use derive_more::{Deref, Display, Error}; -use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready}; -use std::{collections::HashMap, ops::Deref, rc::Rc}; - -/// Static resource files handling -/// -/// `ResourceFiles` service must be registered with `App::service` method. -/// -/// ```rust -/// use std::collections::HashMap; -/// -/// use actix_web::App; -/// -/// fn main() { -/// // serve root directory with default options: -/// // - resolve index.html -/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new(); -/// let app = App::new() -/// .service(pagetop_statics::ResourceFiles::new("/", files)); -/// // or subpath with additional option to not resolve index.html -/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new(); -/// let app = App::new() -/// .service(pagetop_statics::ResourceFiles::new("/imgs", files) -/// .do_not_resolve_defaults()); -/// } -/// ``` -#[allow(clippy::needless_doctest_main)] -pub struct ResourceFiles { - not_resolve_defaults: bool, - use_guard: bool, - not_found_resolves_to: Option, - inner: Rc, -} - -pub struct ResourceFilesInner { - path: String, - files: HashMap<&'static str, Resource>, -} - -const INDEX_HTML: &str = "index.html"; - -impl ResourceFiles { - pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self { - let inner = ResourceFilesInner { - path: path.into(), - files, - }; - Self { - inner: Rc::new(inner), - not_resolve_defaults: false, - not_found_resolves_to: None, - use_guard: false, - } - } - - /// By default trying to resolve '.../' to '.../index.html' if it exists. - /// Turn off this resolution by calling this function. - pub fn do_not_resolve_defaults(mut self) -> Self { - self.not_resolve_defaults = true; - self - } - - /// Resolves not found references to this path. - /// - /// This can be useful for angular-like applications. - pub fn resolve_not_found_to(mut self, path: S) -> Self { - self.not_found_resolves_to = Some(path.to_string()); - self - } - - /// Resolves not found references to root path. - /// - /// This can be useful for angular-like applications. - pub fn resolve_not_found_to_root(self) -> Self { - self.resolve_not_found_to(INDEX_HTML) - } - - /// If this is called, we will use an [actix_web::guard::Guard] to check if this request should be handled. - /// If set to true, we skip using the handler for files that haven't been found, instead of sending 404s. - /// Would be ignored, if `resolve_not_found_to` or `resolve_not_found_to_root` is used. - /// - /// Can be useful if you want to share files on a (sub)path that's also used by a different route handler. - pub fn skip_handler_when_not_found(mut self) -> Self { - self.use_guard = true; - self - } - - fn select_guard(&self) -> Box { - if self.not_resolve_defaults { - Box::new(NotResolveDefaultsGuard::from(self)) - } else { - Box::new(ResolveDefaultsGuard::from(self)) - } - } -} - -impl Deref for ResourceFiles { - type Target = ResourceFilesInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -struct NotResolveDefaultsGuard { - inner: Rc, -} - -impl Guard for NotResolveDefaultsGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - self.inner - .files - .contains_key(ctx.head().uri.path().trim_start_matches('/')) - } -} - -impl From<&ResourceFiles> for NotResolveDefaultsGuard { - fn from(files: &ResourceFiles) -> Self { - Self { - inner: files.inner.clone(), - } - } -} - -struct ResolveDefaultsGuard { - inner: Rc, -} - -impl Guard for ResolveDefaultsGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - let path = ctx.head().uri.path().trim_start_matches('/'); - self.inner.files.contains_key(path) - || ((path.is_empty() || path.ends_with('/')) - && self - .inner - .files - .contains_key((path.to_string() + INDEX_HTML).as_str())) - } -} - -impl From<&ResourceFiles> for ResolveDefaultsGuard { - fn from(files: &ResourceFiles) -> Self { - Self { - inner: files.inner.clone(), - } - } -} - -impl HttpServiceFactory for ResourceFiles { - fn register(self, config: &mut AppService) { - let prefix = self.path.trim_start_matches('/'); - let rdef = if config.is_root() { - ResourceDef::root_prefix(prefix) - } else { - ResourceDef::prefix(prefix) - }; - let guards = if self.use_guard && self.not_found_resolves_to.is_none() { - Some(vec![self.select_guard()]) - } else { - None - }; - config.register_service(rdef, guards, self, None); - } -} - -impl ServiceFactory for ResourceFiles { - type Config = (); - type Response = ServiceResponse; - type Error = Error; - type Service = ResourceFilesService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - ok(ResourceFilesService { - resolve_defaults: !self.not_resolve_defaults, - not_found_resolves_to: self.not_found_resolves_to.clone(), - inner: self.inner.clone(), - }) - .boxed_local() - } -} - -#[derive(Deref)] -pub struct ResourceFilesService { - resolve_defaults: bool, - not_found_resolves_to: Option, - #[deref] - inner: Rc, -} - -impl Service for ResourceFilesService { - type Response = ServiceResponse; - type Error = Error; - type Future = Ready>; - - always_ready!(); - - fn call(&self, req: ServiceRequest) -> Self::Future { - match *req.method() { - Method::HEAD | Method::GET => (), - _ => { - return ok(ServiceResponse::new( - req.into_parts().0, - HttpResponse::MethodNotAllowed() - .insert_header(ContentType::plaintext()) - .insert_header((header::ALLOW, "GET, HEAD")) - .body("This resource only supports GET and HEAD."), - )); - } - } - - let req_path = req.match_info().unprocessed(); - let mut item = self.files.get(req_path); - - if item.is_none() - && self.resolve_defaults - && (req_path.is_empty() || req_path.ends_with('/')) - { - let index_req_path = req_path.to_string() + INDEX_HTML; - item = self.files.get(index_req_path.trim_start_matches('/')); - } - - let (req, response) = if item.is_some() { - let (req, _) = req.into_parts(); - let response = respond_to(&req, item); - (req, response) - } else { - let real_path = match get_pathbuf(req_path) { - Ok(item) => item, - Err(e) => return ok(req.error_response(e)), - }; - - let (req, _) = req.into_parts(); - - let mut item = self.files.get(real_path.as_str()); - - if item.is_none() && self.not_found_resolves_to.is_some() { - let not_found_path = self.not_found_resolves_to.as_ref().unwrap(); - item = self.files.get(not_found_path.as_str()); - } - - let response = respond_to(&req, item); - (req, response) - }; - - ok(ServiceResponse::new(req, response)) - } -} - -fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse { - if let Some(file) = item { - let etag = Some(header::EntityTag::new_strong(format!( - "{:x}:{:x}", - file.data.len(), - file.modified - ))); - - let precondition_failed = !any_match(etag.as_ref(), req); - - let not_modified = !none_match(etag.as_ref(), req); - - let mut resp = HttpResponse::build(StatusCode::OK); - resp.insert_header((header::CONTENT_TYPE, file.mime_type)); - - if let Some(etag) = etag { - resp.insert_header(header::ETag(etag)); - } - - if precondition_failed { - return resp.status(StatusCode::PRECONDITION_FAILED).finish(); - } else if not_modified { - return resp.status(StatusCode::NOT_MODIFIED).finish(); - } - - resp.body(file.data) - } else { - HttpResponse::NotFound().body("Not found") - } -} - -/// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - None | Some(header::IfMatch::Any) => true, - Some(header::IfMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.strong_eq(some_etag) { - return true; - } - } - } - false - } - } -} - -/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - Some(header::IfNoneMatch::Any) => false, - Some(header::IfNoneMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.weak_eq(some_etag) { - return false; - } - } - } - true - } - None => true, - } -} - -/// Error type representing invalid characters in a URI path segment. -/// -/// This enum is used to report specific formatting errors in individual segments of a URI path, -/// such as starting, ending, or containing disallowed characters. Each variant wraps the offending -/// character that caused the error. -#[derive(Debug, PartialEq, Display, Error)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[display(fmt = "The segment started with the wrapped invalid character")] - BadStart(#[error(not(source))] char), - - /// The segment contained the wrapped invalid character. - #[display(fmt = "The segment contained the wrapped invalid character")] - BadChar(#[error(not(source))] char), - - /// The segment ended with the wrapped invalid character. - #[display(fmt = "The segment ended with the wrapped invalid character")] - BadEnd(#[error(not(source))] char), -} - -#[cfg(test)] -mod tests_error_impl { - use super::*; - - fn assert_send_and_sync() {} - - #[test] - fn test_error_impl() { - // ensure backwards compatibility when migrating away from failure - assert_send_and_sync::(); - } -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -fn get_pathbuf(path: &str) -> Result { - let mut buf = Vec::new(); - for segment in path.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(buf.join("/")) -} From eb18690a5c9a6ad6846f1e32c6bb8731fc7596bb Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 1 Jun 2026 02:04:02 +0200 Subject: [PATCH 19/29] =?UTF-8?q?=E2=9C=85=20(tests):=20Adapta=20la=20suit?= =?UTF-8?q?e=20al=20nuevo=20framework=20web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sustituye `service::test::*` por `web::test::*` (migración de actix-web a axum). - Extrae `setup()` en los módulos que sólo renderizan componentes, evitando levantar un router completo en cada test. - Elimina los `env::set_var("PAGETOP_RUN_MODE", "test")` manuales, ya cubiertos por la *feature* `testing`. --- tests/component_html.rs | 14 ++++++++------ tests/component_poweredby.rs | 20 ++++++++++++++------ tests/config.rs | 10 ++++------ tests/locale.rs | 12 ++++++------ tests/service.rs | 8 ++++---- tests/util.rs | 34 +++++++++------------------------- 6 files changed, 45 insertions(+), 53 deletions(-) diff --git a/tests/component_html.rs b/tests/component_html.rs index 06d77ec9..d805d079 100644 --- a/tests/component_html.rs +++ b/tests/component_html.rs @@ -46,18 +46,20 @@ async fn component_html_default_renders_empty_markup() { } #[pagetop::test] -async fn component_html_can_access_http_method() { - let req = service::test::TestRequest::with_uri("/").to_http_request(); +async fn component_html_can_access_request_path() { + let req = web::test::TestRequest::get() + .uri("/hello/world") + .to_http_request(); let mut cx = Context::new(Some(req)); let mut component = Html::with(|cx| { - let method = cx + let path = cx .request() - .map(|r| r.method().to_string()) + .map(|r| r.path().to_string()) .unwrap_or_default(); - html! { span { (method) } } + html! { span { (path) } } }); let markup = component.render(&mut cx); - assert_eq!(markup.0, "GET"); + assert_eq!(markup.0, "/hello/world"); } diff --git a/tests/component_poweredby.rs b/tests/component_poweredby.rs index 7dca895d..1cd689cb 100644 --- a/tests/component_poweredby.rs +++ b/tests/component_poweredby.rs @@ -1,8 +1,16 @@ use pagetop::prelude::*; +/// Inicializa PageTop (locale, extensiones…) una sola vez para toda la suite. +/// +/// Los tests de este módulo renderizan componentes directamente con `Context::default()`, por lo +/// que sólo necesitan el subsistema de localización y las extensiones registradas, no un router. +fn setup() { + let _ = Application::new(); +} + #[pagetop::test] async fn poweredby_default_shows_only_pagetop_recognition() { - let _app = service::test::init_service(Application::new().test()).await; + setup(); let mut p = PoweredBy::default(); let html = p.render(&mut Context::default()); @@ -16,7 +24,7 @@ async fn poweredby_default_shows_only_pagetop_recognition() { #[pagetop::test] async fn poweredby_new_includes_current_year_and_app_name() { - let _app = service::test::init_service(Application::new().test()).await; + setup(); let mut p = PoweredBy::new(); let html = p.render(&mut Context::default()); @@ -40,7 +48,7 @@ async fn poweredby_new_includes_current_year_and_app_name() { #[pagetop::test] async fn poweredby_with_copyright_overrides_text() { - let _app = service::test::init_service(Application::new().test()).await; + setup(); let custom = "2001 © FooBar Inc."; let mut p = PoweredBy::default().with_copyright(Some(custom)); @@ -52,7 +60,7 @@ async fn poweredby_with_copyright_overrides_text() { #[pagetop::test] async fn poweredby_with_copyright_none_hides_text() { - let _app = service::test::init_service(Application::new().test()).await; + setup(); let mut p = PoweredBy::new().with_copyright(None::); let html = p.render(&mut Context::default()); @@ -64,7 +72,7 @@ async fn poweredby_with_copyright_none_hides_text() { #[pagetop::test] async fn poweredby_link_points_to_crates_io() { - let _app = service::test::init_service(Application::new().test()).await; + setup(); let mut p = PoweredBy::default(); let html = p.render(&mut Context::default()); @@ -77,7 +85,7 @@ async fn poweredby_link_points_to_crates_io() { #[pagetop::test] async fn poweredby_getter_reflects_internal_state() { - let _app = service::test::init_service(Application::new().test()).await; + setup(); // Por defecto no hay copyright. let p0 = PoweredBy::default(); diff --git a/tests/config.rs b/tests/config.rs index ff7135c0..91fc8419 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -2,8 +2,6 @@ use pagetop::prelude::*; use serde::Deserialize; -use std::env; - include_config!(SETTINGS: Settings => [ "test.string_value" => "Test String", "test.int_value" => -321, @@ -22,10 +20,12 @@ pub struct Test { pub float_value: f32, } +// La *feature* `testing` (activo con `cargo ts` / `cargo tw`) fija el modo "test" en tiempo de +// compilación dentro de `config::CONFIG_VALUES`, de forma que `global::SETTINGS` y cualquier +// `include_config!` local cargan automáticamente la configuración del modo "test". + #[pagetop::test] async fn check_global_config() { - env::set_var("PAGETOP_RUN_MODE", "test"); - assert_eq!(global::SETTINGS.app.run_mode, "test"); assert_eq!(global::SETTINGS.app.name, "Testing"); assert_eq!(global::SETTINGS.server.bind_port, 9000); @@ -33,8 +33,6 @@ async fn check_global_config() { #[pagetop::test] async fn check_local_config() { - env::set_var("PAGETOP_RUN_MODE", "test"); - assert_eq!(SETTINGS.test.string_value, "Modified value"); assert_eq!(SETTINGS.test.int_value, -321); assert_eq!(SETTINGS.test.float_value, 8.7654); diff --git a/tests/locale.rs b/tests/locale.rs index e15d4f75..51ff1863 100644 --- a/tests/locale.rs +++ b/tests/locale.rs @@ -2,7 +2,7 @@ use pagetop::prelude::*; #[pagetop::test] async fn literal_text() { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let l10n = L10n::n("© 2025 PageTop"); assert_eq!(l10n.get(), Some("© 2025 PageTop".to_string())); @@ -10,7 +10,7 @@ async fn literal_text() { #[pagetop::test] async fn translation_without_args() { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let l10n = L10n::l("test_hello_world"); let translation = l10n.lookup(&Locale::resolve("es-ES")); @@ -19,7 +19,7 @@ async fn translation_without_args() { #[pagetop::test] async fn translation_with_args() { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let l10n = L10n::l("test_hello_user").with_arg("userName", "Manuel"); let translation = l10n.lookup(&Locale::resolve("es-ES")); @@ -28,7 +28,7 @@ async fn translation_with_args() { #[pagetop::test] async fn translation_with_plural_and_select() { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let l10n = L10n::l("test_shared_photos").with_args(vec![ ("userName", "Roberto"), @@ -41,7 +41,7 @@ async fn translation_with_plural_and_select() { #[pagetop::test] async fn check_fallback_language() { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let l10n = L10n::l("test_hello_world"); let translation = l10n.lookup(&Locale::resolve("xx-YY")); // Retrocede a "en-US". @@ -50,7 +50,7 @@ async fn check_fallback_language() { #[pagetop::test] async fn check_unknown_key() { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let l10n = L10n::l("non-existent-key"); let translation = l10n.lookup(&Locale::resolve("en-US")); diff --git a/tests/service.rs b/tests/service.rs index 5aec398e..5f2ca87b 100644 --- a/tests/service.rs +++ b/tests/service.rs @@ -2,11 +2,11 @@ use pagetop::prelude::*; #[pagetop::test] async fn homepage_returns_404() { - let app = service::test::init_service(Application::new().test()).await; + let app = web::test::init_router(Application::new().test()); - let req = service::test::TestRequest::get().uri("/").to_request(); - let resp = service::test::call_service(&app, req).await; + let req = web::test::TestRequest::get().uri("/").to_request(); + let resp = web::test::send_request(&app, req).await; // Comprueba el acceso a la ruta de inicio. - assert_eq!(resp.status(), service::http::StatusCode::OK); + assert_eq!(resp.status(), web::http::StatusCode::OK); } diff --git a/tests/util.rs b/tests/util.rs index d7d8dd65..6dafce8a 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -use std::{borrow::Cow, env, fs, io}; +use std::{borrow::Cow, fs, io}; use tempfile::TempDir; // **< Testing normalize_ascii() >****************************************************************** @@ -265,7 +265,7 @@ mod unix { #[pagetop::test] async fn ok_absolute_dir() -> io::Result<()> { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); // /tmp//sub let td = TempDir::new()?; @@ -279,21 +279,13 @@ mod unix { #[pagetop::test] async fn ok_relative_dir_with_manifest() -> io::Result<()> { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let td = TempDir::new()?; let sub = td.path().join("sub"); fs::create_dir(&sub)?; - // Fija CARGO_MANIFEST_DIR para que "sub" se resuelva contra td.path() - let prev_manifest_dir = env::var_os("CARGO_MANIFEST_DIR"); - env::set_var("CARGO_MANIFEST_DIR", td.path()); - let res = util::resolve_absolute_dir("sub"); - // Restaura entorno. - match prev_manifest_dir { - Some(v) => env::set_var("CARGO_MANIFEST_DIR", v), - None => env::remove_var("CARGO_MANIFEST_DIR"), - } + let res = util::resolve_absolute_dir_with_base("sub", Some(td.path().to_path_buf())); assert_eq!(res?, std::fs::canonicalize(&sub)?); Ok(()) @@ -301,7 +293,7 @@ mod unix { #[pagetop::test] async fn error_not_a_directory() -> io::Result<()> { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let td = TempDir::new()?; let file = td.path().join("foo.txt"); @@ -319,7 +311,7 @@ mod windows { #[pagetop::test] async fn ok_absolute_dir() -> io::Result<()> { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); // C:\Users\...\Temp\... let td = TempDir::new()?; @@ -333,21 +325,13 @@ mod windows { #[pagetop::test] async fn ok_relative_dir_with_manifest() -> io::Result<()> { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let td = TempDir::new()?; let sub = td.path().join("sub"); fs::create_dir(&sub)?; - // Fija CARGO_MANIFEST_DIR para que "sub" se resuelva contra td.path() - let prev_manifest_dir = env::var_os("CARGO_MANIFEST_DIR"); - env::set_var("CARGO_MANIFEST_DIR", td.path()); - let res = util::resolve_absolute_dir("sub"); - // Restaura entorno. - match prev_manifest_dir { - Some(v) => env::set_var("CARGO_MANIFEST_DIR", v), - None => env::remove_var("CARGO_MANIFEST_DIR"), - } + let res = resolve_absolute_dir_with_base("sub", Some(td.path().to_path_buf())); assert_eq!(res?, std::fs::canonicalize(&sub)?); Ok(()) @@ -355,7 +339,7 @@ mod windows { #[pagetop::test] async fn error_not_a_directory() -> io::Result<()> { - let _app = service::test::init_service(Application::new().test()).await; + let _app = web::test::init_router(Application::new().test()); let td = TempDir::new()?; let file = td.path().join("foo.txt"); From b1ce79c78f62386cd573c0e8e4e779c794b429cf Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 1 Jun 2026 02:14:07 +0200 Subject: [PATCH 20/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Migra=20soporte=20HT?= =?UTF-8?q?TP=20de=20actix-web=20a=20axum=20en=20maud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1572 ++++++++-------------------------------------- src/html/maud.rs | 48 +- 2 files changed, 278 insertions(+), 1342 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b01826c..433ce60d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,270 +2,12 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.11.1", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-files" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8c4f30e3272d7c345f88ae0aac3848507ef5ba871f9cc2a41c8085a0f0523b" -dependencies = [ - "actix-http", - "actix-service", - "actix-utils", - "actix-web", - "bitflags 2.11.1", - "bytes", - "derive_more 2.1.1", - "futures-core", - "http-range", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "v_htmlescape", -] - -[[package]] -name = "actix-http" -version = "3.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags 2.11.1", - "brotli", - "bytes", - "bytestring", - "derive_more 2.1.1", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.10.1", - "sha1 0.11.0", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-session" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "400c27fd4cdbe0082b7bbd29ac44a3070cbda1b2114138dc106ba39fe2f90dff" -dependencies = [ - "actix-service", - "actix-utils", - "actix-web", - "anyhow", - "derive_more 2.1.1", - "rand 0.9.4", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more 2.1.1", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.3", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "ahash" version = "0.8.12" @@ -293,21 +35,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -379,142 +106,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.4.1", - "futures-lite 2.6.1", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io 2.6.0", - "async-lock 3.4.2", - "blocking", - "futures-lite 2.6.1", - "once_cell", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.6.1", - "parking", - "polling 3.11.0", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io 2.6.0", - "async-lock 3.4.2", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 2.6.1", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -537,12 +128,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.89" @@ -576,10 +161,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "base64" -version = "0.20.0" +name = "axum" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] [[package]] name = "base64" @@ -593,12 +224,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.1" @@ -617,49 +242,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite 2.6.1", - "piper", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bstr" version = "1.12.1" @@ -688,15 +270,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "bytestring" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" -dependencies = [ - "bytes", -] - [[package]] name = "cc" version = "1.2.62" @@ -704,8 +277,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -715,17 +286,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - [[package]] name = "change-detection" version = "1.2.0" @@ -749,16 +309,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout", -] - [[package]] name = "clap" version = "4.6.1" @@ -840,45 +390,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "aes-gcm", - "base64 0.20.0", - "hkdf", - "hmac", - "percent-encoding", - "rand 0.8.6", - "sha2", - "subtle", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -904,15 +415,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc" version = "3.4.0" @@ -987,28 +489,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "darling" version = "0.20.11" @@ -1049,7 +532,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid 0.9.6", + "const-oid", "pem-rfc7468", "zeroize", ] @@ -1063,19 +546,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -1091,7 +561,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -1105,23 +574,12 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.7", + "block-buffer", + "const-oid", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.0", - "const-oid 0.10.2", - "crypto-common 0.2.1", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1148,15 +606,6 @@ dependencies = [ "serde", ] -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1184,12 +633,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -1201,25 +644,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.4.1" @@ -1373,7 +797,6 @@ checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1424,45 +847,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand 2.4.1", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.32" @@ -1481,10 +865,8 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1515,18 +897,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.2" @@ -1535,8 +905,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", + "r-efi", "wasip2", "wasip3", ] @@ -1552,16 +921,6 @@ dependencies = [ "syn", ] -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - [[package]] name = "glob" version = "0.3.3" @@ -1581,18 +940,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "grass" version = "0.13.4" @@ -1615,26 +962,7 @@ dependencies = [ "lasso", "once_cell", "phf", - "rand 0.8.6", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "rand", ] [[package]] @@ -1685,18 +1013,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1718,7 +1034,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1732,20 +1048,42 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", - "fnv", "itoa", ] [[package]] -name = "http-range" -version = "0.1.5" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" @@ -1760,12 +1098,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] -name = "hybrid-array" -version = "0.4.11" +name = "hyper" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ - "typenum", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] @@ -1923,12 +1287,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "2.14.0" @@ -1961,24 +1319,6 @@ dependencies = [ "syn", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "intl-memoizer" version = "0.5.3" @@ -1998,17 +1338,6 @@ dependencies = [ "unic-langid", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2021,16 +1350,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.98" @@ -2043,21 +1362,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lasso" version = "0.7.3" @@ -2100,7 +1404,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", "plain", "redox_syscall 0.7.5", @@ -2117,12 +1421,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2135,23 +1433,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.14" @@ -2166,9 +1447,6 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "value-bag", -] [[package]] name = "matchers" @@ -2179,6 +1457,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -2186,7 +1470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest", ] [[package]] @@ -2228,17 +1512,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.61.2", ] -[[package]] -name = "mutually_exclusive_features" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" - [[package]] name = "native-tls" version = "0.2.18" @@ -2276,7 +1553,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.6", + "rand", "smallvec", "zeroize", ] @@ -2329,19 +1606,13 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl" version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -2415,9 +1686,7 @@ dependencies = [ name = "pagetop" version = "0.5.0" dependencies = [ - "actix-files", - "actix-session", - "actix-web", + "axum", "chrono", "colored", "config", @@ -2435,11 +1704,12 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "substring", "tempfile", "terminal_size", + "tokio", + "tower", + "tower-http", "tracing", - "tracing-actix-web", "tracing-appender", "tracing-subscriber", "unic-langid", @@ -2451,6 +1721,7 @@ version = "0.1.0" dependencies = [ "pagetop", "pagetop-build", + "tokio", ] [[package]] @@ -2460,6 +1731,7 @@ dependencies = [ "pagetop", "pagetop-build", "serde", + "tokio", ] [[package]] @@ -2494,11 +1766,11 @@ name = "pagetop-seaorm" version = "0.0.4" dependencies = [ "async-trait", - "futures", "pagetop", "sea-orm", "sea-schema", "serde", + "tokio", "url", ] @@ -2506,10 +1778,7 @@ dependencies = [ name = "pagetop-statics" version = "0.1.3" dependencies = [ - "actix-web", "change-detection", - "derive_more 0.99.20", - "futures-util", "mime_guess", "path-slash 0.2.1", ] @@ -2608,7 +1877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand", ] [[package]] @@ -2633,49 +1902,12 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand 2.4.1", - "futures-io", -] - [[package]] name = "pkcs1" version = "0.7.5" @@ -2709,48 +1941,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -2844,12 +2034,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2863,29 +2047,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -2895,17 +2058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -2917,28 +2070,13 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -2947,19 +2085,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "bitflags", ] [[package]] @@ -2973,12 +2099,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.10" @@ -2991,14 +2111,14 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", + "const-oid", + "digest", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature", "spki", "subtle", @@ -3020,30 +2140,16 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -3104,7 +2210,7 @@ checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" dependencies = [ "async-stream", "async-trait", - "derive_more 2.1.1", + "derive_more", "futures-util", "log", "ouroboros", @@ -3197,7 +2303,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -3269,6 +2375,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -3297,19 +2414,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -3319,8 +2425,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -3354,8 +2460,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", + "digest", + "rand_core", ] [[package]] @@ -3385,26 +2491,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.3" @@ -3453,14 +2539,12 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "async-io 1.13.0", - "async-std", - "base64 0.22.1", + "base64", "bytes", "crc", "crossbeam-queue", "either", - "event-listener 5.4.1", + "event-listener", "futures-core", "futures-intrusive", "futures-io", @@ -3478,6 +2562,8 @@ dependencies = [ "sha2", "smallvec", "thiserror", + "tokio", + "tokio-stream", "tracing", "url", ] @@ -3501,7 +2587,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ - "async-std", "dotenvy", "either", "heck 0.5.0", @@ -3517,6 +2602,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", + "tokio", "url", ] @@ -3527,12 +2613,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.22.1", - "bitflags 2.11.1", + "base64", + "bitflags", "byteorder", "bytes", "crc", - "digest 0.10.7", + "digest", "dotenvy", "either", "futures-channel", @@ -3549,10 +2635,10 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.6", + "rand", "rsa", "serde", - "sha1 0.10.6", + "sha1", "sha2", "smallvec", "sqlx-core", @@ -3569,8 +2655,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.22.1", - "bitflags 2.11.1", + "base64", + "bitflags", "byteorder", "crc", "dotenvy", @@ -3587,7 +2673,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.6", + "rand", "serde", "serde_json", "sha2", @@ -3658,15 +2744,6 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -[[package]] -name = "substring" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" -dependencies = [ - "autocfg", -] - [[package]] name = "subtle" version = "2.6.1" @@ -3690,6 +2767,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -3707,10 +2790,10 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand 2.4.1", + "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -3720,7 +2803,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -3822,10 +2905,33 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3870,6 +2976,59 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -3882,19 +3041,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-actix-web" -version = "0.7.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca6b15407f9bfcb35f82d0e79e603e1629ece4e91cc6d9e58f890c184dd20af" -dependencies = [ - "actix-web", - "mutually_exclusive_features", - "pin-project", - "tracing", - "uuid", -] - [[package]] name = "tracing-appender" version = "0.2.5" @@ -4062,28 +3208,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "url" version = "2.5.8" @@ -4108,35 +3238,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "v_htmlescape" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" - [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "vcpkg" version = "0.2.15" @@ -4149,12 +3256,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -4208,16 +3309,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.121" @@ -4278,7 +3369,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -4294,22 +3385,6 @@ dependencies = [ "wasite", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -4319,12 +3394,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.62.2" @@ -4390,16 +3459,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -4417,29 +3477,13 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -4448,90 +3492,42 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winnow" version = "1.0.2" @@ -4605,7 +3601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags", "indexmap", "log", "serde", @@ -4768,31 +3764,3 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/src/html/maud.rs b/src/html/maud.rs index 65360360..ab1236e4 100644 --- a/src/html/maud.rs +++ b/src/html/maud.rs @@ -270,46 +270,14 @@ impl Default for PreEscaped { /// ``` pub const DOCTYPE: PreEscaped<&'static str> = PreEscaped(""); -mod actix_support { - extern crate alloc; +mod axum_support { + use super::PreEscaped; + use axum::response::{Html, IntoResponse, Response}; - use core::{ - pin::Pin, - task::{Context, Poll}, - }; - - use crate::html::PreEscaped; - use actix_web::{ - body::{BodySize, MessageBody}, - http::header, - web::Bytes, - HttpRequest, HttpResponse, Responder, - }; - use alloc::string::String; - - impl MessageBody for PreEscaped { - type Error = ::Error; - - fn size(&self) -> BodySize { - self.0.size() - } - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - Pin::new(&mut self.0).poll_next(cx) - } - } - - impl Responder for PreEscaped { - type Body = String; - - fn respond_to(self, _req: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(header::ContentType::html()) - .message_body(self.0) - .unwrap() + /// Convierte un bloque de [`Markup`](super::Markup) en una respuesta HTTP HTML 200. + impl IntoResponse for PreEscaped { + fn into_response(self) -> Response { + Html(self.0).into_response() } } } @@ -318,7 +286,7 @@ mod actix_support { pub mod html_private { extern crate alloc; - use super::{display, Render}; + use super::{Render, display}; use alloc::string::String; use core::fmt::Display; From 2c52af4b9d68bdd5be1b3befa9a4886a0fa7e8f6 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 1 Jun 2026 22:02:23 +0200 Subject: [PATCH 21/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(statics):=20Renombr?= =?UTF-8?q?a=20StaticResource=20a=20StaticFile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarifica la distinción entre un fichero estático individual (`StaticFile`) y el contenedor de varios ficheros (`StaticResources`). --- helpers/pagetop-statics/README.md | 21 ++++++++------------- helpers/pagetop-statics/src/lib.rs | 15 ++++++--------- helpers/pagetop-statics/src/resource.rs | 6 +++--- helpers/pagetop-statics/src/sets.rs | 4 ++-- src/lib.rs | 8 ++++---- src/web.rs | 15 +++++++++------ 6 files changed, 32 insertions(+), 37 deletions(-) diff --git a/helpers/pagetop-statics/README.md b/helpers/pagetop-statics/README.md index 7466dc1f..3184f095 100644 --- a/helpers/pagetop-statics/README.md +++ b/helpers/pagetop-statics/README.md @@ -11,30 +11,25 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## 🗺️ Descripción general +## Descripción general Este *crate* permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de compilación. +## Créditos -## 📚 Créditos - -Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files) -(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y -[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión -[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por -[Alexander Korolev](https://crates.io/users/kilork). - -Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar -`static-files` manualmente como dependencia en su `Cargo.toml`. +Para ello, adapta el código de [static-files](https://crates.io/crates/static_files) (versión +[0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) desarrollado por +[Alexander Korolev](https://crates.io/users/kilork), bajo licencia MIT/Apache 2.0. La implementación +se integra en PageTop para evitar que cada proyecto tenga que declarar `static-files` manualmente +como dependencia en su `Cargo.toml`. ## 🚧 Advertencia diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs index f0135695..d72176c6 100644 --- a/helpers/pagetop-statics/src/lib.rs +++ b/helpers/pagetop-statics/src/lib.rs @@ -26,14 +26,11 @@ compilación. ## Créditos -Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files) -(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y -[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión -[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por -[Alexander Korolev](https://crates.io/users/kilork). - -Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar -`static-files` manualmente como dependencia en su `Cargo.toml`. +Para ello, adapta el código de [static-files](https://crates.io/crates/static_files) (versión +[0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) desarrollado por +[Alexander Korolev](https://crates.io/users/kilork), bajo licencia MIT/Apache 2.0. La implementación +se integra en PageTop para evitar que cada proyecto tenga que declarar `static-files` manualmente +como dependencia en su `Cargo.toml`. */ #![doc(test(no_crate_inject))] @@ -44,7 +41,7 @@ Estas implementaciones se integran en PageTop para evitar que cada proyecto teng /// Resource definition and single module based generation. pub mod resource; -pub use resource::Resource as StaticResource; +pub use resource::Resource as StaticFile; mod resource_dir; pub use resource_dir::{ResourceDir, resource_dir}; diff --git a/helpers/pagetop-statics/src/resource.rs b/helpers/pagetop-statics/src/resource.rs index 0b81969e..62a31ee7 100644 --- a/helpers/pagetop-statics/src/resource.rs +++ b/helpers/pagetop-statics/src/resource.rs @@ -93,9 +93,9 @@ pub fn generate_resources, G: AsRef>( /// ```rust /// use std::collections::HashMap; /// -/// use pagetop_statics::StaticResource; +/// use pagetop_statics::StaticFile; /// -/// fn generate_mapping() -> HashMap<&'static str, StaticResource> { +/// fn generate_mapping() -> HashMap<&'static str, StaticFile> { /// include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs")) /// } /// @@ -221,7 +221,7 @@ pub(crate) fn generate_function_header( ) -> io::Result<()> { writeln!( f, - "#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticResource> {{", + "#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticFile> {{", ) } diff --git a/helpers/pagetop-statics/src/sets.rs b/helpers/pagetop-statics/src/sets.rs index 5e09f1ff..e319de0f 100644 --- a/helpers/pagetop-statics/src/sets.rs +++ b/helpers/pagetop-statics/src/sets.rs @@ -116,7 +116,7 @@ where writeln!( module_file, " -use ::{crate_name}::StaticResource; +use ::{crate_name}::StaticFile; use ::std::collections::HashMap;" )?; @@ -177,7 +177,7 @@ fn create_set_module_file(module_dir: &Path, module_index: usize) -> io::Result< "#[allow(clippy::wildcard_imports)] use super::*; #[allow(clippy::unreadable_literal)] -pub(crate) fn generate({DEFAULT_VARIABLE_NAME}: &mut HashMap<&'static str, StaticResource>) {{", +pub(crate) fn generate({DEFAULT_VARIABLE_NAME}: &mut HashMap<&'static str, StaticFile>) {{", )?; Ok(set_module) diff --git a/src/lib.rs b/src/lib.rs index d4712a0c..9e900335 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,26 +134,26 @@ pub const PAGETOP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub use pagetop_macros::{AutoDefault, builder_fn, html, main, test}; -pub use pagetop_statics::{StaticResource, resource}; +pub use pagetop_statics::{StaticFile, resource}; pub use getter_methods::Getters; /// Contenedor para un conjunto de recursos embebidos. #[derive(AutoDefault)] pub struct StaticResources { - bundle: HashMap<&'static str, StaticResource>, + bundle: HashMap<&'static str, StaticFile>, } impl StaticResources { /// Crea un contenedor para un conjunto de recursos generado por `build.rs` (consultar /// [`pagetop_build`](https://docs.rs/pagetop-build)). - pub fn new(bundle: HashMap<&'static str, StaticResource>) -> Self { + pub fn new(bundle: HashMap<&'static str, StaticFile>) -> Self { Self { bundle } } } impl Deref for StaticResources { - type Target = HashMap<&'static str, StaticResource>; + type Target = HashMap<&'static str, StaticFile>; fn deref(&self) -> &Self::Target { &self.bundle diff --git a/src/web.rs b/src/web.rs index 63916cd3..787a7828 100644 --- a/src/web.rs +++ b/src/web.rs @@ -5,6 +5,8 @@ //! 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 crate::StaticFile; + use std::collections::HashMap; use std::convert::Infallible; use std::task::{Context, Poll}; @@ -25,10 +27,6 @@ pub use axum::response::{IntoResponse, Response}; // Operaciones HTTP para registrar rutas. pub use axum::routing::{delete, get, patch, post, put}; -// Servicios para archivos estáticos (disco y embebidos). -pub use pagetop_statics::StaticResource; -pub use tower_http::services::ServeDir; - // **< HttpRequest >******************************************************************************** /// Representa una petición HTTP. @@ -91,6 +89,11 @@ impl FromRequestParts for HttpRequest { } } +// **< ServeDir >*********************************************************************************** + +// Servicio para archivos estáticos en disco. +pub use tower_http::services::ServeDir; + // **< ServeEmbedded >****************************************************************************** /// Servicio para archivos estáticos embebidos en el binario. @@ -104,12 +107,12 @@ impl FromRequestParts for HttpRequest { /// recursos con un [`Arc`](std::sync::Arc) para evitar copias innecesarias. #[derive(Clone)] pub struct ServeEmbedded { - files: std::sync::Arc>, + files: std::sync::Arc>, } impl ServeEmbedded { /// Crea un nuevo servicio a partir del mapa de recursos embebidos generado por `build.rs`. - pub fn new(files: HashMap<&'static str, StaticResource>) -> Self { + pub fn new(files: HashMap<&'static str, StaticFile>) -> Self { Self { files: std::sync::Arc::new(files), } From 4ccb792db562bec7c347c997da63830c4608ec12 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 1 Jun 2026 22:05:53 +0200 Subject: [PATCH 22/29] =?UTF-8?q?=F0=9F=93=9D=20Corrige=20documentaci?= =?UTF-8?q?=C3=B3n=20de=20la=20extensi=C3=B3n=20Welcome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/extension/welcome.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs index b6d09dbe..34040f40 100644 --- a/src/base/extension/welcome.rs +++ b/src/base/extension/welcome.rs @@ -2,11 +2,11 @@ use crate::prelude::*; /// Página de bienvenida de PageTop. /// -/// Esta extensión se instala por defecto si el ajuste de configuración [`global::App::welcome`] es -/// `true`. Muestra una página de bienvenida de PageTop en la ruta raíz (`/`). +/// Se registra automáticamente cuando la aplicación arranca sin extensión raíz. Muestra una página +/// de bienvenida de PageTop en la ruta raíz (`/`) usando el componente [`Intro`]. /// -/// No obstante, cualquier extensión puede sobrescribir este comportamiento si utiliza estas mismas -/// rutas. +/// También puede incluirse explícitamente como dependencia de la extensión raíz o de cualquier otra +/// extensión dentro de la estructura de la aplicación. /// /// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente. pub struct Welcome; From 3951f1da1a4d4d1bdd68254d89776b5b6354ef8a Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Mon, 1 Jun 2026 23:32:28 +0200 Subject: [PATCH 23/29] =?UTF-8?q?=F0=9F=8E=A8=20Corrige=20orden=20de=20atr?= =?UTF-8?q?ibutos=20externos=20en=20structs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `///` debe preceder a `#[derive(...)]` al ser azúcar de `#[doc = "..."]`. --- extensions/pagetop-bootsier/src/config.rs | 5 +++-- src/global.rs | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/extensions/pagetop-bootsier/src/config.rs b/extensions/pagetop-bootsier/src/config.rs index 6c2365ba..0875d022 100644 --- a/extensions/pagetop-bootsier/src/config.rs +++ b/extensions/pagetop-bootsier/src/config.rs @@ -28,13 +28,14 @@ include_config!(SETTINGS: Settings => [ "bootsier.max_width" => "1440px", ]); -#[derive(Debug, Deserialize)] /// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`]. +#[derive(Debug, Deserialize)] pub struct Settings { pub bootsier: Bootsier, } -#[derive(Debug, Deserialize)] + /// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`]. +#[derive(Debug, Deserialize)] pub struct Bootsier { /// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem". pub max_width: UnitValue, diff --git a/src/global.rs b/src/global.rs index d6bdbc47..82c89394 100644 --- a/src/global.rs +++ b/src/global.rs @@ -44,9 +44,9 @@ include_config!(SETTINGS: Settings => [ // **< Settings >*********************************************************************************** -#[derive(Debug, Deserialize)] /// Tipos para las secciones globales [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log) y /// [`[server]`](Server) de [`SETTINGS`]. +#[derive(Debug, Deserialize)] pub struct Settings { pub app: App, pub dev: Dev, @@ -54,8 +54,8 @@ pub struct Settings { pub server: Server, } -#[derive(Debug, Deserialize)] /// Sección `[app]` de la configuración. Forma parte de [`Settings`]. +#[derive(Debug, Deserialize)] pub struct App { /// Nombre de la aplicación. pub name: String, @@ -88,8 +88,8 @@ pub struct App { pub run_mode: String, } -#[derive(Debug, Deserialize)] /// Sección `[dev]` de la configuración. Forma parte de [`Settings`]. +#[derive(Debug, Deserialize)] pub struct Dev { /// Directorio desde el que servir los archivos estáticos de PageTop. /// @@ -102,8 +102,8 @@ pub struct Dev { pub pagetop_static_dir: String, } -#[derive(Debug, Deserialize)] /// Sección `[log]` de la configuración. Forma parte de [`Settings`]. +#[derive(Debug, Deserialize)] pub struct Log { /// Gestión de trazas y registro de eventos activada (*true*) o desactivada (*false*). pub enabled: bool, @@ -122,8 +122,8 @@ pub struct Log { pub format: LogFormat, } -#[derive(Debug, Deserialize)] /// Sección `[server]` de la configuración. Forma parte de [`Settings`]. +#[derive(Debug, Deserialize)] pub struct Server { /// Dirección de enlace para el servidor web. pub bind_address: String, From dfc1bdbc4c0fdd1cbbc4baee366db0550b39938d Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 2 Jun 2026 00:24:45 +0200 Subject: [PATCH 24/29] =?UTF-8?q?=E2=9C=A8=20(seaorm):=20Incluye=20DbType?= =?UTF-8?q?=20y=20retoca=20docs=20de=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-bootsier/src/config.rs | 4 +- extensions/pagetop-seaorm/src/config.rs | 45 +++++++++++++++++------ src/config.rs | 4 +- src/global.rs | 11 +++--- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/extensions/pagetop-bootsier/src/config.rs b/extensions/pagetop-bootsier/src/config.rs index 0875d022..14ee27e1 100644 --- a/extensions/pagetop-bootsier/src/config.rs +++ b/extensions/pagetop-bootsier/src/config.rs @@ -28,13 +28,13 @@ include_config!(SETTINGS: Settings => [ "bootsier.max_width" => "1440px", ]); -/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`]. +/// Ajustes para la sección [`Bootsier`] de [`SETTINGS`]. #[derive(Debug, Deserialize)] pub struct Settings { pub bootsier: Bootsier, } -/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`]. +/// Sección **`[bootsier]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] pub struct Bootsier { /// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem". diff --git a/extensions/pagetop-seaorm/src/config.rs b/extensions/pagetop-seaorm/src/config.rs index b1276c50..e8b119fa 100644 --- a/extensions/pagetop-seaorm/src/config.rs +++ b/extensions/pagetop-seaorm/src/config.rs @@ -4,12 +4,12 @@ //! //! ```toml //! [database] -//! db_type = "mysql" +//! db_type = "postgres" //! db_name = "db" //! db_user = "user" //! db_pass = "password" //! db_host = "localhost" -//! db_port = 3306 +//! db_port = 5432 //! max_pool_size = 5 //! ``` //! @@ -29,25 +29,28 @@ use serde::Deserialize; include_config!(SETTINGS: Settings => [ // [database] - "database.db_type" => "", - "database.db_name" => "", - "database.db_user" => "", - "database.db_pass" => "", - "database.db_host" => "localhost", + "database.db_type" => "", + "database.db_name" => "", + "database.db_user" => "", + "database.db_pass" => "", + "database.db_host" => "localhost", "database.max_pool_size" => 5, ]); +/// Ajustes para la sección [`Database`] de [`SETTINGS`]. #[derive(Debug, Deserialize)] -/// Tipos para la sección [`[database]`](Database) de [`SETTINGS`]. pub struct Settings { pub database: Database, } +/// Sección **`[database]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] -/// Sección `[database]` de la configuración. Forma parte de [`Settings`]. pub struct Database { - /// Tipo de base de datos: *"mysql"*, *"postgres"* ó *"sqlite"*. - pub db_type: String, + /// Motor de base de datos. + /// + /// Valores aceptados: `"mysql"` (también `"mariadb"`), `"postgres"` (también `"postgresql"`) y + /// `"sqlite"`. Si se omite, la aplicación terminará con un error al arrancar. + pub db_type: DbType, /// Nombre (para mysql/postgres) o referencia (para sqlite) de la base de datos. pub db_name: String, /// Usuario de conexión a la base de datos (para mysql/postgres). @@ -56,9 +59,27 @@ pub struct Database { pub db_pass: String, /// Servidor de conexión a la base de datos (para mysql/postgres). pub db_host: String, - /// Puerto de conexión a la base de datos (para mysql/postgres). Si es `None` se usa el puerto + /// Puerto de conexión a la base de datos (para mysql/postgres). Si se omite, se usa el puerto /// predeterminado para el motor: 3306 para MySQL y 5432 para PostgreSQL. pub db_port: Option, /// Número máximo de conexiones habilitadas. pub max_pool_size: u32, } + +/// Motor de base de datos. Usado en el campo [`Database::db_type`] de [`SETTINGS`]. +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DbType { + /// Valor por defecto cuando `db_type` no está configurado. En este caso la aplicación terminará + /// con un error al arrancar. + #[serde(rename = "")] + Unset, + /// Usa el motor MySQL. Acepta también el alias `"mariadb"`. + #[serde(alias = "mariadb")] + Mysql, + /// Usa el motor PostgreSQL. Acepta también el alias `"postgresql"`. + #[serde(alias = "postgresql")] + Postgres, + /// Usa el motor SQLite. + Sqlite, +} diff --git a/src/config.rs b/src/config.rs index 9c687a0f..6faedfeb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -234,8 +234,8 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( macro_rules! include_config { ( $SETTINGS_NAME:ident : $settings_type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { #[doc = concat!( - "Ajustes de configuración y **valores por defecto** para ", - "[`", stringify!($settings_type), "`]." + "Carga [`", stringify!($settings_type), "`] ", + "(y aplica **valores por defecto** en claves no definidas)." )] #[doc = ""] #[doc = "Valores predeterminados que se aplican en ausencia de configuración:"] diff --git a/src/global.rs b/src/global.rs index 82c89394..2d7b1b60 100644 --- a/src/global.rs +++ b/src/global.rs @@ -44,8 +44,7 @@ include_config!(SETTINGS: Settings => [ // **< Settings >*********************************************************************************** -/// Tipos para las secciones globales [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log) y -/// [`[server]`](Server) de [`SETTINGS`]. +/// Ajustes para las secciones globales [`App`], [`Dev`], [`Log`] y [`Server`] de [`SETTINGS`]. #[derive(Debug, Deserialize)] pub struct Settings { pub app: App, @@ -54,7 +53,7 @@ pub struct Settings { pub server: Server, } -/// Sección `[app]` de la configuración. Forma parte de [`Settings`]. +/// Sección **`[app]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] pub struct App { /// Nombre de la aplicación. @@ -88,7 +87,7 @@ pub struct App { pub run_mode: String, } -/// Sección `[dev]` de la configuración. Forma parte de [`Settings`]. +/// Sección **`[dev]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] pub struct Dev { /// Directorio desde el que servir los archivos estáticos de PageTop. @@ -102,7 +101,7 @@ pub struct Dev { pub pagetop_static_dir: String, } -/// Sección `[log]` de la configuración. Forma parte de [`Settings`]. +/// Sección **`[log]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] pub struct Log { /// Gestión de trazas y registro de eventos activada (*true*) o desactivada (*false*). @@ -122,7 +121,7 @@ pub struct Log { pub format: LogFormat, } -/// Sección `[server]` de la configuración. Forma parte de [`Settings`]. +/// Sección **`[server]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] pub struct Server { /// Dirección de enlace para el servidor web. From 830602b24e734236314954bdae0cfc842e8a84e4 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 9 Jun 2026 19:22:34 +0200 Subject: [PATCH 25/29] =?UTF-8?q?=F0=9F=8E=A8=20(seaorm):=20Mejora=20API?= =?UTF-8?q?=20y=20documentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reescribe la documentación con ejemplos completos, guía rápida y tablas de referencia. - Renombra `connection()` a `dbconn()`. - Añade `execute()` para SQL en crudo y corrige `fetch_all`/`fetch_one` para aceptar `&Q` en lugar de `&mut Q`. - Cambia `futures::executor::block_on` por `tokio::task::block_in_place` para compatibilidad con el *runtime* multi-hilo. - Los fallos de migración al arrancar provocan `panic!` en lugar de log de error silencioso. - Actualiza `#[pagetop::test]` para usar `flavor = "multi_thread"`, alineándolo con `#[pagetop::main]` y con las extensiones que usan SeaORM. --- extensions/pagetop-seaorm/README.md | 55 ++- extensions/pagetop-seaorm/src/db.rs | 380 +++++++++++++----- extensions/pagetop-seaorm/src/lib.rs | 127 ++++-- extensions/pagetop-seaorm/src/migration.rs | 182 ++++++++- .../src/migration/connection.rs | 2 +- .../pagetop-seaorm/src/migration/manager.rs | 2 +- .../pagetop-seaorm/src/migration/migrator.rs | 10 +- .../pagetop-seaorm/src/migration/schema.rs | 17 +- helpers/pagetop-macros/src/lib.rs | 5 +- 9 files changed, 602 insertions(+), 178 deletions(-) diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index 42738cd0..23a4ce40 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -45,6 +45,9 @@ opcional; si se omite se usa el puerto predeterminado del motor. ```rust,ignore use pagetop::prelude::*; +use pagetop_seaorm::install_migrations; + +mod migration; struct MyApp; @@ -66,9 +69,10 @@ async fn main() -> std::io::Result<()> { } ``` -**Escribe las migraciones** usando la API de SeaORM: +**Escribe las migraciones** usando la API de [`migration`]: ```rust,no_run +// src/migration/m20240101_000001_create_users.rs use pagetop_seaorm::migration::*; pub struct Migration; @@ -81,6 +85,7 @@ impl MigrationTrait for Migration { table_auto(Users::Table) .col(pk_auto(Users::Id)) .col(string_uniq(Users::Email)) + .col(string(Users::Name)) .to_owned(), ) .await @@ -92,6 +97,52 @@ enum Users { Table, Id, Email, + Name, +} +``` + +**Define las entidades** en un módulo `entity/` usando las macros de derivación de [`db`]: + +```rust,no_run +// src/entity/user.rs +use pagetop_seaorm::db::*; + +#[derive(Clone, Debug, DeriveEntityModel, PartialEq)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub email: String, + pub name: String, +} + +#[derive(Clone, Copy, Debug, DeriveRelation, EnumIter)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} +``` + +**Opera con la base de datos** pasando la conexión [`db::dbconn()`] a cada consulta: + +```rust,ignore +use pagetop_seaorm::db::*; + +// Asumiendo que existe un módulo `user` con la entidad definida arriba. +async fn example() -> Result<(), DbErr> { + // Listar todos los registros: + let users = user::Entity::find().all(dbconn()).await?; + + // Buscar por clave primaria: + let found = user::Entity::find_by_id(1).one(dbconn()).await?; + + // Insertar un registro: + let new_user = user::ActiveModel { + email: Set("alice@example.com".to_owned()), + name: Set("Alice".to_owned()), + ..Default::default() + }; + user::Entity::insert(new_user).exec(dbconn()).await?; + Ok(()) } ``` @@ -125,7 +176,7 @@ extensión. Los ficheros adaptados del original son: | `manager.rs` | Adapta *features* propias | | `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI | | `prelude.rs` | Absorbido en `migration.rs`, descarta exportaciones del CLI | -| `schema.rs` | Integra con ajustes, original de [loco](https://github.com/loco-rs/loco) | +| `schema.rs` | Integra con ajustes, adaptado de [loco](https://github.com/loco-rs/loco) | | `seaql_migrations.rs` | Integración completa | diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index 2b47c399..a2b5dd4d 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -1,128 +1,199 @@ -//! API completa de SeaORM para operaciones con la base de datos. +//! Definición de entidades y acceso a la base de datos. //! -//! Re-exporta el *prelude* de SeaORM (entidades, traits, tipos de valor, macros de derivación…) -//! y expone tres funciones de consulta propias. Con una sola importación tienes todo lo necesario -//! para definir entidades y realizar operaciones CRUD: +//! Agrupa los *traits*, macros y tipos del sistema de entidades de SeaORM, junto con las funciones +//! [`dbconn`], [`execute`], [`fetch_all`] y [`fetch_one`], en una sola importación: //! -//! ```rust,ignore +//! ```rust //! use pagetop_seaorm::db::*; //! ``` //! -//! Para definir el esquema de la base de datos o escribir migraciones usa además -//! [`crate::migration`]. +//! El sistema de entidades (`Entity::find()`, `Entity::insert()`, transacciones) es el camino +//! recomendado para la mayoría de operaciones. Las funciones [`execute`], [`fetch_all`] y +//! [`fetch_one`] ofrecen vías alternativas para cuando ese sistema no es suficiente, como consultas +//! sin entidad concreta, SQL específico para el motor de base de datos utilizado o sentencias +//! puntuales. +//! +//! 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í +//! se necesita SQL en crudo con parámetros reales, se puede construir un [`api::Statement`] +//! directamente con [`api::Statement::from_sql_and_values`]. +//! +//! ## Tipos esenciales +//! +//! Destacan los siguientes elementos de uso más frecuente: +//! +//! - **Acceso**: [`DatabaseConnection`], [`dbconn`] (para obtener el pool de conexiones). +//! - **Consultas**: [`EntityTrait`], [`QueryFilter`], [`QueryOrder`], [`QuerySelect`]. +//! - **Transacciones**: [`TransactionTrait`], [`DatabaseTransaction`]. +//! - **Modelos activos**: [`ActiveModelTrait`], [`ActiveValue`] ([`ActiveValue::Set`], +//! [`ActiveValue::Unchanged`], [`ActiveValue::NotSet`]). +//! - **Macros de derivación**: [`DeriveEntityModel`], [`DeriveColumn`], [`DerivePrimaryKey`], +//! [`DeriveRelation`], [`EnumIter`]. +//! - **Errores**: [`DbErr`]. +//! - **Resultados**: [`QueryResult`] (filas sin tipar), [`ExecResult`] (INSERT/UPDATE/DELETE). +//! +//! ## Definir una entidad +//! +//! ```rust +//! use pagetop_seaorm::db::*; +//! +//! #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +//! #[sea_orm(table_name = "users")] +//! // El struct debe llamarse `Model`: es un requisito de `DeriveEntityModel`. +//! pub struct Model { +//! #[sea_orm(primary_key)] +//! pub id: i32, +//! pub email: String, +//! pub name: String, +//! } +//! +//! #[derive(Clone, Copy, Debug, EnumIter, DeriveRelation)] +//! pub enum Relation {} +//! +//! // `DeriveEntityModel` genera también `ActiveModel`, `Entity`, `Column` y `PrimaryKey`. +//! impl ActiveModelBehavior for ActiveModel {} +//! ``` +//! +//! ## Operaciones CRUD +//! +//! ```rust,ignore +//! use pagetop_seaorm::db::*; +//! +//! // El código asume que existe un módulo `user` con una entidad SeaORM definida. +//! +//! async fn example() -> Result<(), DbErr> { +//! // Buscar todos los registros. +//! let users = user::Entity::find().all(dbconn()).await?; +//! +//! // Buscar con filtro. +//! let alices = user::Entity::find() +//! .filter(user::Column::Name.eq("Alice")) +//! .all(dbconn()) +//! .await?; +//! +//! // Buscar por clave primaria. +//! let found = user::Entity::find_by_id(1).one(dbconn()).await?; +//! +//! // Insertar. +//! let model = user::ActiveModel { +//! name: ActiveValue::Set("Alice".into()), +//! ..Default::default() +//! }; +//! user::Entity::insert(model).exec(dbconn()).await?; +//! +//! // Actualizar: campos con `ActiveValue::Set`, clave primaria con `ActiveValue::Unchanged`. +//! let patch = user::ActiveModel { +//! id: ActiveValue::Unchanged(1), +//! name: ActiveValue::Set("Bob".into()), +//! ..Default::default() +//! }; +//! patch.update(dbconn()).await?; +//! +//! // Eliminar por clave primaria. +//! user::Entity::delete_by_id(1).exec(dbconn()).await?; +//! +//! // Transacción. `Box::pin` es necesario: `TransactionTrait` exige `Pin>`. +//! dbconn().transaction::<_, DbErr, _>(|txn| Box::pin(async move { +//! user::Entity::insert( +//! user::ActiveModel { name: ActiveValue::Set("Carol".into()), ..Default::default() } +//! ).exec(txn).await?; +//! user::Entity::delete_by_id(2).exec(txn).await?; +//! Ok(()) +//! })).await?; +//! Ok(()) +//! } +//! ``` +//! +//! Para migraciones y definición de esquemas usa [`migration`](crate::migration). +//! +//! ## Acceso completo a SeaORM +//! +//! El módulo [`api`] re-exporta el crate `sea_orm` íntegro bajo ese alias. Úsalo cuando necesites +//! un tipo o función que no esté expuesto directamente en `db::*`: +//! +//! ```rust +//! use pagetop_seaorm::db::api; +//! +//! // Tipos o utilidades no incluidos en db::*: +//! let _: api::DatabaseBackend = api::DatabaseBackend::Sqlite; +//! ``` +//! +//! ## 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 +//! [`fetch_all`] y [`fetch_one`]. Es el compañero natural de esas funciones dentro del módulo `db`: +//! +//! ```rust +//! use pagetop_seaorm::db::*; +//! use pagetop_seaorm::db::query::*; +//! +//! async fn example() -> Result<(), DbErr> { +//! let stmt = Query::select() +//! .column(Asterisk) +//! .from(Alias::new("users")) +//! .to_owned(); +//! let rows = fetch_all(&stmt).await?; +//! Ok(()) +//! } +//! ``` pub use sea_orm::prelude::*; -use sea_orm::sea_query::{ - MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder, +pub use sea_orm::{ + ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait, }; -use sea_orm::{DatabaseBackend, ExecResult, Statement}; -/// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades. +/// Permite implementar *traits* con métodos `async`: +#[doc(inline)] +pub use async_trait; + +/// Re-exporta el crate `sea_orm` íntegro como puerta de acceso a su API completa. /// -/// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades -/// de SeaORM. El coste de esta llamada es prácticamente nulo: sólo devuelve una referencia a un -/// valor inicializado una sola vez al arrancar la aplicación. +/// Ú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. +/// +/// Proporciona los constructores de consultas (`Query`, `Expr`, `Alias`, ...) que se pasan a +/// [`fetch_all`] y [`fetch_one`]. Aunque [`migration`](crate::migration) expone las mismas +/// herramientas en el contexto de la definición de esquemas, `query` las sitúa donde corresponde +/// cuando se trata del acceso a la base de datos en tiempo de ejecución. +#[doc(inline)] +pub use sea_orm::sea_query as query; + +/// Devuelve una referencia estática al pool de conexiones. +/// +/// El pool se inicializa una sola vez al arrancar la aplicación; las llamadas posteriores devuelven +/// la misma referencia sin coste apreciable. Se puede invocar tantas veces como sea necesario sin +/// penalización. /// /// ```rust,no_run /// use pagetop_seaorm::db::*; /// -/// // Consultas tipadas con el sistema de entidades de SeaORM: -/// // let users = User::find().all(connection()).await?; -/// // let user = User::find_by_id(1).one(connection()).await?; -/// // User::insert(model).exec(connection()).await?; -/// let _conn = connection(); +/// let _conn: &DatabaseConnection = dbconn(); /// ``` -pub fn connection() -> &'static DatabaseConnection { +#[inline] +pub fn dbconn() -> &'static DatabaseConnection { &super::DBCONN } -/// Ejecuta una consulta para devolver todas las filas resultantes. +/// Ejecuta una sentencia SQL en crudo y devuelve su resultado. /// -/// Acepta cualquier tipo que implemente [`crate::migration::QueryStatementWriter`] (p. ej. [`crate::migration::SelectStatement`]) y -/// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada -/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con -/// [`QueryResult::try_get`]. +/// No construye la sentencia (INSERT, UPDATE, DELETE), sino que la recibe como una cadena ya +/// formada. Útil para SQL que el sistema de entidades no cubre. El [`ExecResult`] devuelto expone +/// [`rows_affected`](ExecResult::rows_affected) y [`last_insert_id`](ExecResult::last_insert_id) +/// (fiable en MySQL y SQLite; en PostgreSQL devuelve `0`, usa `RETURNING` con el sistema de +/// entidades si necesitas el id insertado). /// -/// ```rust,no_run -/// use pagetop_seaorm::db::*; -/// use pagetop_seaorm::migration::*; +/// > **Nota:** no sirve para SELECT porque no devuelve filas. Para leer datos usa [`fetch_all`] o +/// > [`fetch_one`]. /// -/// async fn example() -> Result<(), DbErr> { -/// let mut stmt = Query::select() -/// .column(Asterisk) -/// .from(Alias::new("users")) -/// .to_owned(); -/// let rows = fetch_all(&mut stmt).await?; -/// for row in rows { -/// let name: String = row.try_get("", "name")?; -/// println!("{name}"); -/// } -/// Ok(()) -/// } -/// ``` -pub async fn fetch_all(stmt: &mut Q) -> Result, DbErr> { - let dbconn = &*super::DBCONN; - let dbbackend = dbconn.get_database_backend(); - dbconn - .query_all(Statement::from_string( - dbbackend, - match dbbackend { - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - )) - .await -} - -/// Ejecuta una consulta y devuelve sólo la primera fila, si existe. +/// > **Advertencia:** nunca interpoles valores externos en la cadena SQL directamente. Para +/// > sentencias con parámetros de usuario usa el sistema de entidades. /// -/// Funciona igual que [`fetch_all`] pero detiene la ejecución tras la primera fila y devuelve -/// `None` si la consulta no produce resultados. -/// -/// ```rust,no_run -/// use pagetop_seaorm::db::*; -/// use pagetop_seaorm::migration::*; -/// -/// async fn example() -> Result<(), DbErr> { -/// let mut stmt = Query::select() -/// .column(Asterisk) -/// .from(Alias::new("users")) -/// .and_where(Expr::col(Alias::new("id")).eq(1)) -/// .to_owned(); -/// if let Some(row) = fetch_one(&mut stmt).await? { -/// let name: String = row.try_get("", "name")?; -/// println!("{name}"); -/// } -/// Ok(()) -/// } -/// ``` -pub async fn fetch_one( - stmt: &mut Q, -) -> Result, DbErr> { - let dbconn = &*super::DBCONN; - let dbbackend = dbconn.get_database_backend(); - dbconn - .query_one(Statement::from_string( - dbbackend, - match dbbackend { - DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder), - DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder), - DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder), - }, - )) - .await -} - -/// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de -/// la operación. -/// -/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe -/// como cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El -/// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado. -/// -/// ```rust,no_run +/// ```rust /// use pagetop_seaorm::db::*; /// /// async fn example() -> Result<(), DbErr> { @@ -132,9 +203,106 @@ pub async fn fetch_one( /// } /// ``` pub async fn execute(stmt: impl Into) -> Result { - let dbconn = &*super::DBCONN; - let dbbackend = dbconn.get_database_backend(); - dbconn - .execute(Statement::from_string(dbbackend, stmt.into())) + let conn = dbconn(); + let backend = conn.get_database_backend(); + conn.execute(api::Statement::from_string(backend, stmt.into())) .await } + +/// Ejecuta una consulta para devolver todas las filas resultantes. +/// +/// Acepta cualquier tipo que implemente [`query::QueryStatementWriter`] (p. ej. +/// [`query::SelectStatement`]) y serializa la sentencia para el motor de base de datos usado antes +/// de ejecutarla. Cada fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con +/// [`QueryResult::try_get`]. +/// +/// Usa esta función cuando la consulta SELECT no mapea una entidad concreta (JOINs, agregaciones, +/// proyecciones parciales) o cuando necesitas control total sobre el SQL generado. Para sentencias +/// que modifican datos (INSERT, UPDATE, DELETE), usa [`execute`]. Para consultas que sí mapean a +/// una entidad, es preferible `Entity::find().all(dbconn())`. +/// +/// Los valores se integran como literales escapados, no como parámetros de base de datos. Para +/// datos procedentes del usuario, el sistema de entidades es más robusto. +/// +/// ```rust +/// use pagetop_seaorm::db::*; +/// use pagetop_seaorm::db::query::*; +/// +/// async fn example() -> Result<(), DbErr> { +/// let stmt = Query::select() +/// .column(Asterisk) +/// .from(Alias::new("users")) +/// .to_owned(); +/// let rows = fetch_all(&stmt).await?; +/// for row in rows { +/// let name: String = row.try_get("", "name")?; +/// println!("{name}"); +/// } +/// Ok(()) +/// } +/// ``` +pub async fn fetch_all( + stmt: &Q, +) -> Result, DbErr> { + let conn = dbconn(); + let backend = conn.get_database_backend(); + conn.query_all(api::Statement::from_string( + backend, + match backend { + api::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder), + api::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder), + api::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder), + }, + )) + .await +} + +/// Ejecuta una consulta y devuelve sólo la primera fila, si existe. +/// +/// Funciona igual que [`fetch_all`] pero devuelve la primera fila si existe, o `None` si la +/// consulta no produce resultados. Está diseñada para sentencias SELECT; para modificar datos sin +/// entidad mapeada, usa [`execute`]. +/// +/// Si la consulta puede devolver varias filas, se recomienda incluir `LIMIT 1` en la sentencia +/// para que el motor detenga la búsqueda en cuanto encuentre la primera fila y no recupere +/// resultados que se descartarán de todas formas. +/// +/// Usa esta función cuando la consulta SELECT no mapea una entidad concreta (JOINs, agregaciones, +/// proyecciones parciales) o cuando necesitas control total sobre el SQL generado. Para consultas +/// que sí mapean a una entidad, es preferible `Entity::find().one(dbconn())`. +/// +/// Los valores se integran como literales escapados, no como parámetros de base de datos. Para +/// datos procedentes del usuario, el sistema de entidades es más robusto. +/// +/// ```rust +/// use pagetop_seaorm::db::*; +/// use pagetop_seaorm::db::query::*; +/// +/// async fn example() -> Result<(), DbErr> { +/// let stmt = Query::select() +/// .column(Asterisk) +/// .from(Alias::new("users")) +/// .and_where(Expr::col(Alias::new("id")).eq(1)) +/// .to_owned(); +/// if let Some(row) = fetch_one(&stmt).await? { +/// let name: String = row.try_get("", "name")?; +/// println!("{name}"); +/// } +/// Ok(()) +/// } +/// ``` +pub async fn fetch_one( + stmt: &Q, +) -> Result, DbErr> { + let conn = dbconn(); + let backend = conn.get_database_backend(); + conn.query_one(api::Statement::from_string( + backend, + match backend { + api::DatabaseBackend::MySql => stmt.to_string(query::MysqlQueryBuilder), + api::DatabaseBackend::Postgres => stmt.to_string(query::PostgresQueryBuilder), + api::DatabaseBackend::Sqlite => stmt.to_string(query::SqliteQueryBuilder), + }, + )) + .await +} diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index 8af917ab..ef056f64 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -46,6 +46,9 @@ opcional; si se omite se usa el puerto predeterminado del motor. ```rust,ignore use pagetop::prelude::*; +use pagetop_seaorm::install_migrations; + +mod migration; struct MyApp; @@ -57,7 +60,7 @@ impl Extension for MyApp { } fn initialize(&self) { - install_migrations!(m20240101_000001_create_users_table); + install_migrations!(m20240101_000001_create_users); } } @@ -67,9 +70,10 @@ async fn main() -> std::io::Result<()> { } ``` -**Escribe las migraciones** usando la API de SeaORM: +**Escribe las migraciones** usando la API de [`migration`]: ```rust,no_run +// src/migration/m20240101_000001_create_users.rs use pagetop_seaorm::migration::*; pub struct Migration; @@ -82,6 +86,7 @@ impl MigrationTrait for Migration { table_auto(Users::Table) .col(pk_auto(Users::Id)) .col(string_uniq(Users::Email)) + .col(string(Users::Name)) .to_owned(), ) .await @@ -93,6 +98,52 @@ enum Users { Table, Id, Email, + Name, +} +``` + +**Define las entidades** en un módulo `entity/` usando las macros de derivación de [`db`]: + +```rust,no_run +// src/entity/user.rs +use pagetop_seaorm::db::*; + +#[derive(Clone, Debug, DeriveEntityModel, PartialEq)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub email: String, + pub name: String, +} + +#[derive(Clone, Copy, Debug, DeriveRelation, EnumIter)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} +``` + +**Opera con la base de datos** pasando la conexión [`db::dbconn()`] a cada consulta: + +```rust,ignore +use pagetop_seaorm::db::*; + +// Asumiendo que existe un módulo `user` con la entidad definida arriba. +async fn example() -> Result<(), DbErr> { + // Listar todos los registros: + let users = user::Entity::find().all(dbconn()).await?; + + // Buscar por clave primaria: + let found = user::Entity::find_by_id(1).one(dbconn()).await?; + + // Insertar un registro: + let new_user = user::ActiveModel { + email: Set("alice@example.com".to_owned()), + name: Set("Alice".to_owned()), + ..Default::default() + }; + user::Entity::insert(new_user).exec(dbconn()).await?; + Ok(()) } ``` */ @@ -116,7 +167,18 @@ pub mod db; pub mod migration; -pub(crate) use futures::executor::block_on as run_now; +// Ejecuta un *future* de forma síncrona dentro del runtime de Tokio. +// +// Usa [`tokio::task::block_in_place`] para ceder el hilo actual al código bloqueante sin detener el +// *pool* de trabajo de Tokio, y a continuación ejecuta el *future* con el *handle* del *runtime* +// activo. Requiere el *runtime* multi-hilo (predeterminado con `#[pagetop::main]`). +// +// En tests, `#[pagetop::test]` aplica `multi_thread` por defecto. Si se utiliza `#[tokio::test]` +// directamente, habría que añadir `(flavor = "multi_thread")` si el test invoca código que llame a +// esta función. +pub(crate) fn run_now(future: F) -> F::Output { + tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(future)) +} pub(crate) static DBCONN: LazyLock = LazyLock::new(|| { trace::info!( @@ -125,51 +187,48 @@ pub(crate) static DBCONN: LazyLock = LazyLock::new(|| { &config::SETTINGS.database.max_pool_size ); - let db_uri = match config::SETTINGS.database.db_type.as_str() { - "mysql" | "postgres" => { - let mut tmp_uri = Url::parse( - format!( - "{}://{}/{}", - &config::SETTINGS.database.db_type, - &config::SETTINGS.database.db_host, - &config::SETTINGS.database.db_name - ) - .as_str(), - ) - .unwrap(); + let db_uri: String = match config::SETTINGS.database.db_type { + config::DbType::Unset => panic!( + "database.db_type is not configured: set it to \"mysql\", \"postgres\" or \"sqlite\"" + ), + config::DbType::Mysql | config::DbType::Postgres => { + let scheme = if matches!(config::SETTINGS.database.db_type, config::DbType::Mysql) { + "mysql" + } else { + "postgres" + }; + let mut tmp_uri = Url::parse(&format!( + "{}://{}/{}", + scheme, + &config::SETTINGS.database.db_host, + &config::SETTINGS.database.db_name + )) + .expect("Invalid database URL: check db_host and db_name in config"); tmp_uri .set_username(config::SETTINGS.database.db_user.as_str()) - .unwrap(); + .expect("Failed to set db_user in connection URL"); // https://github.com/launchbadge/sqlx/issues/1624 tmp_uri .set_password(Some(config::SETTINGS.database.db_pass.as_str())) - .unwrap(); + .expect("Failed to set db_pass in connection URL"); if let Some(port) = config::SETTINGS.database.db_port { - tmp_uri.set_port(Some(port)).unwrap(); + tmp_uri + .set_port(Some(port)) + .expect("Failed to set db_port in connection URL"); } - tmp_uri + tmp_uri.to_string() + } + config::DbType::Sqlite => { + format!("sqlite://{}", &config::SETTINGS.database.db_name) } - "sqlite" => Url::parse( - format!( - "{}://{}", - &config::SETTINGS.database.db_type, - &config::SETTINGS.database.db_name - ) - .as_str(), - ) - .unwrap(), - _ => panic!( - "Unrecognized database type \"{}\"", - config::SETTINGS.database.db_type - ), }; run_now(Database::connect::({ - let mut db_opt = ConnectOptions::new(db_uri.to_string()); + let mut db_opt = ConnectOptions::new(db_uri); db_opt.max_connections(config::SETTINGS.database.max_pool_size); db_opt })) - .unwrap_or_else(|_| panic!("Failed to connect to database")) + .expect("Failed to connect to database") }); /// Implementa la extensión. diff --git a/extensions/pagetop-seaorm/src/migration.rs b/extensions/pagetop-seaorm/src/migration.rs index 14b8f85b..f3544f7f 100644 --- a/extensions/pagetop-seaorm/src/migration.rs +++ b/extensions/pagetop-seaorm/src/migration.rs @@ -1,15 +1,116 @@ //! API para definir y ejecutar migraciones de base de datos. //! -//! Re-exporta los tipos de SeaORM necesarios para escribir migraciones y ofrece las macros -//! [`crate::install_migrations`] y [`crate::uninstall_migrations`] para aplicarlas o revertirlas al -//! arrancar la extensión. +//! Cuando una extensión necesita persistir datos en una base de datos usando `pagetop_seaorm`, +//! define sus migraciones en un submódulo `migration/` y las aplica al arrancar con la macro +//! [`install_migrations!`](crate::install_migrations). //! -//! ```rust,ignore -//! use pagetop_seaorm::db::*; +//! Con una sola importación tienes todo lo necesario: +//! +//! ```rust //! use pagetop_seaorm::migration::*; //! ``` +//! +//! # Convención de nombrado +//! +//! Cada migración es un módulo con el formato `m__`. El +//! prefijo numérico garantiza el orden cronológico de aplicación: +//! +//! ```text +//! src/ +//! └── migration/ +//! ├── m20240101_000001_create_users.rs +//! └── m20240115_000002_add_email_index.rs +//! ``` +//! +//! # Estructura de una migración +//! +//! Cada archivo define un *struct* `Migration` que implementa [`MigrationTrait`]. El método `up` +//! aplica el cambio; `down` lo revierte. Si no se implementa, devuelve un error; es **obligatorio** +//! implementarlo si la extensión usa [`uninstall_migrations!`](crate::uninstall_migrations): +//! +//! ```rust,no_run +//! // src/migration/m20240101_000001_create_users.rs +//! use pagetop_seaorm::migration::*; +//! +//! pub struct Migration; +//! +//! #[async_trait::async_trait] +//! impl MigrationTrait for Migration { +//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { +//! manager +//! .create_table( +//! table_auto(Users::Table) +//! .col(pk_auto(Users::Id)) +//! .col(string_uniq(Users::Email)) +//! .col(string(Users::Name)) +//! .to_owned(), +//! ) +//! .await +//! } +//! } +//! +//! #[derive(DeriveIden)] +//! enum Users { +//! Table, +//! Id, +//! Email, +//! Name, +//! } +//! ``` +//! +//! # Seguimiento automático +//! +//! Las migraciones se mantienen en una tabla `seaql_migrations` de la base de datos. Cada migración +//! aplicada queda registrada con su nombre y su marca de tiempo. Las migraciones ya aplicadas se +//! omiten en ejecuciones posteriores. +//! +//! # Operaciones con `SchemaManager` +//! +//! El parámetro `manager` que recibe cada migración expone los métodos necesarios para +//! modificar el esquema. Estos son los más habituales: +//! +//! | Método | Acción | +//! |-----------------------------------------------|---------------------------------| +//! | [`SchemaManager::create_table`] | Crea una tabla | +//! | [`SchemaManager::drop_table`] | Elimina una tabla | +//! | [`SchemaManager::alter_table`] | Modifica una tabla existente | +//! | [`SchemaManager::rename_table`] | Renombra una tabla | +//! | [`SchemaManager::truncate_table`] | Vacía una tabla | +//! | [`SchemaManager::create_index`] | Crea un índice | +//! | [`SchemaManager::drop_index`] | Elimina un índice | +//! | [`SchemaManager::create_foreign_key`] | Crea una clave foránea | +//! | [`SchemaManager::drop_foreign_key`] | Elimina una clave foránea | +//! | [`SchemaManager::has_table`] | Comprueba si existe una tabla | +//! | [`SchemaManager::has_column`] | Comprueba si existe una columna | +//! | [`SchemaManager::has_index`] | Comprueba si existe un índice | +//! | [`SchemaManager::create_type`] *(PostgreSQL)* | Crea un tipo personalizado | +//! | [`SchemaManager::alter_type`] *(PostgreSQL)* | Modifica un tipo personalizado | +//! | [`SchemaManager::drop_type`] *(PostgreSQL)* | Elimina un tipo personalizado | +//! +//! # Funciones de esquema +//! +//! Las funciones de esquema disponibles simplifican la definición de columnas. Siguen el patrón +//! `(col)` (NOT NULL), `_null(col)` (nulable) y `_uniq(col)` (NOT NULL + UNIQUE). +//! Algunos ejemplos: +//! +//! | Función | SQL equivalente | +//! |---------------------|-----------------------------------------------------| +//! | `table_auto(tabla)` | `CREATE TABLE IF NOT EXISTS` + timestamps | +//! | `pk_auto(col)` | `INTEGER NOT NULL PRIMARY KEY` (con autoincremento) | +//! | `pk_uuid(col)` | `UUID NOT NULL PRIMARY KEY` | +//! | `string(col)` | `VARCHAR NOT NULL` | +//! | `string_null(col)` | `VARCHAR NULL` | +//! | `string_uniq(col)` | `VARCHAR NOT NULL UNIQUE` | +//! | `integer(col)` | `INTEGER NOT NULL` | +//! | `boolean(col)` | `BOOLEAN NOT NULL` | +//! | `timestamp(col)` | `TIMESTAMP NOT NULL` | +//! | `uuid(col)` | `UUID NOT NULL` | +//! +//! Estas son sólo las funciones más habituales. El módulo [`schema`] define la lista completa, con +//! variantes para `decimal`, `date`, `time`, `json`, `blob`, `binary`, `array`, `enumeration`, +//! `char`, `interval` y sus formas `_null` y `_uniq`. -// **< Adaptación de `sea-orm-migration` (ver §Créditos en README.md) >***************************** +// **< Adaptación de `sea-orm-migration/lib.rs` (ver §Créditos en README.md) >********************** //pub mod cli; pub mod connection; @@ -20,10 +121,14 @@ pub mod schema; pub mod seaql_migrations; //pub mod util; +#[doc(inline)] pub use connection::*; +#[doc(inline)] pub use manager::*; //pub use migrator::*; +/// Permite implementar *traits* con métodos `async`: +#[doc(inline)] pub use async_trait; //pub use sea_orm; //pub use sea_orm::sea_query; @@ -49,54 +154,84 @@ pub trait MigrationTrait: MigrationName + Send + Sync { // ************************************************************************************************* +#[doc(inline)] pub use migrator::MigratorTrait; +#[doc(inline)] pub use schema::*; -pub use sea_orm::sea_query::*; pub use sea_orm::DeriveIden; +pub use sea_orm::sea_query::*; use pagetop::core::TypeInfo; -use pagetop::trace; impl MigrationName for M { fn name(&self) -> &str { + // Extrae el módulo contenedor, descartando el segmento final "Migration". TypeInfo::NameTo(-2).of::() } } +/// Elemento de migración listo para incluir en la lista de un [`MigratorTrait`]. pub type MigrationItem = Box; +/// Interfaz síncrona para ejecutar migraciones desde código no asíncrono. +/// +/// Todo tipo que implemente [`MigratorTrait`] obtiene esta interfaz automáticamente, incluidos los +/// tipos generados por los macros [`install_migrations!`](crate::install_migrations) y +/// [`uninstall_migrations!`](crate::uninstall_migrations). pub trait MigratorBase { + /// Ejecuta las migraciones pendientes en orden ascendente. + /// + /// Provoca un `panic!` si alguna migración falla, evitando que la aplicación arranque con un + /// esquema de base de datos inconsistente. fn run_up(); + /// Revierte todas las migraciones en orden descendente. + /// + /// Provoca un `panic!` si alguna reversión falla. fn run_down(); } -#[rustfmt::skip] impl MigratorBase for M { fn run_up() { - if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) { - trace::error!("Migration upgrade failed ({})", e); - }; + let conn = SchemaManagerConnection::Connection(&super::DBCONN); + if let Err(e) = super::run_now(Self::up(conn, None)) { + panic!("Migration upgrade failed: {e}"); + } } fn run_down() { - if let Err(e) = super::run_now(Self::down(SchemaManagerConnection::Connection(&super::DBCONN), None)) { - trace::error!("Migration downgrade failed ({})", e); - }; + let conn = SchemaManagerConnection::Connection(&super::DBCONN); + if let Err(e) = super::run_now(Self::down(conn, None)) { + panic!("Migration downgrade failed: {e}"); + } } } /// Aplica las migraciones pendientes al arrancar una extensión. /// -/// Recibe uno o más módulos de migración y ejecuta el método `up` de los que aún no estén +/// Recibe uno o más nombres de módulo de migración y ejecuta el método `up` de los que aún no estén /// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde /// [`Extension::initialize`](pagetop::core::extension::Extension::initialize). /// +/// **Requisito:** cada módulo de migración debe declararse como submódulo público bajo un módulo +/// `migration` accesible desde el punto de llamada, y exportar `pub struct Migration` que +/// implemente [`MigrationTrait`]. El macro genera rutas de la forma +/// `migration::::Migration`. Estructura mínima: +/// +/// En `src/migration.rs`: /// ```rust,ignore +/// pub mod m20240101_000001_create_users; +/// pub mod m20240115_000002_add_email_index; +/// ``` +/// +/// En `src/lib.rs`: +/// ```rust,ignore +/// mod migration; +/// /// impl Extension for MyExt { /// fn initialize(&self) { /// install_migrations!( -/// m20240101_000001_create_users_table, +/// m20240101_000001_create_users, /// m20240115_000002_add_email_index, /// ); /// } @@ -123,14 +258,21 @@ macro_rules! install_migrations { /// Revierte las migraciones de una extensión en orden inverso al de su aplicación. /// -/// Ejecuta el método `down` de cada migración indicada. Si alguna no implementa `down`, -/// detiene el proceso con un error. Complementario a [`crate::install_migrations`]. +/// Ejecuta el método `down` de cada migración indicada. Si el método `down` de alguna migración +/// devuelve un error, provoca un `panic!`. Complementario a +/// [`install_migrations!`](crate::install_migrations). /// +/// **Requisito:** los módulos de migración deben declararse como en +/// [`install_migrations!`](crate::install_migrations). Todos los módulos indicados **deben +/// implementar `down`**; la implementación por defecto devuelve error, lo que provoca un pánico en +/// `run_down`. +/// +/// En `src/lib.rs`: /// ```rust,ignore /// impl Extension for MyExt { /// fn uninitialize(&self) { /// uninstall_migrations!( -/// m20240101_000001_create_users_table, +/// m20240101_000001_create_users, /// m20240115_000002_add_email_index, /// ); /// } diff --git a/extensions/pagetop-seaorm/src/migration/connection.rs b/extensions/pagetop-seaorm/src/migration/connection.rs index 7c937a37..a34acc47 100644 --- a/extensions/pagetop-seaorm/src/migration/connection.rs +++ b/extensions/pagetop-seaorm/src/migration/connection.rs @@ -1,8 +1,8 @@ -use futures::Future; use sea_orm::{ AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait, }; +use std::future::Future; use std::pin::Pin; pub enum SchemaManagerConnection<'c> { diff --git a/extensions/pagetop-seaorm/src/migration/manager.rs b/extensions/pagetop-seaorm/src/migration/manager.rs index 3f962bdf..91d4b100 100644 --- a/extensions/pagetop-seaorm/src/migration/manager.rs +++ b/extensions/pagetop-seaorm/src/migration/manager.rs @@ -1,9 +1,9 @@ use super::{IntoSchemaManagerConnection, SchemaManagerConnection}; use sea_orm::sea_query::{ - extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, TableTruncateStatement, + extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, }; use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder}; #[allow(unused_imports)] diff --git a/extensions/pagetop-seaorm/src/migration/migrator.rs b/extensions/pagetop-seaorm/src/migration/migrator.rs index 45ecdbac..d10b4edb 100644 --- a/extensions/pagetop-seaorm/src/migration/migrator.rs +++ b/extensions/pagetop-seaorm/src/migration/migrator.rs @@ -1,14 +1,14 @@ -use futures::Future; use std::collections::HashSet; use std::fmt::Display; +use std::future::Future; use std::pin::Pin; use std::time::SystemTime; use pagetop::trace::info; use sea_orm::sea_query::{ - self, extension::postgres::Type, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, - SelectStatement, SimpleExpr, Table, + self, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, SelectStatement, SimpleExpr, + Table, extension::postgres::Type, }; use sea_orm::{ ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden, @@ -18,10 +18,10 @@ use sea_orm::{ #[allow(unused_imports)] use sea_schema::probe::SchemaProbe; -use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager}; +use super::{IntoSchemaManagerConnection, MigrationTrait, SchemaManager, seaql_migrations}; -#[derive(Copy, Clone, Debug, PartialEq, Eq)] /// Status of migration +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MigrationStatus { /// Not yet applied Pending, diff --git a/extensions/pagetop-seaorm/src/migration/schema.rs b/extensions/pagetop-seaorm/src/migration/schema.rs index 4dea5ee2..0ad92946 100644 --- a/extensions/pagetop-seaorm/src/migration/schema.rs +++ b/extensions/pagetop-seaorm/src/migration/schema.rs @@ -1,13 +1,14 @@ -//! Adaptación de +//! Adapted from //! -//! # Ayudantes de esquema de base de datos +//! # Database Table Schema Helpers //! -//! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`. +//! This module defines functions and helpers for creating database table +//! schemas using the `sea-orm` and `sea-query` libraries. //! -//! # Ejemplo +//! # Example //! -//! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes -//! de esquema. +//! The following example shows how the user migration file should be and using +//! the schema helpers to create the Db fields. //! //! ```rust //! use pagetop_seaorm::migration::*; @@ -597,7 +598,7 @@ pub fn array_uniq(col: T, elem_type: ColumnType) -> ColumnDef { array(col, elem_type).unique_key().take() } -/// Añade las columnas de timestamp (`CreatedAt` y `UpdatedAt`) a una tabla existente. +/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { let mut t = t; t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) @@ -605,7 +606,7 @@ pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { .take() } -/// Crea un alias. +/// Create an Alias. pub fn name>(name: T) -> Alias { Alias::new(name) } diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index a0796a0e..63349aa0 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -461,6 +461,9 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { /// Define funciones de prueba asíncronas para usar con PageTop. /// +/// Usa el *runtime* multi-hilo de **Tokio**, igual que [`#[pagetop::main]`](macro@main), para +/// garantizar compatibilidad con extensiones que ejecutan código asíncrono de forma síncrona. +/// /// # Ejemplo /// /// ```rust,ignore @@ -472,7 +475,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { let mut output: TokenStream = (quote! { - #[::tokio::test] + #[::tokio::test(flavor = "multi_thread")] }) .into(); From 0410b8c060e2622dcfca12589d5f8ecb5ef147a7 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 9 Jun 2026 19:24:51 +0200 Subject: [PATCH 26/29] =?UTF-8?q?=F0=9F=93=9D=20Correcci=C3=B3n=20m=C3=ADn?= =?UTF-8?q?ima=20de=20documentaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/component_poweredby.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/component_poweredby.rs b/tests/component_poweredby.rs index 1cd689cb..89ff3269 100644 --- a/tests/component_poweredby.rs +++ b/tests/component_poweredby.rs @@ -1,6 +1,6 @@ use pagetop::prelude::*; -/// Inicializa PageTop (locale, extensiones…) una sola vez para toda la suite. +/// Inicializa PageTop (locale, extensiones...) una sola vez para toda la suite. /// /// Los tests de este módulo renderizan componentes directamente con `Context::default()`, por lo /// que sólo necesitan el subsistema de localización y las extensiones registradas, no un router. From 47b6553fe48bc744d5c815e988b51c6f58ccec9c Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 11 Jun 2026 06:21:54 +0200 Subject: [PATCH 27/29] =?UTF-8?q?=F0=9F=90=9B=20(build):=20A=C3=ADsla=20di?= =?UTF-8?q?r=20temp.=20por=20nombre=20de=20destino?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- helpers/pagetop-build/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index f8390ee6..eacb6180 100644 --- a/helpers/pagetop-build/src/lib.rs +++ b/helpers/pagetop-build/src/lib.rs @@ -203,9 +203,11 @@ impl StaticFilesBundle { where P: AsRef, { - // Crea un directorio temporal para el archivo CSS. + // Crea un directorio temporal único para el archivo CSS (basado en su nombre, para que + // varias llamadas a from_scss en el mismo build.rs no se pisen). let out_dir = std::env::var("OUT_DIR").unwrap(); - let temp_dir = Path::new(&out_dir).join("from_scss_files"); + let safe_name = target_name.replace(['.', '-'], "_"); + let temp_dir = Path::new(&out_dir).join(format!("from_scss_{safe_name}")); // Limpia el directorio temporal de ejecuciones previas, si existe. if temp_dir.exists() { From 35a5221c920ee73c4013d1addbc3c6bf7fb20d0a Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 11 Jun 2026 06:46:16 +0200 Subject: [PATCH 28/29] =?UTF-8?q?=E2=9C=A8=20(macros):=20Permite=20(expr)?= =?UTF-8?q?=20como=20atributo=20en=20html!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `Attribute::Splice` en el AST de Maud, de modo que `(expr)` en posición de atributo renderiza la expresión directamente sobre el buffer de salida del elemento. --- helpers/pagetop-macros/src/maud/ast.rs | 16 ++++++++++++++++ helpers/pagetop-macros/src/maud/generate.rs | 10 ++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/helpers/pagetop-macros/src/maud/ast.rs b/helpers/pagetop-macros/src/maud/ast.rs index cdda2331..c8309ef5 100644 --- a/helpers/pagetop-macros/src/maud/ast.rs +++ b/helpers/pagetop-macros/src/maud/ast.rs @@ -212,6 +212,7 @@ impl DiagnosticParse for Element { || input.peek(Lit) || input.peek(Dot) || input.peek(Pound) + || input.peek(Paren) { let attr = input.diagnostic_parse(diagnostics)?; @@ -346,6 +347,10 @@ pub enum Attribute { name: HtmlName, attr_type: AttributeType, }, + Splice { + paren_token: Paren, + expr: Expr, + }, } impl DiagnosticParse for Attribute { @@ -374,6 +379,12 @@ impl DiagnosticParse for Attribute { pound_token: input.parse()?, name: input.diagnostic_parse(diagnostics)?, }) + } else if lookahead.peek(Paren) { + let content; + Ok(Self::Splice { + paren_token: parenthesized!(content in input), + expr: content.parse()?, + }) } else { let name = input.diagnostic_parse::(diagnostics)?; @@ -424,6 +435,11 @@ impl ToTokens for Attribute { name.to_tokens(tokens); attr_type.to_tokens(tokens); } + Self::Splice { paren_token, expr } => { + paren_token.surround(tokens, |tokens| { + expr.to_tokens(tokens); + }); + } } } } diff --git a/helpers/pagetop-macros/src/maud/generate.rs b/helpers/pagetop-macros/src/maud/generate.rs index a3dfb36e..ed2fa214 100644 --- a/helpers/pagetop-macros/src/maud/generate.rs +++ b/helpers/pagetop-macros/src/maud/generate.rs @@ -139,7 +139,7 @@ impl Generator { } fn attrs(&self, attrs: Vec, build: &mut Builder) { - let (classes, id, named_attrs) = split_attrs(attrs); + let (classes, id, named_attrs, spliced) = split_attrs(attrs); if !classes.is_empty() { let mut toggle_class_exprs = vec![]; @@ -184,6 +184,9 @@ impl Generator { for (name, attr_type) in named_attrs { self.attr(name, attr_type, build); } + for expr in spliced { + self.splice(expr, build); + } } fn control_flow>(&self, control_flow: ControlFlow, build: &mut Builder) { @@ -316,10 +319,12 @@ fn split_attrs( Vec<(HtmlNameOrMarkup, Option)>, Option, Vec<(HtmlName, AttributeType)>, + Vec, ) { let mut classes = vec![]; let mut id = None; let mut named_attrs = vec![]; + let mut spliced = vec![]; for attr in attrs { match attr { @@ -328,10 +333,11 @@ fn split_attrs( } Attribute::Id { name, .. } => id = Some(name), Attribute::Named { name, attr_type } => named_attrs.push((name, attr_type)), + Attribute::Splice { expr, .. } => spliced.push(expr), } } - (classes, id, named_attrs) + (classes, id, named_attrs, spliced) } //////////////////////////////////////////////////////// From 0121fad94ac596ee4ce6e0dba3db788c86c9fa9d Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 11 Jun 2026 07:18:04 +0200 Subject: [PATCH 29/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(html):=20Simplifica?= =?UTF-8?q?=20API=20de=20Classes=20y=20ClassesOp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elimina `ClassesOp::Replace` (sustituible con `Remove`+`Add`), renombra `Set` a `Reset` por claridad semántica, añade `Classes::is_empty()` y mejora documentación de `ClassesOp` con nota sobre orden CSS. --- src/html/classes.rs | 62 +++++++++++++++++++++++-------------------- tests/html_classes.rs | 53 ++++-------------------------------- 2 files changed, 38 insertions(+), 77 deletions(-) diff --git a/src/html/classes.rs b/src/html/classes.rs index 2f665c19..903475ec 100644 --- a/src/html/classes.rs +++ b/src/html/classes.rs @@ -1,4 +1,4 @@ -use crate::{AutoDefault, CowStr, builder_fn, util}; +use crate::{AutoDefault, builder_fn, util}; use std::collections::HashSet; @@ -7,6 +7,27 @@ use std::collections::HashSet; /// Cada variante opera sobre **una o más clases** proporcionadas como una cadena separada por /// espacios (p. ej. `"btn active"`), que se normalizan internamente a minúsculas en /// [`Classes::with_classes()`]. +/// +/// # Orden de las clases y CSS +/// +/// El navegador aplica los estilos según la especificidad de los selectores y el orden en que las +/// reglas aparecen en la **hoja de estilos**, no por el orden de las clases en el atributo `class`. +/// Por tanto, `"btn active"` y `"active btn"` producen exactamente el mismo resultado visual. +/// +/// Las operaciones [`Add`](Self::Add) y [`Prepend`](Self::Prepend) permiten controlar ese orden +/// únicamente por legibilidad o por convención de proyecto, no porque afecte al comportamiento +/// del navegador. +/// +/// # Reemplazar una clase +/// +/// Para sustituir una clase por otra encadena [`Remove`](Self::Remove) y [`Add`](Self::Add): +/// ```rust +/// # use pagetop::prelude::*; +/// let c = Classes::new("btn btn-primary active") +/// .with_classes(ClassesOp::Remove, "btn-primary") +/// .with_classes(ClassesOp::Add, "btn-secondary"); +/// assert_eq!(c.get(), Some("btn active btn-secondary".to_string())); +/// ``` #[derive(AutoDefault, Clone, Debug, PartialEq)] pub enum ClassesOp { /// Añade las clases que no existan al final. @@ -16,9 +37,6 @@ pub enum ClassesOp { Prepend, /// Elimina las clases indicadas que existan. Remove, - /// Sustituye una o varias clases existentes (indicadas en la variante) por las clases - /// proporcionadas. - Replace(CowStr), /// Alterna presencia/ausencia de una o más clases. /// /// Si en una misma llamada se repite una clase (p. ej. `"a a"`) que ya existe, el resultado @@ -26,7 +44,7 @@ pub enum ClassesOp { /// final). Toggle, /// Sustituye la lista completa por las clases indicadas. - Set, + Reset, } /// Lista de clases CSS normalizadas para el atributo `class` de HTML. @@ -36,8 +54,8 @@ pub enum ClassesOp { /// /// # Normalización /// -/// - Aunque el orden de las clases en el atributo `class` no afecta al resultado en CSS, -/// [`ClassesOp`] ofrece operaciones para controlar su orden de aparición por legibilidad. +/// - El orden de las clases no afecta al resultado en CSS; las operaciones de ordenación +/// ([`Add`](ClassesOp::Add), [`Prepend`](ClassesOp::Prepend)) son puramente estéticas. /// - Solo se acepta una lista de clases con caracteres ASCII. /// - Las clases se almacenan en minúsculas. /// - No se permiten clases duplicadas tras la normalización (por ejemplo, `Btn` y `btn` se @@ -51,7 +69,8 @@ pub enum ClassesOp { /// # use pagetop::prelude::*; /// let classes = Classes::new("Btn btn-primary") /// .with_classes(ClassesOp::Add, "Active") -/// .with_classes(ClassesOp::Replace("active".into()), "Disabled") +/// .with_classes(ClassesOp::Remove, "active") +/// .with_classes(ClassesOp::Add, "Disabled") /// .with_classes(ClassesOp::Remove, "btn-primary"); /// /// assert_eq!(classes.get(), Some("btn disabled".to_string())); @@ -109,26 +128,6 @@ impl Classes { } self.0.retain(|c| !to_remove.contains(c.as_str())); } - ClassesOp::Replace(classes_to_replace) => { - let Some(classes_to_replace) = util::normalize_ascii_or_empty( - classes_to_replace.as_ref(), - "ClassesOp::Replace", - ) else { - return self; - }; - let mut pos = self.0.len(); - let mut replaced = false; - for class in classes_to_replace.as_ref().split_ascii_whitespace() { - if let Some(replace_pos) = self.0.iter().position(|c| c == class) { - self.0.remove(replace_pos); - pos = pos.min(replace_pos); - replaced = true; - } - } - if replaced { - self.add(normalized.as_ref().split_ascii_whitespace(), pos); - } - } ClassesOp::Toggle => { for class in normalized.as_ref().split_ascii_whitespace() { if let Some(pos) = self.0.iter().position(|c| c == class) { @@ -138,7 +137,7 @@ impl Classes { } } } - ClassesOp::Set => { + ClassesOp::Reset => { self.0.clear(); self.add(normalized.as_ref().split_ascii_whitespace(), 0); } @@ -168,6 +167,11 @@ impl Classes { // **< Classes GETTERS >************************************************************************ + /// Devuelve `true` si no hay clases. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Devuelve la cadena de clases, si existe. pub fn get(&self) -> Option { if self.0.is_empty() { diff --git a/tests/html_classes.rs b/tests/html_classes.rs index 91eaaad7..e2198335 100644 --- a/tests/html_classes.rs +++ b/tests/html_classes.rs @@ -79,19 +79,19 @@ async fn classes_prepend_ignores_empty_input() { } #[pagetop::test] -async fn classes_set_replaces_entire_list_and_dedups() { - let c = Classes::new("a b c").with_classes(ClassesOp::Set, "X y y Z"); +async fn classes_reset_replaces_entire_list_and_dedups() { + let c = Classes::new("a b c").with_classes(ClassesOp::Reset, "X y y Z"); assert_classes(&c, Some("x y z")); } #[pagetop::test] -async fn classes_set_with_empty_input_clears() { +async fn classes_reset_with_empty_input_clears() { let base = Classes::new("a b"); - let c = base.with_classes(ClassesOp::Set, " \n "); + let c = base.with_classes(ClassesOp::Reset, " \n "); assert_classes(&c, None); } -// **< Mutation operations (remove/toggle/replace) >************************************************ +// **< Mutation operations (remove/toggle) >******************************************************** #[pagetop::test] async fn classes_remove_is_case_insensitive() { @@ -138,49 +138,6 @@ async fn classes_toggle_duplicate_tokens_are_applied_sequentially() { assert_classes(&c, Some("b a")); } -#[pagetop::test] -async fn classes_replace_removes_targets_and_inserts_new_at_min_position() { - let c = Classes::new("a b c d").with_classes(ClassesOp::Replace("c a".into()), "x y"); - assert_classes(&c, Some("x y b d")); -} - -#[pagetop::test] -async fn classes_replace_when_none_found_does_nothing() { - let c = Classes::new("a b").with_classes(ClassesOp::Replace("x y".into()), "c d"); - assert_classes(&c, Some("a b")); -} - -#[pagetop::test] -async fn classes_replace_is_case_insensitive_on_targets_and_new_values_are_normalized() { - let c = Classes::new("btn btn-primary active") - .with_classes(ClassesOp::Replace("BTN-PRIMARY".into()), "Btn-Secondary"); - assert_classes(&c, Some("btn btn-secondary active")); -} - -#[pagetop::test] -async fn classes_replace_with_empty_new_removes_only() { - let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b".into()), " "); - assert_classes(&c, Some("a c")); -} - -#[pagetop::test] -async fn classes_replace_dedups_against_existing_items() { - let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b".into()), "c d"); - assert_classes(&c, Some("a d c")); -} - -#[pagetop::test] -async fn classes_replace_ignores_target_whitespace_and_repetition() { - let c = Classes::new("a b c").with_classes(ClassesOp::Replace(" b b ".into()), "x y"); - assert_classes(&c, Some("a x y c")); -} - -#[pagetop::test] -async fn classes_replace_rejects_non_ascii_targets_is_noop() { - let c = Classes::new("a b c").with_classes(ClassesOp::Replace("b ñ".into()), "x"); - assert_classes(&c, Some("a b c")); -} - // **< Queries (contains) >************************************************************************* #[pagetop::test]