Compare commits
29 commits
5d4f1fb007
...
0121fad94a
| Author | SHA1 | Date | |
|---|---|---|---|
| 0121fad94a | |||
| 35a5221c92 | |||
| 47b6553fe4 | |||
| 0410b8c060 | |||
| 830602b24e | |||
| dfc1bdbc4c | |||
| 3951f1da1a | |||
| 4ccb792db5 | |||
| 2c52af4b9d | |||
| b1ce79c78f | |||
| eb18690a5c | |||
| 87e4eac27c | |||
| 7d43742a11 | |||
| c1afe0e70c | |||
| 019961ed77 | |||
| 7553ed35ec | |||
| 9c58d5e1d6 | |||
| 026448e511 | |||
| 796ae5ce81 | |||
| aa931ea052 | |||
| 8c861bff05 | |||
| fa5489dbb0 | |||
| a0805ed0fb | |||
| bd8a34341d | |||
| 23d4fd8a80 | |||
| b4284f74f8 | |||
| 50abfe3b56 | |||
| 35883bdcde | |||
| 9e625c2b46 |
146 changed files with 5720 additions and 2426 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
[alias]
|
[alias]
|
||||||
ts = ["test", "--features", "testing"] # cargo ts
|
ts = ["test", "--features", "testing"] # cargo ts
|
||||||
tw = ["test", "--workspace", "--features", "testing"] # cargo tw
|
tw = ["test", "--workspace", "--features", "testing"] # cargo tw
|
||||||
|
td = ["test", "--doc", "-p"] # cargo td <crate>
|
||||||
|
|
|
||||||
|
|
@ -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*
|
* [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
|
# 👾 Icono
|
||||||
|
|
||||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||||
|
|
|
||||||
2012
Cargo.lock
generated
2012
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
159
Cargo.toml
159
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]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
|
|
@ -69,17 +9,51 @@ members = [
|
||||||
# Extensions
|
# Extensions
|
||||||
"extensions/pagetop-aliner",
|
"extensions/pagetop-aliner",
|
||||||
"extensions/pagetop-bootsier",
|
"extensions/pagetop-bootsier",
|
||||||
|
"extensions/pagetop-seaorm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
||||||
homepage = "https://pagetop.cillero.es"
|
homepage = "https://pagetop.cillero.es"
|
||||||
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
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 = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||||
|
tempfile = "3.27"
|
||||||
|
terminal_size = "0.4"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-appender = "0.2"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||||
|
unic-langid = { version = "0.9", features = ["macros"] }
|
||||||
|
url = "2.5"
|
||||||
# Helpers
|
# Helpers
|
||||||
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
||||||
pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" }
|
pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" }
|
||||||
|
|
@ -88,5 +62,68 @@ pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||||
# Extensions
|
# Extensions
|
||||||
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
||||||
pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
|
pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
|
||||||
|
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
||||||
# PageTop
|
# PageTop
|
||||||
pagetop = { version = "0.5", path = "." }
|
pagetop = { version = "0.5", path = "." }
|
||||||
|
|
||||||
|
[workspace.dependencies.sea-orm]
|
||||||
|
version = "1.1"
|
||||||
|
features = ["debug-print", "macros", "runtime-tokio-native-tls"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[workspace.dependencies.sea-schema]
|
||||||
|
version = "0.16"
|
||||||
|
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "pagetop"
|
||||||
|
version = "0.5.0"
|
||||||
|
|
||||||
|
description = """
|
||||||
|
Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.
|
||||||
|
"""
|
||||||
|
categories = ["web-programming::http-server"]
|
||||||
|
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
|
||||||
|
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
testing = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
colored.workspace = true
|
||||||
|
config.workspace = true
|
||||||
|
figlet-rs.workspace = true
|
||||||
|
fluent-templates.workspace = true
|
||||||
|
getter-methods.workspace = true
|
||||||
|
indexmap.workspace = true
|
||||||
|
itoa.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
pagetop-macros.workspace = true
|
||||||
|
pagetop-minimal.workspace = true
|
||||||
|
pagetop-statics.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
terminal_size.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
|
tower-http.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-appender.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
unic-langid.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pagetop-aliner.workspace = true
|
||||||
|
pagetop-bootsier.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
pagetop-build.workspace = true
|
||||||
|
|
|
||||||
28
README.md
28
README.md
|
|
@ -11,9 +11,10 @@
|
||||||
[](https://crates.io/crates/pagetop)
|
[](https://crates.io/crates/pagetop)
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
||||||
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
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.
|
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
|
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||||
|
|
@ -58,7 +59,7 @@ impl Extension for HelloWorld {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render()
|
||||||
|
|
@ -109,22 +110,29 @@ 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
|
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
||||||
componentes flexibles.
|
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
|
## 🧪 Pruebas
|
||||||
|
|
||||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
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:
|
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||||
|
|
||||||
| Comando | Descripción |
|
| Comando | Descripción |
|
||||||
| ------- | ----------- |
|
| ----------------------- | --------------------------------------------------------------- |
|
||||||
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
|
| `cargo ts` | Lanza **todos los tests** de `pagetop` |
|
||||||
| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. |
|
| `cargo ts --test util` | Lanza los tests de integración del archivo `tests/util.rs` |
|
||||||
| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. |
|
| `cargo ts --doc locale` | Lanza los *doctests* de `pagetop` cuyo *path* contiene `locale` |
|
||||||
| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. |
|
| `cargo tw` | Lanza **todos los tests** del *workspace* |
|
||||||
|
| `cargo td <crate>` | Lanza los *doctests* de un *crate* concreto del *workspace* |
|
||||||
|
|
||||||
> **Nota**
|
> **Nota**
|
||||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
> * Todos los alias, excepto `cargo td`, aplican la *feature* `testing` para los *crates* que la
|
||||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
> 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
|
## 🚧 Advertencia
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use pagetop_bootsier::prelude::*;
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
include_locales!(LOC from "examples/locale");
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
|
@ -11,12 +11,12 @@ impl Extension for FormControls {
|
||||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
scfg.route("/", service::web::get().to(form_controls));
|
router.route("/", web::get(form_controls))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn form_controls(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(
|
.with_child(
|
||||||
Intro::default()
|
Intro::default()
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,15 @@ use pagetop::prelude::*;
|
||||||
struct HelloName;
|
struct HelloName;
|
||||||
|
|
||||||
impl Extension for HelloName {
|
impl Extension for HelloName {
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
scfg.route("/hello/{name}", service::web::get().to(hello_name));
|
router.route("/hello/{name}", web::get(hello_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_name(
|
async fn hello_name(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
path: service::web::Path<String>,
|
web::Path(name): web::Path<String>,
|
||||||
) -> ResultPage<Markup, ErrorPage> {
|
) -> Result<Markup, ErrorPage> {
|
||||||
let name = path.into_inner();
|
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(Html::with(move |_| {
|
.with_child(Html::with(move |_| {
|
||||||
html! {
|
html! {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ use pagetop::prelude::*;
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
impl Extension for HelloWorld {
|
impl Extension for HelloWorld {
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
scfg.route("/", service::web::get().to(hello_world));
|
router.route("/", web::get(hello_world))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(Html::with(|_| {
|
.with_child(Html::with(|_| {
|
||||||
html! {
|
html! {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ include_locales!(LOC from "examples/locale");
|
||||||
struct IntroColors;
|
struct IntroColors;
|
||||||
|
|
||||||
impl Extension for IntroColors {
|
impl Extension for IntroColors {
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
scfg.route("/", service::web::get().to(intro_colors));
|
router.route("/", web::get(intro_colors))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn intro_colors(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(
|
.with_child(
|
||||||
Intro::default()
|
Intro::default()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use pagetop_bootsier::prelude::*;
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
include_locales!(LOC from "examples/locale");
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
|
@ -8,7 +8,11 @@ struct SuperMenu;
|
||||||
|
|
||||||
impl Extension for SuperMenu {
|
impl Extension for SuperMenu {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
vec![
|
||||||
|
&pagetop_aliner::Aliner,
|
||||||
|
&pagetop_bootsier::Bootsier,
|
||||||
|
&pagetop::base::extension::Welcome,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(&self) {
|
fn initialize(&self) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-aliner"
|
name = "pagetop-aliner"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML
|
Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML
|
||||||
|
|
@ -11,11 +10,15 @@ keywords = ["pagetop", "theme", "css"]
|
||||||
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pagetop.workspace = true
|
pagetop.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
pagetop-build.workspace = true
|
pagetop-build.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
[](https://crates.io/crates/pagetop-aliner)
|
[](https://crates.io/crates/pagetop-aliner)
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
||||||
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🧭 Sobre PageTop
|
## 🧭 Sobre PageTop
|
||||||
|
|
@ -65,7 +64,7 @@ o **fuerza el tema por código** en una página concreta:
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_aliner::Aliner;
|
use pagetop_aliner::Aliner;
|
||||||
|
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Aliner)
|
.with_theme(&Aliner)
|
||||||
.add_child(
|
.add_child(
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta:
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_aliner::Aliner;
|
use pagetop_aliner::Aliner;
|
||||||
|
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Aliner)
|
.with_theme(&Aliner)
|
||||||
.with_child(
|
.with_child(
|
||||||
|
|
@ -83,9 +83,12 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML.
|
include_locales!(LOCALES_ALINER);
|
||||||
|
|
||||||
|
/// 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.
|
/// - Comprobar el funcionamiento de temas, plantillas y regiones.
|
||||||
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
|
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
|
||||||
|
|
@ -94,24 +97,33 @@ use pagetop::prelude::*;
|
||||||
pub struct Aliner;
|
pub struct Aliner;
|
||||||
|
|
||||||
impl Extension for 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<ThemeRef> {
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
Some(&Self)
|
Some(&Self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
static_files_service!(scfg, [aliner] => "/aliner");
|
serve_static_files!(router, [aliner] => "/aliner");
|
||||||
|
router
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme for Aliner {
|
impl Theme for Aliner {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/normalize.css")
|
StyleSheet::from("/pagetop/css/normalize.css")
|
||||||
.with_version("8.0.1")
|
.with_version("8.0.1")
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_assets(AssetsOp::AddStyleSheet(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/basic.css")
|
StyleSheet::from("/pagetop/css/basic.css")
|
||||||
.with_version(PAGETOP_VERSION)
|
.with_version(PAGETOP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
2
extensions/pagetop-aliner/src/locale/en-US/extension.ftl
Normal file
2
extensions/pagetop-aliner/src/locale/en-US/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
extension_name = Aliner
|
||||||
|
extension_description = Minimal theme that schematically shows the HTML page composition.
|
||||||
2
extensions/pagetop-aliner/src/locale/es-ES/extension.ftl
Normal file
2
extensions/pagetop-aliner/src/locale/es-ES/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
extension_name = Aliner
|
||||||
|
extension_description = Tema mínimo que muestra esquemáticamente la composición de las páginas HTML.
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-bootsier"
|
name = "pagetop-bootsier"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
||||||
|
|
@ -11,6 +10,7 @@ keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
|
||||||
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
|
@ -18,5 +18,8 @@ authors.workspace = true
|
||||||
pagetop.workspace = true
|
pagetop.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
pagetop-build.workspace = true
|
pagetop-build.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
[](https://crates.io/crates/pagetop-bootsier)
|
[](https://crates.io/crates/pagetop-bootsier)
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||||
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🧭 Sobre PageTop
|
## 🧭 Sobre PageTop
|
||||||
|
|
@ -65,7 +64,7 @@ o **fuerza el tema por código** en una página concreta:
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_bootsier::Bootsier;
|
use pagetop_bootsier::Bootsier;
|
||||||
|
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Bootsier)
|
.with_theme(&Bootsier)
|
||||||
.add_child(
|
.add_child(
|
||||||
|
|
@ -80,6 +79,13 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 📚 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
|
## 🚧 Advertencia
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,14 @@ include_config!(SETTINGS: Settings => [
|
||||||
"bootsier.max_width" => "1440px",
|
"bootsier.max_width" => "1440px",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/// Ajustes para la sección [`Bootsier`] de [`SETTINGS`].
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`].
|
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub bootsier: Bootsier,
|
pub bootsier: Bootsier,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sección **`[bootsier]`** de la configuración. Forma parte de [`Settings`].
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`].
|
|
||||||
pub struct Bootsier {
|
pub struct Bootsier {
|
||||||
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
||||||
pub max_width: UnitValue,
|
pub max_width: UnitValue,
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ o **fuerza el tema por código** en una página concreta:
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_bootsier::Bootsier;
|
use pagetop_bootsier::Bootsier;
|
||||||
|
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Bootsier)
|
.with_theme(&Bootsier)
|
||||||
.with_child(
|
.with_child(
|
||||||
|
|
@ -96,12 +96,6 @@ pub mod config;
|
||||||
|
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
||||||
/// *Prelude* del tema.
|
|
||||||
pub mod prelude {
|
|
||||||
pub use crate::config::*;
|
|
||||||
pub use crate::theme::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plantillas que Bootsier añade.
|
/// Plantillas que Bootsier añade.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub enum BootsierTemplate {
|
pub enum BootsierTemplate {
|
||||||
|
|
@ -134,13 +128,22 @@ impl Template for BootsierTemplate {
|
||||||
pub struct Bootsier;
|
pub struct Bootsier;
|
||||||
|
|
||||||
impl Extension for 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<ThemeRef> {
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
Some(&Self)
|
Some(&Self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs");
|
serve_static_files!(router, [bootsier_bs] => "/bootsier/bs");
|
||||||
static_files_service!(scfg, [bootsier_js] => "/bootsier/js");
|
serve_static_files!(router, [bootsier_js] => "/bootsier/js");
|
||||||
|
router
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
extension_name = Bootsier
|
||||||
|
extension_description = Bootstrap-based theme with flexible styles and components.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
extension_name = Bootsier
|
||||||
|
extension_description = Tema basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
||||||
|
|
@ -58,7 +58,7 @@ impl BorderColor {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
|
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
|
||||||
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
|
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
|
||||||
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
|
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ impl BreakPoint {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let bp = BreakPoint::MD;
|
/// let bp = BreakPoint::MD;
|
||||||
/// assert_eq!(bp.class_with("col", ""), "col-md");
|
/// assert_eq!(bp.class_with("col", ""), "col-md");
|
||||||
/// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
/// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ impl ButtonColor {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// ButtonColor::Background(Color::Primary).to_class(),
|
/// ButtonColor::Background(Color::Primary).to_class(),
|
||||||
/// "btn-primary"
|
/// "btn-primary"
|
||||||
|
|
@ -132,7 +132,7 @@ impl ButtonSize {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
|
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
|
||||||
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
|
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
|
||||||
/// assert_eq!(ButtonSize::Default.to_class(), "");
|
/// assert_eq!(ButtonSize::Default.to_class(), "");
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl Color {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(Color::Primary.to_class(), "primary");
|
/// assert_eq!(Color::Primary.to_class(), "primary");
|
||||||
/// assert_eq!(Color::Danger.to_class(), "danger");
|
/// assert_eq!(Color::Danger.to_class(), "danger");
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -124,7 +124,7 @@ impl Opacity {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
/// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
||||||
/// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
/// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
||||||
/// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
/// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
||||||
|
|
@ -156,7 +156,7 @@ impl Opacity {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
|
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
|
||||||
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
|
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
|
||||||
/// assert_eq!(Opacity::Default.to_class(), "");
|
/// assert_eq!(Opacity::Default.to_class(), "");
|
||||||
|
|
@ -237,7 +237,7 @@ impl ColorBg {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
|
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
|
||||||
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
|
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
|
||||||
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
|
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
|
||||||
|
|
@ -321,7 +321,7 @@ impl ColorText {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(ColorText::Body.to_class(), "text-body");
|
/// assert_eq!(ColorText::Body.to_class(), "text-body");
|
||||||
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
|
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
|
||||||
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
|
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ impl ScaleSize {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
/// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
||||||
/// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
/// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
||||||
/// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
/// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ impl RoundedRadius {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
/// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
||||||
/// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
/// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
||||||
/// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
/// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
||||||
|
|
@ -103,7 +103,7 @@ impl RoundedRadius {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
|
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
|
||||||
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
|
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
|
||||||
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
|
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ use crate::theme::{ButtonAction, ButtonColor, ButtonSize};
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
/// let save = Button::submit(L10n::n("Save"))
|
/// let save = Button::submit(L10n::n("Save"))
|
||||||
/// .with_color(ButtonColor::Background(Color::Primary));
|
/// .with_color(ButtonColor::Background(Color::Primary));
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -26,45 +26,33 @@ use crate::theme::attrs::{BorderColor, Opacity, ScaleSize, Side};
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// **Borde global:**
|
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
|
/// // Borde global.
|
||||||
/// let b = classes::Border::with(ScaleSize::Two);
|
/// let b = classes::Border::with(ScaleSize::Two);
|
||||||
/// assert_eq!(b.to_class(), "border-2");
|
/// assert_eq!(b.to_class(), "border-2");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Aditivo (solo borde superior):**
|
/// // Aditivo (sólo borde superior):
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
|
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
|
||||||
/// assert_eq!(b.to_class(), "border-top-1");
|
/// assert_eq!(b.to_class(), "border-top-1");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Sustractivo (borde global menos el superior):**
|
/// // Sustractivo (borde global menos el superior):
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
|
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
|
||||||
/// assert_eq!(b.to_class(), "border border-top-0");
|
/// assert_eq!(b.to_class(), "border border-top-0");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
|
/// // Ancho por lado (lado lógico inicial a 2 y final a 4):
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::default()
|
/// let b = classes::Border::default()
|
||||||
/// .with_side(Side::Start, ScaleSize::Two)
|
/// .with_side(Side::Start, ScaleSize::Two)
|
||||||
/// .with_side(Side::End, ScaleSize::Four);
|
/// .with_side(Side::End, ScaleSize::Four);
|
||||||
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
|
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Combinado (ejemplo completo):**
|
/// // Combinado (ejemplo completo):
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::new() // Borde por defecto.
|
/// let b = classes::Border::new() // Borde por defecto.
|
||||||
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
|
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
|
||||||
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
|
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
|
||||||
/// .with_color(BorderColor::Theme(Color::Primary))
|
/// .with_color(BorderColor::Theme(Color::Primary))
|
||||||
/// .with_opacity(Opacity::Half);
|
/// .with_opacity(Opacity::Half);
|
||||||
///
|
|
||||||
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
|
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
|
||||||
/// ```
|
/// ```
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|
@ -158,7 +146,7 @@ impl Border {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// // Convertir explícitamente con `From::from`:
|
/// // Convertir explícitamente con `From::from`:
|
||||||
/// let b = classes::Border::from(ScaleSize::Two);
|
/// let b = classes::Border::from(ScaleSize::Two);
|
||||||
/// assert_eq!(b.to_class(), "border-2");
|
/// assert_eq!(b.to_class(), "border-2");
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ use crate::theme::attrs::{ColorBg, ColorText, Opacity};
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
/// // Sin clases.
|
/// // Sin clases.
|
||||||
/// let s = classes::Background::new();
|
/// let s = classes::Background::new();
|
||||||
/// assert_eq!(s.to_class(), "");
|
/// assert_eq!(s.to_class(), "");
|
||||||
|
|
@ -90,7 +91,7 @@ impl From<(ColorBg, Opacity)> for Background {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
||||||
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -105,7 +106,7 @@ impl From<ColorBg> for Background {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let s: classes::Background = ColorBg::Black.into();
|
/// let s: classes::Background = ColorBg::Black.into();
|
||||||
/// assert_eq!(s.to_class(), "bg-black");
|
/// assert_eq!(s.to_class(), "bg-black");
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -121,7 +122,8 @@ impl From<ColorBg> for Background {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
/// // Sin clases.
|
/// // Sin clases.
|
||||||
/// let s = classes::Text::new();
|
/// let s = classes::Text::new();
|
||||||
/// assert_eq!(s.to_class(), "");
|
/// assert_eq!(s.to_class(), "");
|
||||||
|
|
@ -202,7 +204,7 @@ impl From<(ColorText, Opacity)> for Text {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
||||||
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -218,7 +220,7 @@ impl From<ColorText> for Text {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let s: classes::Text = ColorText::Black.into();
|
/// let s: classes::Text = ColorText::Black.into();
|
||||||
/// assert_eq!(s.to_class(), "text-black");
|
/// assert_eq!(s.to_class(), "text-black");
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::attrs::{ScaleSize, Side};
|
|
||||||
use crate::theme::BreakPoint;
|
use crate::theme::BreakPoint;
|
||||||
|
use crate::theme::attrs::{ScaleSize, Side};
|
||||||
|
|
||||||
// **< Margin >*************************************************************************************
|
// **< Margin >*************************************************************************************
|
||||||
|
|
||||||
|
|
@ -10,7 +10,8 @@ use crate::theme::BreakPoint;
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
|
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
|
||||||
/// assert_eq!(m.to_class(), "mt-3");
|
/// assert_eq!(m.to_class(), "mt-3");
|
||||||
///
|
///
|
||||||
|
|
@ -97,7 +98,8 @@ impl Margin {
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
|
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
|
||||||
/// assert_eq!(p.to_class(), "px-2");
|
/// assert_eq!(p.to_class(), "px-2");
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -14,42 +14,30 @@ use crate::theme::attrs::RoundedRadius;
|
||||||
///
|
///
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// **Radio global:**
|
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
|
/// // Radio global:
|
||||||
/// let r = classes::Rounded::with(RoundedRadius::Default);
|
/// let r = classes::Rounded::with(RoundedRadius::Default);
|
||||||
/// assert_eq!(r.to_class(), "rounded");
|
/// assert_eq!(r.to_class(), "rounded");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Sin redondeo:**
|
/// // Sin redondeo:
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new();
|
/// let r = classes::Rounded::new();
|
||||||
/// assert_eq!(r.to_class(), "");
|
/// assert_eq!(r.to_class(), "");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Radio en las esquinas de un lado lógico:**
|
/// // Radio en las esquinas de un lado lógico:
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
|
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
|
||||||
/// assert_eq!(r.to_class(), "rounded-end-2");
|
/// assert_eq!(r.to_class(), "rounded-end-2");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Radio en una esquina concreta:**
|
/// // Radio en una esquina concreta:
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
|
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
|
||||||
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
||||||
/// ```
|
|
||||||
///
|
///
|
||||||
/// **Combinado (ejemplo completo):**
|
/// // Combinado (ejemplo completo):
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new()
|
/// let r = classes::Rounded::new()
|
||||||
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
|
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
|
||||||
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
|
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
|
||||||
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
|
/// .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");
|
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
|
||||||
/// ```
|
/// ```
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,6 @@
|
||||||
//! Con [`container::Width`](crate::theme::container::Width) se puede definir el ancho y el
|
//! 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
|
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
|
||||||
//! fondo, texto, borde o esquinas redondeadas.
|
//! 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;
|
mod props;
|
||||||
pub use props::{Kind, Width};
|
pub use props::{Kind, Width};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
use pagetop::prelude::*;
|
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
|
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
|
||||||
/// si existen componentes hijos (*children*).
|
/// 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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
||||||
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de
|
//! 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
|
//! 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).
|
//! 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;
|
mod props;
|
||||||
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::*;
|
||||||
|
|
||||||
/// 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))
|
/// 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
|
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra u oculta según
|
||||||
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
/// la interacción del usuario. Admite variaciones para el tamaño y el color del botón, también para
|
||||||
/// apertura, alineación o política de cierre.
|
/// 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
|
/// 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.
|
/// 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
|
/// 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`].
|
/// 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**.
|
/// 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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Dropdown {
|
pub struct Dropdown {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
// **< AutoClose >**********************************************************************************
|
// **< AutoClose >**********************************************************************************
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,4 @@
|
||||||
//! Definiciones para crear formularios ([`Form`]).
|
//! 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;
|
mod props;
|
||||||
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true);
|
/// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true);
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
|
@ -82,7 +82,7 @@ impl Item {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let interests = form::check::Field::new()
|
/// let interests = form::check::Field::new()
|
||||||
/// .with_name("interests")
|
/// .with_name("interests")
|
||||||
/// .with_label(L10n::n("Areas of interest"))
|
/// .with_label(L10n::n("Areas of interest"))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::form;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::form;
|
||||||
|
|
||||||
/// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*).
|
/// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*).
|
||||||
///
|
///
|
||||||
|
|
@ -17,7 +17,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let accept_terms = form::Checkbox::check() // También sirve new() o default().
|
/// let accept_terms = form::Checkbox::check() // También sirve new() o default().
|
||||||
/// .with_name("terms_accepted")
|
/// .with_name("terms_accepted")
|
||||||
/// .with_label(L10n::n("I accept the terms and conditions"))
|
/// .with_label(L10n::n("I accept the terms and conditions"))
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::form;
|
use crate::theme::form;
|
||||||
|
|
||||||
/// Componente para crear un **formulario**.
|
/// Componente para crear un **formulario** ([`form`]).
|
||||||
///
|
///
|
||||||
/// Este componente renderiza un `<form>` 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.
|
/// - `id`: identificador opcional del formulario.
|
||||||
/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS).
|
/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS).
|
||||||
|
|
@ -17,13 +17,33 @@ use crate::theme::form;
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
/// let search = Form::new()
|
///
|
||||||
/// .with_id("search")
|
/// let form_login = Form::new()
|
||||||
/// .with_action("/search")
|
/// .with_id("login")
|
||||||
/// .with_method(form::Method::Get)
|
/// .with_action("/login")
|
||||||
/// .with_child(form::input::Field::search().with_name("q"));
|
/// .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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Form {
|
pub struct Form {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let personal_data = form::Fieldset::new()
|
/// let personal_data = form::Fieldset::new()
|
||||||
/// .with_legend(L10n::n("Personal data"))
|
/// .with_legend(L10n::n("Personal data"))
|
||||||
/// .with_description(L10n::n("Enter your full name and contact email."))
|
/// .with_description(L10n::n("Enter your full name and contact email."))
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let token = form::Hidden::new()
|
/// let token = form::Hidden::new()
|
||||||
/// .with_name("csrf_token")
|
/// .with_name("csrf_token")
|
||||||
/// .with_value("a1b2c3d4e5");
|
/// .with_value("a1b2c3d4e5");
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::form;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::form;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ impl fmt::Display for Mode {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let email = form::input::Field::email()
|
/// let email = form::input::Field::email()
|
||||||
/// .with_name("email")
|
/// .with_name("email")
|
||||||
/// .with_label(L10n::n("Email address"))
|
/// .with_label(L10n::n("Email address"))
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ pub enum CheckboxKind {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// // Correo electrónico con sugerencia semántica del navegador.
|
/// // Correo electrónico con sugerencia semántica del navegador.
|
||||||
/// let ac = form::Autocomplete::email();
|
/// let ac = form::Autocomplete::email();
|
||||||
///
|
///
|
||||||
|
|
@ -244,7 +244,7 @@ impl fmt::Display for Autocomplete {
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let ac = form::Autocomplete::token(form::AutofillField::Username);
|
/// let ac = form::Autocomplete::token(form::AutofillField::Username);
|
||||||
/// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress);
|
/// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress);
|
||||||
/// let ac = form::Autocomplete::section("job", form::AutofillField::Email);
|
/// let ac = form::Autocomplete::section("job", form::AutofillField::Email);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true);
|
/// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true);
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
|
@ -76,7 +76,7 @@ impl Item {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let plan = form::radio::Field::new()
|
/// let plan = form::radio::Field::new()
|
||||||
/// .with_name("plan")
|
/// .with_name("plan")
|
||||||
/// .with_label(L10n::n("Subscription plan"))
|
/// .with_label(L10n::n("Subscription plan"))
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use pagetop::prelude::*;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let volume = form::Range::new()
|
/// let volume = form::Range::new()
|
||||||
/// .with_name("volume")
|
/// .with_name("volume")
|
||||||
/// .with_label(L10n::n("Volume"))
|
/// .with_label(L10n::n("Volume"))
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::form;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::form;
|
||||||
|
|
||||||
// **< Item >***************************************************************************************
|
// **< Item >***************************************************************************************
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true);
|
/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true);
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
|
@ -76,7 +76,7 @@ impl Item {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let group = form::select::Group::new(L10n::n("Europe"))
|
/// 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("es", L10n::n("Spanish")))
|
||||||
/// .with_item(form::select::Item::new("fr", L10n::n("French")));
|
/// .with_item(form::select::Item::new("fr", L10n::n("French")));
|
||||||
|
|
@ -149,7 +149,7 @@ pub enum Entry {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let idioma = form::select::Field::new()
|
/// let idioma = form::select::Field::new()
|
||||||
/// .with_name("language")
|
/// .with_name("language")
|
||||||
/// .with_label(L10n::n("Language"))
|
/// .with_label(L10n::n("Language"))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::form;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::form;
|
||||||
|
|
||||||
/// Componente para crear un **área de texto** de formulario.
|
/// Componente para crear un **área de texto** de formulario.
|
||||||
///
|
///
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::theme::*;
|
||||||
/// let descripcion = form::Textarea::new()
|
/// let descripcion = form::Textarea::new()
|
||||||
/// .with_name("description")
|
/// .with_name("description")
|
||||||
/// .with_label(L10n::n("Description"))
|
/// .with_label(L10n::n("Description"))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::prelude::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
use pagetop::prelude::*;
|
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`].
|
/// A una imagen se le puede:
|
||||||
/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
///
|
||||||
|
/// - 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::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
||||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
/// ([`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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
|
|
@ -53,7 +55,7 @@ impl Component for Image {
|
||||||
{
|
{
|
||||||
(logo.render(cx))
|
(logo.render(cx))
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
image::Source::Responsive(source) => Some(source),
|
image::Source::Responsive(source) => Some(source),
|
||||||
image::Source::Thumbnail(source) => Some(source),
|
image::Source::Thumbnail(source) => Some(source),
|
||||||
|
|
|
||||||
|
|
@ -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`],
|
//! 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
|
//! 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
|
//! 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).
|
//! 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;
|
mod props;
|
||||||
pub use props::{Kind, Layout};
|
pub use props::{Kind, Layout};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,35 @@
|
||||||
use pagetop::prelude::*;
|
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
|
/// 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
|
/// ([`nav::Kind`]) como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`).
|
||||||
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
/// 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**.
|
/// 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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Nav {
|
pub struct Nav {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::*;
|
||||||
|
|
||||||
// **< ItemKind >***********************************************************************************
|
// **< ItemKind >***********************************************************************************
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! 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
|
//! 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))
|
//! 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.
|
//! 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;
|
mod props;
|
||||||
pub use props::{Layout, Position};
|
pub use props::{Layout, Position};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
|
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,139 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::*;
|
||||||
|
|
||||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
const TOGGLE_COLLAPSE: &str = "collapse";
|
||||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
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
|
/// 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
|
/// 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`].
|
/// 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**.
|
/// 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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Navbar {
|
pub struct Navbar {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
|
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
// **< Layout >*************************************************************************************
|
// **< Layout >*************************************************************************************
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,4 @@
|
||||||
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
|
//! 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()))
|
|
||||||
//! );
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
|
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
use crate::theme::*;
|
||||||
|
|
||||||
/// 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
|
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
||||||
/// características principales:
|
/// 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
|
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
|
||||||
/// adecuados para lectores de pantalla y navegación por teclado.
|
/// 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**.
|
/// 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)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Offcanvas {
|
pub struct Offcanvas {
|
||||||
#[getters(skip)]
|
#[getters(skip)]
|
||||||
|
|
|
||||||
29
extensions/pagetop-seaorm/Cargo.toml
Normal file
29
extensions/pagetop-seaorm/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "pagetop-seaorm"
|
||||||
|
version = "0.0.4"
|
||||||
|
|
||||||
|
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
|
||||||
|
edition.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]
|
||||||
|
async-trait.workspace = true
|
||||||
|
pagetop.workspace = true
|
||||||
|
sea-orm.workspace = true
|
||||||
|
sea-schema.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
url.workspace = true
|
||||||
201
extensions/pagetop-seaorm/LICENSE-APACHE
Normal file
201
extensions/pagetop-seaorm/LICENSE-APACHE
Normal file
|
|
@ -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.
|
||||||
21
extensions/pagetop-seaorm/LICENSE-MIT
Normal file
21
extensions/pagetop-seaorm/LICENSE-MIT
Normal file
|
|
@ -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.
|
||||||
201
extensions/pagetop-seaorm/README.md
Normal file
201
extensions/pagetop-seaorm/README.md
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<h1>PageTop SeaORM</h1>
|
||||||
|
|
||||||
|
<p>Proporciona a <strong>PageTop</strong> acceso basado en <a href="https://www.sea-ql.org/SeaORM">SeaORM</a> a bases de datos relacionales.</p>
|
||||||
|
|
||||||
|
[](https://docs.rs/pagetop-seaorm)
|
||||||
|
[](https://crates.io/crates/pagetop-seaorm)
|
||||||
|
[](https://crates.io/crates/pagetop-seaorm)
|
||||||
|
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🧭 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` 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:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_seaorm::install_migrations;
|
||||||
|
|
||||||
|
mod migration;
|
||||||
|
|
||||||
|
struct MyApp;
|
||||||
|
|
||||||
|
impl Extension for MyApp {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
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 [`migration`]:
|
||||||
|
|
||||||
|
```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,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 📚 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).
|
||||||
|
|
||||||
|
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
|
||||||
|
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` | 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` | Absorbido en `migration.rs`, descarta exportaciones del CLI |
|
||||||
|
| `schema.rs` | Integra con ajustes, adaptado de [loco](https://github.com/loco-rs/loco) |
|
||||||
|
| `seaql_migrations.rs` | Integración completa |
|
||||||
|
|
||||||
|
|
||||||
|
## 🚧 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.
|
||||||
85
extensions/pagetop-seaorm/src/config.rs
Normal file
85
extensions/pagetop-seaorm/src/config.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
//! Opciones de configuración de la extensión.
|
||||||
|
//!
|
||||||
|
//! Ejemplo:
|
||||||
|
//!
|
||||||
|
//! ```toml
|
||||||
|
//! [database]
|
||||||
|
//! db_type = "postgres"
|
||||||
|
//! db_name = "db"
|
||||||
|
//! db_user = "user"
|
||||||
|
//! db_pass = "password"
|
||||||
|
//! db_host = "localhost"
|
||||||
|
//! db_port = 5432
|
||||||
|
//! 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.max_pool_size" => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// Ajustes para la sección [`Database`] de [`SETTINGS`].
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub database: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sección **`[database]`** de la configuración. Forma parte de [`Settings`].
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Database {
|
||||||
|
/// 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).
|
||||||
|
pub db_user: String,
|
||||||
|
/// Contraseña para la conexión a la base de datos (para mysql/postgres).
|
||||||
|
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 se omite, se usa el puerto
|
||||||
|
/// predeterminado para el motor: 3306 para MySQL y 5432 para PostgreSQL.
|
||||||
|
pub db_port: Option<u16>,
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
308
extensions/pagetop-seaorm/src/db.rs
Normal file
308
extensions/pagetop-seaorm/src/db.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
//! Definición de entidades y acceso a la base de datos.
|
||||||
|
//!
|
||||||
|
//! 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
|
||||||
|
//! use pagetop_seaorm::db::*;
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! 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<Box<dyn Future>>`.
|
||||||
|
//! 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::*;
|
||||||
|
|
||||||
|
pub use sea_orm::{
|
||||||
|
ActiveValue, DatabaseTransaction, ExecResult, QueryOrder, QuerySelect, TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// Ú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::*;
|
||||||
|
///
|
||||||
|
/// let _conn: &DatabaseConnection = dbconn();
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn dbconn() -> &'static DatabaseConnection {
|
||||||
|
&super::DBCONN
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ejecuta una sentencia SQL en crudo y devuelve su resultado.
|
||||||
|
///
|
||||||
|
/// 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).
|
||||||
|
///
|
||||||
|
/// > **Nota:** no sirve para SELECT porque no devuelve filas. Para leer datos usa [`fetch_all`] o
|
||||||
|
/// > [`fetch_one`].
|
||||||
|
///
|
||||||
|
/// > **Advertencia:** nunca interpoles valores externos en la cadena SQL directamente. Para
|
||||||
|
/// > sentencias con parámetros de usuario usa el sistema de entidades.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// 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<String>) -> Result<ExecResult, DbErr> {
|
||||||
|
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<Q: query::QueryStatementWriter>(
|
||||||
|
stmt: &Q,
|
||||||
|
) -> Result<Vec<QueryResult>, 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<Q: query::QueryStatementWriter>(
|
||||||
|
stmt: &Q,
|
||||||
|
) -> Result<Option<QueryResult>, 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
|
||||||
|
}
|
||||||
249
extensions/pagetop-seaorm/src/lib.rs
Normal file
249
extensions/pagetop-seaorm/src/lib.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
/*!
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<h1>PageTop SeaORM</h1>
|
||||||
|
|
||||||
|
<p>Proporciona a <strong>PageTop</strong> acceso basado en <a href="https://www.sea-ql.org/SeaORM">SeaORM</a> a bases de datos relacionales.</p>
|
||||||
|
|
||||||
|
[](https://docs.rs/pagetop-seaorm)
|
||||||
|
[](https://crates.io/crates/pagetop-seaorm)
|
||||||
|
[](https://crates.io/crates/pagetop-seaorm)
|
||||||
|
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🧭 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` 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:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_seaorm::install_migrations;
|
||||||
|
|
||||||
|
mod migration;
|
||||||
|
|
||||||
|
struct MyApp;
|
||||||
|
|
||||||
|
impl Extension for MyApp {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![
|
||||||
|
&pagetop_seaorm::SeaORM,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize(&self) {
|
||||||
|
install_migrations!(m20240101_000001_create_users);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&MyApp).run()?.await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
|
||||||
|
#![doc(
|
||||||
|
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||||
|
)]
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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<F: std::future::Future>(future: F) -> F::Output {
|
||||||
|
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(future))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) static DBCONN: LazyLock<DatabaseConnection> = 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: 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())
|
||||||
|
.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()))
|
||||||
|
.expect("Failed to set db_pass in connection URL");
|
||||||
|
if let Some(port) = config::SETTINGS.database.db_port {
|
||||||
|
tmp_uri
|
||||||
|
.set_port(Some(port))
|
||||||
|
.expect("Failed to set db_port in connection URL");
|
||||||
|
}
|
||||||
|
tmp_uri.to_string()
|
||||||
|
}
|
||||||
|
config::DbType::Sqlite => {
|
||||||
|
format!("sqlite://{}", &config::SETTINGS.database.db_name)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
run_now(Database::connect::<ConnectOptions>({
|
||||||
|
let mut db_opt = ConnectOptions::new(db_uri);
|
||||||
|
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
|
||||||
|
db_opt
|
||||||
|
}))
|
||||||
|
.expect("Failed to connect to database")
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Implementa la extensión.
|
||||||
|
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(&DBCONN);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
extensions/pagetop-seaorm/src/locale/en-US/extension.ftl
Normal file
2
extensions/pagetop-seaorm/src/locale/en-US/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
extension_name = SeaORM support
|
||||||
|
extension_description = Provides SeaORM-based access to relational databases.
|
||||||
2
extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl
Normal file
2
extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
extension_name = Soporte a SeaORM
|
||||||
|
extension_description = Proporciona acceso basado en SeaORM a bases de datos relacionales.
|
||||||
298
extensions/pagetop-seaorm/src/migration.rs
Normal file
298
extensions/pagetop-seaorm/src/migration.rs
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
//! API para definir y ejecutar migraciones de base de datos.
|
||||||
|
//!
|
||||||
|
//! 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).
|
||||||
|
//!
|
||||||
|
//! 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<YYYYMMDD>_<NNNNNN>_<nombre_descriptivo>`. 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
|
||||||
|
//! `<tipo>(col)` (NOT NULL), `<tipo>_null(col)` (nulable) y `<tipo>_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/lib.rs` (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;
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use migrator::MigratorTrait;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use schema::*;
|
||||||
|
pub use sea_orm::DeriveIden;
|
||||||
|
pub use sea_orm::sea_query::*;
|
||||||
|
|
||||||
|
use pagetop::core::TypeInfo;
|
||||||
|
|
||||||
|
impl<M: MigrationTrait> MigrationName for M {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
// Extrae el módulo contenedor, descartando el segmento final "Migration".
|
||||||
|
TypeInfo::NameTo(-2).of::<M>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elemento de migración listo para incluir en la lista de un [`MigratorTrait`].
|
||||||
|
pub type MigrationItem = Box<dyn MigrationTrait>;
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: MigratorTrait> MigratorBase for M {
|
||||||
|
fn run_up() {
|
||||||
|
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() {
|
||||||
|
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 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::<nombre>::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,
|
||||||
|
/// 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<MigrationItem> {
|
||||||
|
let mut m = Vec::<MigrationItem>::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 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,
|
||||||
|
/// 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<MigrationItem> {
|
||||||
|
let mut m = Vec::<MigrationItem>::new();
|
||||||
|
$(
|
||||||
|
m.push(Box::new(migration::$migration_module::Migration));
|
||||||
|
)*
|
||||||
|
m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Migrator::run_down();
|
||||||
|
}};
|
||||||
|
}
|
||||||
148
extensions/pagetop-seaorm/src/migration/connection.rs
Normal file
148
extensions/pagetop-seaorm/src/migration/connection.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
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> {
|
||||||
|
Connection(&'c DatabaseConnection),
|
||||||
|
Transaction(&'c DatabaseTransaction),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ConnectionTrait for SchemaManagerConnection<'_> {
|
||||||
|
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<ExecResult, DbErr> {
|
||||||
|
match self {
|
||||||
|
SchemaManagerConnection::Connection(conn) => conn.execute(stmt).await,
|
||||||
|
SchemaManagerConnection::Transaction(trans) => trans.execute(stmt).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
|
||||||
|
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<Option<QueryResult>, 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<Vec<QueryResult>, 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 TransactionTrait for SchemaManagerConnection<'_> {
|
||||||
|
async fn begin(&self) -> Result<DatabaseTransaction, DbErr> {
|
||||||
|
match self {
|
||||||
|
SchemaManagerConnection::Connection(conn) => conn.begin().await,
|
||||||
|
SchemaManagerConnection::Transaction(trans) => trans.begin().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn begin_with_config(
|
||||||
|
&self,
|
||||||
|
isolation_level: Option<IsolationLevel>,
|
||||||
|
access_mode: Option<AccessMode>,
|
||||||
|
) -> Result<DatabaseTransaction, DbErr> {
|
||||||
|
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<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
|
||||||
|
where
|
||||||
|
F: for<'a> FnOnce(
|
||||||
|
&'a DatabaseTransaction,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
|
||||||
|
+ Send,
|
||||||
|
T: Send,
|
||||||
|
E: std::fmt::Display + std::fmt::Debug + Send,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await,
|
||||||
|
SchemaManagerConnection::Transaction(trans) => trans.transaction(callback).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn transaction_with_config<F, T, E>(
|
||||||
|
&self,
|
||||||
|
callback: F,
|
||||||
|
isolation_level: Option<IsolationLevel>,
|
||||||
|
access_mode: Option<AccessMode>,
|
||||||
|
) -> Result<T, TransactionError<E>>
|
||||||
|
where
|
||||||
|
F: for<'a> FnOnce(
|
||||||
|
&'a DatabaseTransaction,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
|
||||||
|
+ Send,
|
||||||
|
T: Send,
|
||||||
|
E: std::fmt::Display + std::fmt::Debug + 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
186
extensions/pagetop-seaorm/src/migration/manager.rs
Normal file
186
extensions/pagetop-seaorm/src/migration/manager.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
|
||||||
|
use sea_orm::sea_query::{
|
||||||
|
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)]
|
||||||
|
use sea_schema::probe::SchemaProbe;
|
||||||
|
|
||||||
|
/// Helper struct for writing migration scripts in migration file
|
||||||
|
pub struct SchemaManager<'c> {
|
||||||
|
conn: SchemaManagerConnection<'c>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'c> SchemaManager<'c> {
|
||||||
|
pub fn new<T>(conn: T) -> Self
|
||||||
|
where
|
||||||
|
T: IntoSchemaManagerConnection<'c>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
conn: conn.into_schema_manager_connection(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exec_stmt<S>(&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 SchemaManager<'_> {
|
||||||
|
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 SchemaManager<'_> {
|
||||||
|
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 SchemaManager<'_> {
|
||||||
|
pub async fn has_table<T>(&self, table: T) -> Result<bool, DbErr>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
has_table(&self.conn, table).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn has_column<T, C>(&self, _table: T, _column: C) -> Result<bool, DbErr>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
C: AsRef<str>,
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?;
|
||||||
|
|
||||||
|
res.try_get("", "has_column")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn has_index<T, I>(&self, _table: T, _index: I) -> Result<bool, DbErr>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
I: AsRef<str>,
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?;
|
||||||
|
|
||||||
|
res.try_get("", "has_index")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn has_table<C, T>(conn: &C, _table: T) -> Result<bool, DbErr>
|
||||||
|
where
|
||||||
|
C: ConnectionTrait,
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?;
|
||||||
|
|
||||||
|
res.try_get("", "has_table")
|
||||||
|
}
|
||||||
617
extensions/pagetop-seaorm/src/migration/migrator.rs
Normal file
617
extensions/pagetop-seaorm/src/migration/migrator.rs
Normal file
|
|
@ -0,0 +1,617 @@
|
||||||
|
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, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query, SelectStatement, SimpleExpr,
|
||||||
|
Table, extension::postgres::Type,
|
||||||
|
};
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden,
|
||||||
|
DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement,
|
||||||
|
TransactionTrait,
|
||||||
|
};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use sea_schema::probe::SchemaProbe;
|
||||||
|
|
||||||
|
use super::{IntoSchemaManagerConnection, MigrationTrait, SchemaManager, seaql_migrations};
|
||||||
|
|
||||||
|
/// Status of migration
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
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<dyn MigrationTrait>,
|
||||||
|
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<Box<dyn MigrationTrait>>;
|
||||||
|
|
||||||
|
/// 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<Migration> {
|
||||||
|
Self::migrations()
|
||||||
|
.into_iter()
|
||||||
|
.map(|migration| Migration {
|
||||||
|
migration,
|
||||||
|
status: MigrationStatus::Pending,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get list of applied migrations from database
|
||||||
|
async fn get_migration_models<C>(db: &C) -> Result<Vec<seaql_migrations::Model>, 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<C>(db: &C) -> Result<Vec<Migration>, 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<String> = migration_models
|
||||||
|
.into_iter()
|
||||||
|
.map(|model| model.version)
|
||||||
|
.collect();
|
||||||
|
let migration_in_fs: HashSet<String> = 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<String> = 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<C>(db: &C) -> Result<Vec<Migration>, 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<C>(db: &C) -> Result<Vec<Migration>, 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<C>(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<C>(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::<Self>(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::<Self>(manager, None).await?;
|
||||||
|
exec_up::<Self>(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::<Self>(manager, None).await })
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply pending migrations
|
||||||
|
async fn up<'c, C>(db: C, steps: Option<u32>) -> Result<(), DbErr>
|
||||||
|
where
|
||||||
|
C: IntoSchemaManagerConnection<'c>,
|
||||||
|
{
|
||||||
|
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||||
|
Box::pin(async move { exec_up::<Self>(manager, steps).await })
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rollback applied migrations
|
||||||
|
async fn down<'c, C>(db: C, steps: Option<u32>) -> Result<(), DbErr>
|
||||||
|
where
|
||||||
|
C: IntoSchemaManagerConnection<'c>,
|
||||||
|
{
|
||||||
|
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||||
|
Box::pin(async move { exec_down::<Self>(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<Box<dyn Future<Output = Result<(), DbErr>> + 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<M>(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::<M>(manager, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exec_up<M>(manager: &SchemaManager<'_>, mut steps: Option<u32>) -> 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<M>(manager: &SchemaManager<'_>, mut steps: Option<u32>) -> 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<C>(db: &C) -> SelectStatement
|
||||||
|
where
|
||||||
|
C: ConnectionTrait,
|
||||||
|
{
|
||||||
|
match db.get_database_backend() {
|
||||||
|
#[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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_schema<C>(db: &C) -> SimpleExpr
|
||||||
|
where
|
||||||
|
C: ConnectionTrait,
|
||||||
|
{
|
||||||
|
match db.get_database_backend() {
|
||||||
|
#[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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<C>(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(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,
|
||||||
|
Oid,
|
||||||
|
Typname,
|
||||||
|
Typnamespace,
|
||||||
|
Typelem,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum PgDepend {
|
||||||
|
Table,
|
||||||
|
Objid,
|
||||||
|
Deptype,
|
||||||
|
Refclassid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum PgNamespace {
|
||||||
|
Table,
|
||||||
|
Oid,
|
||||||
|
Nspname,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_pg_types<C>(db: &C) -> SelectStatement
|
||||||
|
where
|
||||||
|
C: ConnectionTrait,
|
||||||
|
{
|
||||||
|
Query::select()
|
||||||
|
.column(PgType::Typname)
|
||||||
|
.from(PgType::Table)
|
||||||
|
.left_join(
|
||||||
|
PgNamespace::Table,
|
||||||
|
Expr::col((PgNamespace::Table, PgNamespace::Oid))
|
||||||
|
.equals((PgType::Table, PgType::Typnamespace)),
|
||||||
|
)
|
||||||
|
.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")),
|
||||||
|
)
|
||||||
|
.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 {
|
||||||
|
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<A> QueryTable for sea_orm::Insert<A>
|
||||||
|
where
|
||||||
|
A: ActiveModelTrait,
|
||||||
|
{
|
||||||
|
type Statement = sea_orm::Insert<A>;
|
||||||
|
|
||||||
|
fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert<A> {
|
||||||
|
sea_orm::QueryTrait::query(&mut self).into_table(table_name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> QueryTable for sea_orm::DeleteMany<E>
|
||||||
|
where
|
||||||
|
E: EntityTrait,
|
||||||
|
{
|
||||||
|
type Statement = sea_orm::DeleteMany<E>;
|
||||||
|
|
||||||
|
fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany<E> {
|
||||||
|
sea_orm::QueryTrait::query(&mut self).from_table(table_name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
612
extensions/pagetop-seaorm/src/migration/schema.rs
Normal file
612
extensions/pagetop-seaorm/src/migration/schema.rs
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
//! Adapted from <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
|
||||||
|
//!
|
||||||
|
//! # 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 pagetop_seaorm::migration::*;
|
||||||
|
//!
|
||||||
|
//! 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(DeriveIden)]
|
||||||
|
//! pub enum Users {
|
||||||
|
//! Table,
|
||||||
|
//! Id,
|
||||||
|
//! Pid,
|
||||||
|
//! Email,
|
||||||
|
//! Name,
|
||||||
|
//! Password,
|
||||||
|
//! ResetToken,
|
||||||
|
//! ResetSentAt,
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use sea_orm::sea_query::{
|
||||||
|
self, Alias, ColumnDef, ColumnType, Expr, Iden, IntoIden, PgInterval, Table,
|
||||||
|
TableCreateStatement,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
enum GeneralIds {
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapping table schema creation.
|
||||||
|
pub fn table_auto<T: IntoIden + 'static>(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<T: IntoIden>(name: T) -> ColumnDef {
|
||||||
|
integer(name).auto_increment().primary_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a UUID primary key
|
||||||
|
pub fn pk_uuid<T: IntoIden>(name: T) -> ColumnDef {
|
||||||
|
uuid(name).primary_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).char_len(length).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).char_len(length).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
char_len(col, length).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).char().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).char().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
char(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).string_len(length).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).string_len(length).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
string_len(col, length).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).string().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).string().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
string(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).text().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).text().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
text(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tiny_integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).tiny_integer().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tiny_integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).tiny_integer().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tiny_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
tiny_integer(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small_integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).small_integer().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small_integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).small_integer().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
small_integer(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).integer().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).integer().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
integer(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn big_integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).big_integer().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn big_integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).big_integer().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn big_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
big_integer(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tiny_unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).tiny_unsigned().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tiny_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).tiny_unsigned().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tiny_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
tiny_unsigned(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small_unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).small_unsigned().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).small_unsigned().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn small_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
small_unsigned(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).unsigned().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).unsigned().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
unsigned(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn big_unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).big_unsigned().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn big_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).big_unsigned().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn big_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
big_unsigned(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn float<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).float().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn float_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).float().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn float_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
float(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn double<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).double().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn double_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).double().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn double_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
double(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimal_len<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.decimal_len(precision, scale)
|
||||||
|
.not_null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimal_len_null<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.decimal_len(precision, scale)
|
||||||
|
.null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimal_len_uniq<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||||
|
decimal_len(col, precision, scale).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimal<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).decimal().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimal_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).decimal().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decimal_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
decimal(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date_time<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).date_time().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date_time_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).date_time().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date_time_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
date_time(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interval<T: IntoIden>(
|
||||||
|
col: T,
|
||||||
|
fields: Option<PgInterval>,
|
||||||
|
precision: Option<u32>,
|
||||||
|
) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.interval(fields, precision)
|
||||||
|
.not_null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interval_null<T: IntoIden>(
|
||||||
|
col: T,
|
||||||
|
fields: Option<PgInterval>,
|
||||||
|
precision: Option<u32>,
|
||||||
|
) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.interval(fields, precision)
|
||||||
|
.null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interval_uniq<T: IntoIden>(
|
||||||
|
col: T,
|
||||||
|
fields: Option<PgInterval>,
|
||||||
|
precision: Option<u32>,
|
||||||
|
) -> ColumnDef {
|
||||||
|
interval(col, fields, precision).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).timestamp().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).timestamp().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
timestamp(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp_with_time_zone<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.timestamp_with_time_zone()
|
||||||
|
.not_null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp_with_time_zone_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).timestamp_with_time_zone().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timestamp_with_time_zone_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
timestamp_with_time_zone(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).time().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).time().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
time(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).date().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).date().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
date(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn year<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).year().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn year_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).year().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn year_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
year(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binary_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).binary_len(length).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binary_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).binary_len(length).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binary_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
binary_len(col, length).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binary<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).binary().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binary_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).binary().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binary_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
binary(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn var_binary<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).var_binary(length).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn var_binary_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).var_binary(length).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn var_binary_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
var_binary(col, length).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bit<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).bit(length).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bit_null<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).bit(length).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bit_uniq<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
|
||||||
|
bit(col, length).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn varbit<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).varbit(length).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn varbit_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).varbit(length).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn varbit_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||||
|
varbit(col, length).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blob<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).blob().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blob_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).blob().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blob_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
blob(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn boolean<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).boolean().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn boolean_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).boolean().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn boolean_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
boolean(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money_len<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.money_len(precision, scale)
|
||||||
|
.not_null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money_len_null<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.money_len(precision, scale)
|
||||||
|
.null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money_len_uniq<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||||
|
money_len(col, precision, scale).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).money().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).money().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
money(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).json().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).json().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
json(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_binary<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).json_binary().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_binary_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).json_binary().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_binary_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
json_binary(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uuid<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).uuid().not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uuid_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).uuid().null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uuid_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||||
|
uuid(col).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn custom<T: IntoIden, N: IntoIden>(col: T, name: N) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).custom(name).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn custom_null<T: IntoIden, N: IntoIden>(col: T, name: N) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).custom(name).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enumeration<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
|
||||||
|
where
|
||||||
|
T: IntoIden,
|
||||||
|
N: IntoIden,
|
||||||
|
S: IntoIden,
|
||||||
|
V: IntoIterator<Item = S>,
|
||||||
|
{
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.enumeration(name, variants)
|
||||||
|
.not_null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enumeration_null<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
|
||||||
|
where
|
||||||
|
T: IntoIden,
|
||||||
|
N: IntoIden,
|
||||||
|
S: IntoIden,
|
||||||
|
V: IntoIterator<Item = S>,
|
||||||
|
{
|
||||||
|
ColumnDef::new(col)
|
||||||
|
.enumeration(name, variants)
|
||||||
|
.null()
|
||||||
|
.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enumeration_uniq<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
|
||||||
|
where
|
||||||
|
T: IntoIden,
|
||||||
|
N: IntoIden,
|
||||||
|
S: IntoIden,
|
||||||
|
V: IntoIterator<Item = S>,
|
||||||
|
{
|
||||||
|
enumeration(col, name, variants).unique_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn array<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).array(elem_type).not_null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn array_null<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
|
||||||
|
ColumnDef::new(col).array(elem_type).null().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn array_uniq<T: IntoIden>(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<T: Into<String>>(name: T) -> Alias {
|
||||||
|
Alias::new(name)
|
||||||
|
}
|
||||||
15
extensions/pagetop-seaorm/src/migration/seaql_migrations.rs
Normal file
15
extensions/pagetop-seaorm/src/migration/seaql_migrations.rs
Normal file
|
|
@ -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 {}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-build"
|
name = "pagetop-build"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el
|
Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el
|
||||||
|
|
@ -12,9 +11,10 @@ keywords = ["pagetop", "build", "assets", "resources", "static"]
|
||||||
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
grass = "0.13"
|
grass.workspace = true
|
||||||
pagetop-statics.workspace = true
|
pagetop-statics.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro
|
||||||
usen nombres diferentes.
|
usen nombres diferentes.
|
||||||
|
|
||||||
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
|
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
|
||||||
en [`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:
|
para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
|
|
@ -105,7 +105,7 @@ pub struct MyExtension;
|
||||||
impl Extension for MyExtension {
|
impl Extension for MyExtension {
|
||||||
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||||
static_files_service!(scfg, guides => "/ruta/a/guides");
|
serve_static_files!(scfg, guides => "/ruta/a/guides");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ No hay ningún problema en generar más de un conjunto de recursos para cada pro
|
||||||
usen nombres diferentes.
|
usen nombres diferentes.
|
||||||
|
|
||||||
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
|
Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos
|
||||||
en [`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:
|
para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
|
|
@ -104,9 +104,10 @@ use pagetop::prelude::*;
|
||||||
pub struct MyExtension;
|
pub struct MyExtension;
|
||||||
|
|
||||||
impl Extension for MyExtension {
|
impl Extension for MyExtension {
|
||||||
/// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
/// Registra los recursos de `guides` en el router bajo `/ruta/a/guides`.
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, mut router: Router) -> Router {
|
||||||
static_files_service!(scfg, guides => "/ruta/a/guides");
|
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"
|
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use grass::{from_path, Options, OutputStyle};
|
use grass::{Options, OutputStyle, from_path};
|
||||||
use pagetop_statics::{resource_dir, ResourceDir};
|
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::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
@ -202,9 +203,11 @@ impl StaticFilesBundle {
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
// 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 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.
|
// Limpia el directorio temporal de ejecuciones previas, si existe.
|
||||||
if temp_dir.exists() {
|
if temp_dir.exists() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-macros"
|
name = "pagetop-macros"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Una colección de macros que mejoran la experiencia de desarrollo con PageTop.
|
Una colección de macros que mejoran la experiencia de desarrollo con PageTop.
|
||||||
|
|
@ -11,6 +10,7 @@ keywords = ["pagetop", "macros", "proc-macros", "codegen"]
|
||||||
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ authors.workspace = true
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2 = "1.0"
|
proc-macro2.workspace = true
|
||||||
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
proc-macro2-diagnostics.workspace = true
|
||||||
quote = "1.0"
|
quote.workspace = true
|
||||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
syn.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ mod smart_default;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::{quote, quote_spanned};
|
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)).
|
/// Macro para escribir plantillas HTML (basada en [Maud](https://docs.rs/maud)).
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
|
|
@ -164,7 +164,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
||||||
/// documentación se mostrará la entrada del método `with_...()`.
|
/// documentación se mostrará la entrada del método `with_...()`.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
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();
|
let ts: proc_macro2::TokenStream = item.clone().into();
|
||||||
|
|
||||||
|
|
@ -451,7 +451,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let mut output: TokenStream = (quote! {
|
let mut output: TokenStream = (quote! {
|
||||||
#[::pagetop::service::rt::main(system = "::pagetop::service::rt::System")]
|
#[::tokio::main]
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
|
@ -461,6 +461,9 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
/// Define funciones de prueba asíncronas para usar con PageTop.
|
/// 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
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
|
|
@ -472,7 +475,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let mut output: TokenStream = (quote! {
|
let mut output: TokenStream = (quote! {
|
||||||
#[::pagetop::service::rt::test(system = "::pagetop::service::rt::System")]
|
#[::tokio::test(flavor = "multi_thread")]
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use proc_macro2::TokenStream;
|
||||||
use proc_macro2_diagnostics::{Diagnostic, SpanDiagnosticExt};
|
use proc_macro2_diagnostics::{Diagnostic, SpanDiagnosticExt};
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::{
|
use syn::{
|
||||||
braced, bracketed,
|
Error, Expr, Ident, Lit, LitBool, LitInt, LitStr, Local, Pat, Stmt, braced, bracketed,
|
||||||
ext::IdentExt,
|
ext::IdentExt,
|
||||||
parenthesized,
|
parenthesized,
|
||||||
parse::{Lookahead1, Parse, ParseStream},
|
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,
|
At, Brace, Bracket, Colon, Comma, Dot, Else, Eq, FatArrow, For, If, In, Let, Match, Minus,
|
||||||
Paren, Pound, Question, Semi, Slash, While,
|
Paren, Pound, Question, Semi, Slash, While,
|
||||||
},
|
},
|
||||||
Error, Expr, Ident, Lit, LitBool, LitInt, LitStr, Local, Pat, Stmt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -213,6 +212,7 @@ impl DiagnosticParse for Element {
|
||||||
|| input.peek(Lit)
|
|| input.peek(Lit)
|
||||||
|| input.peek(Dot)
|
|| input.peek(Dot)
|
||||||
|| input.peek(Pound)
|
|| input.peek(Pound)
|
||||||
|
|| input.peek(Paren)
|
||||||
{
|
{
|
||||||
let attr = input.diagnostic_parse(diagnostics)?;
|
let attr = input.diagnostic_parse(diagnostics)?;
|
||||||
|
|
||||||
|
|
@ -347,6 +347,10 @@ pub enum Attribute {
|
||||||
name: HtmlName,
|
name: HtmlName,
|
||||||
attr_type: AttributeType,
|
attr_type: AttributeType,
|
||||||
},
|
},
|
||||||
|
Splice {
|
||||||
|
paren_token: Paren,
|
||||||
|
expr: Expr,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagnosticParse for Attribute {
|
impl DiagnosticParse for Attribute {
|
||||||
|
|
@ -375,6 +379,12 @@ impl DiagnosticParse for Attribute {
|
||||||
pound_token: input.parse()?,
|
pound_token: input.parse()?,
|
||||||
name: input.diagnostic_parse(diagnostics)?,
|
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 {
|
} else {
|
||||||
let name = input.diagnostic_parse::<HtmlName>(diagnostics)?;
|
let name = input.diagnostic_parse::<HtmlName>(diagnostics)?;
|
||||||
|
|
||||||
|
|
@ -425,6 +435,11 @@ impl ToTokens for Attribute {
|
||||||
name.to_tokens(tokens);
|
name.to_tokens(tokens);
|
||||||
attr_type.to_tokens(tokens);
|
attr_type.to_tokens(tokens);
|
||||||
}
|
}
|
||||||
|
Self::Splice { paren_token, expr } => {
|
||||||
|
paren_token.surround(tokens, |tokens| {
|
||||||
|
expr.to_tokens(tokens);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1079,7 +1094,7 @@ impl<E: ToTokens> ToTokens for MatchArm<E> {
|
||||||
|
|
||||||
pub trait DiagnosticParse: Sized {
|
pub trait DiagnosticParse: Sized {
|
||||||
fn diagnostic_parse(input: ParseStream, diagnostics: &mut Vec<Diagnostic>)
|
fn diagnostic_parse(input: ParseStream, diagnostics: &mut Vec<Diagnostic>)
|
||||||
-> syn::Result<Self>;
|
-> syn::Result<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: DiagnosticParse> DiagnosticParse for Box<T> {
|
impl<T: DiagnosticParse> DiagnosticParse for Box<T> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use proc_macro2::{Ident, Span, TokenStream};
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
use quote::{quote, ToTokens};
|
use quote::{ToTokens, quote};
|
||||||
use syn::{parse_quote, token::Brace, Expr, Local};
|
use syn::{Expr, Local, parse_quote, token::Brace};
|
||||||
|
|
||||||
use crate::maud::{ast::*, escape};
|
use crate::maud::{ast::*, escape};
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ impl Generator {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
fn attrs(&self, attrs: Vec<Attribute>, build: &mut Builder) {
|
||||||
let (classes, id, named_attrs) = split_attrs(attrs);
|
let (classes, id, named_attrs, spliced) = split_attrs(attrs);
|
||||||
|
|
||||||
if !classes.is_empty() {
|
if !classes.is_empty() {
|
||||||
let mut toggle_class_exprs = vec![];
|
let mut toggle_class_exprs = vec![];
|
||||||
|
|
@ -184,6 +184,9 @@ impl Generator {
|
||||||
for (name, attr_type) in named_attrs {
|
for (name, attr_type) in named_attrs {
|
||||||
self.attr(name, attr_type, build);
|
self.attr(name, attr_type, build);
|
||||||
}
|
}
|
||||||
|
for expr in spliced {
|
||||||
|
self.splice(expr, build);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_flow<E: Into<Element>>(&self, control_flow: ControlFlow<E>, build: &mut Builder) {
|
fn control_flow<E: Into<Element>>(&self, control_flow: ControlFlow<E>, build: &mut Builder) {
|
||||||
|
|
@ -316,10 +319,12 @@ fn split_attrs(
|
||||||
Vec<(HtmlNameOrMarkup, Option<Expr>)>,
|
Vec<(HtmlNameOrMarkup, Option<Expr>)>,
|
||||||
Option<HtmlNameOrMarkup>,
|
Option<HtmlNameOrMarkup>,
|
||||||
Vec<(HtmlName, AttributeType)>,
|
Vec<(HtmlName, AttributeType)>,
|
||||||
|
Vec<Expr>,
|
||||||
) {
|
) {
|
||||||
let mut classes = vec![];
|
let mut classes = vec![];
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
let mut named_attrs = vec![];
|
let mut named_attrs = vec![];
|
||||||
|
let mut spliced = vec![];
|
||||||
|
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
match attr {
|
match attr {
|
||||||
|
|
@ -328,10 +333,11 @@ fn split_attrs(
|
||||||
}
|
}
|
||||||
Attribute::Id { name, .. } => id = Some(name),
|
Attribute::Id { name, .. } => id = Some(name),
|
||||||
Attribute::Named { name, attr_type } => named_attrs.push((name, attr_type)),
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
|
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use syn::DeriveInput;
|
||||||
use syn::parse::Error;
|
use syn::parse::Error;
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::DeriveInput;
|
|
||||||
|
|
||||||
use crate::smart_default::default_attr::{ConversionStrategy, DefaultAttr};
|
use crate::smart_default::default_attr::{ConversionStrategy, DefaultAttr};
|
||||||
use crate::smart_default::util::find_only;
|
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();
|
let mut doc = String::new();
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let body_tt = match body {
|
let body_tt = match body {
|
||||||
syn::Fields::Named(ref fields) => {
|
syn::Fields::Named(fields) => {
|
||||||
doc.push_str(" {");
|
doc.push_str(" {");
|
||||||
let result = {
|
let result = {
|
||||||
let field_assignments = fields
|
let field_assignments = fields
|
||||||
|
|
@ -101,7 +101,7 @@ fn default_body_tt(body: &syn::Fields) -> Result<(TokenStream, String), Error> {
|
||||||
doc.push('}');
|
doc.push('}');
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
syn::Fields::Unnamed(ref fields) => {
|
syn::Fields::Unnamed(fields) => {
|
||||||
doc.push('(');
|
doc.push('(');
|
||||||
let result = {
|
let result = {
|
||||||
let field_assignments = fields
|
let field_assignments = fields
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::{parse::Error, MetaNameValue};
|
use syn::{MetaNameValue, parse::Error};
|
||||||
|
|
||||||
use crate::smart_default::util::find_only;
|
use crate::smart_default::util::find_only;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-minimal"
|
name = "pagetop-minimal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones
|
Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones
|
||||||
|
|
@ -12,10 +11,11 @@ keywords = ["pagetop", "build", "assets", "resources", "static"]
|
||||||
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
concat-string = "1.0"
|
concat-string.workspace = true
|
||||||
indoc = "2.0"
|
indoc.workspace = true
|
||||||
pastey = "0.2"
|
pastey.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-statics"
|
name = "pagetop-statics"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Librería para automatizar la recopilación de recursos estáticos en PageTop.
|
Librería para automatizar la recopilación de recursos estáticos en PageTop.
|
||||||
|
|
@ -11,6 +10,7 @@ keywords = ["pagetop", "build", "static", "resources", "file"]
|
||||||
|
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
|
@ -19,15 +19,11 @@ default = ["change-detection"]
|
||||||
sort = []
|
sort = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
change-detection = { version = "1.2", optional = true }
|
change-detection = { workspace = true, optional = true }
|
||||||
mime_guess = "2.0"
|
mime_guess.workspace = true
|
||||||
path-slash = "0.2"
|
path-slash.workspace = true
|
||||||
|
|
||||||
actix-web.workspace = true
|
|
||||||
derive_more = "0.99.17"
|
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
change-detection = { version = "1.2", optional = true }
|
change-detection = { workspace = true, optional = true }
|
||||||
mime_guess = "2.0"
|
mime_guess.workspace = true
|
||||||
path-slash = "0.2"
|
path-slash.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -11,30 +11,25 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🧭 Sobre PageTop
|
## Sobre PageTop
|
||||||
|
|
||||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
[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
|
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||||
configurables, basadas en HTML, CSS y JavaScript.
|
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
|
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
|
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||||
compilación.
|
compilación.
|
||||||
|
|
||||||
|
## Créditos
|
||||||
|
|
||||||
## 📚 Créditos
|
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
|
||||||
Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files)
|
[Alexander Korolev](https://crates.io/users/kilork), bajo licencia MIT/Apache 2.0. La implementación
|
||||||
(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y
|
se integra en PageTop para evitar que cada proyecto tenga que declarar `static-files` manualmente
|
||||||
[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión
|
como dependencia en su `Cargo.toml`.
|
||||||
[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`.
|
|
||||||
|
|
||||||
|
|
||||||
## 🚧 Advertencia
|
## 🚧 Advertencia
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use resource_dir::resource_dir;
|
||||||
mod sets {
|
mod sets {
|
||||||
include!("src/sets.rs");
|
include!("src/sets.rs");
|
||||||
}
|
}
|
||||||
use sets::{generate_resources_sets, SplitByCount};
|
use sets::{SplitByCount, generate_resources_sets};
|
||||||
|
|
||||||
use std::{env, path::Path};
|
use std::{env, path::Path};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,11 @@ compilación.
|
||||||
|
|
||||||
## Créditos
|
## Créditos
|
||||||
|
|
||||||
Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files)
|
Para ello, adapta el código de [static-files](https://crates.io/crates/static_files) (versión
|
||||||
(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y
|
[0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) desarrollado por
|
||||||
[actix-web-static-files](https://crates.io/crates/actix_web_static_files) (versión
|
[Alexander Korolev](https://crates.io/users/kilork), bajo licencia MIT/Apache 2.0. La implementación
|
||||||
[4.0.1](https://github.com/kilork/actix-web-static-files/tree/v4.0.1)), desarrollados ambos por
|
se integra en PageTop para evitar que cada proyecto tenga que declarar `static-files` manualmente
|
||||||
[Alexander Korolev](https://crates.io/users/kilork).
|
como dependencia en su `Cargo.toml`.
|
||||||
|
|
||||||
Estas implementaciones se integran en PageTop para evitar que cada proyecto tenga que declarar
|
|
||||||
`static-files` manualmente como dependencia en su `Cargo.toml`.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#![doc(test(no_crate_inject))]
|
#![doc(test(no_crate_inject))]
|
||||||
|
|
@ -44,13 +41,10 @@ Estas implementaciones se integran en PageTop para evitar que cada proyecto teng
|
||||||
|
|
||||||
/// Resource definition and single module based generation.
|
/// Resource definition and single module based generation.
|
||||||
pub mod resource;
|
pub mod resource;
|
||||||
pub use resource::Resource as StaticResource;
|
pub use resource::Resource as StaticFile;
|
||||||
|
|
||||||
mod resource_dir;
|
mod resource_dir;
|
||||||
pub use resource_dir::{resource_dir, ResourceDir};
|
pub use resource_dir::{ResourceDir, resource_dir};
|
||||||
|
|
||||||
mod resource_files;
|
|
||||||
pub use resource_files::{ResourceFiles, UriSegmentError};
|
|
||||||
|
|
||||||
/// Support for module based generations. Use it for large data sets (more than 128 Mb).
|
/// Support for module based generations. Use it for large data sets (more than 128 Mb).
|
||||||
pub mod sets;
|
pub mod sets;
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,9 @@ pub fn generate_resources<P: AsRef<Path>, G: AsRef<Path>>(
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use std::collections::HashMap;
|
/// 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"))
|
/// include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs"))
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
|
@ -221,7 +221,7 @@ pub(crate) fn generate_function_header<F: Write>(
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
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> {{",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::sets::{generate_resources_sets, SplitByCount};
|
use super::sets::{SplitByCount, generate_resources_sets};
|
||||||
use std::{
|
use std::{
|
||||||
env, io,
|
env, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
|
|
||||||
|
|
@ -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<String>,
|
|
||||||
inner: Rc<ResourceFilesInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<S: ToString>(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<dyn Guard> {
|
|
||||||
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<ResourceFilesInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ResourceFilesInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ServiceRequest> for ResourceFiles {
|
|
||||||
type Config = ();
|
|
||||||
type Response = ServiceResponse;
|
|
||||||
type Error = Error;
|
|
||||||
type Service = ResourceFilesService;
|
|
||||||
type InitError = ();
|
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
|
||||||
|
|
||||||
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<String>,
|
|
||||||
#[deref]
|
|
||||||
inner: Rc<ResourceFilesInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Service<ServiceRequest> for ResourceFilesService {
|
|
||||||
type Response = ServiceResponse;
|
|
||||||
type Error = Error;
|
|
||||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
|
||||||
|
|
||||||
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::<header::IfMatch>() {
|
|
||||||
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::<header::IfNoneMatch>() {
|
|
||||||
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<T: Send + Sync + 'static>() {}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error_impl() {
|
|
||||||
// ensure backwards compatibility when migrating away from failure
|
|
||||||
assert_send_and_sync::<UriSegmentError>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return `BadRequest` for `UriSegmentError`
|
|
||||||
impl ResponseError for UriSegmentError {
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
|
||||||
HttpResponse::new(StatusCode::BAD_REQUEST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
|
|
||||||
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("/"))
|
|
||||||
}
|
|
||||||
|
|
@ -5,8 +5,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::resource::{
|
use super::resource::{
|
||||||
collect_resources, generate_function_end, generate_function_header, generate_resource_insert,
|
DEFAULT_VARIABLE_NAME, collect_resources, generate_function_end, generate_function_header,
|
||||||
generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME,
|
generate_resource_insert, generate_uses, generate_variable_header, generate_variable_return,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Defines the split strategie.
|
/// Defines the split strategie.
|
||||||
|
|
@ -116,7 +116,7 @@ where
|
||||||
writeln!(
|
writeln!(
|
||||||
module_file,
|
module_file,
|
||||||
"
|
"
|
||||||
use ::{crate_name}::StaticResource;
|
use ::{crate_name}::StaticFile;
|
||||||
use ::std::collections::HashMap;"
|
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)]
|
"#[allow(clippy::wildcard_imports)]
|
||||||
use super::*;
|
use super::*;
|
||||||
#[allow(clippy::unreadable_literal)]
|
#[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)
|
Ok(set_module)
|
||||||
|
|
|
||||||
8
rustfmt.toml
Normal file
8
rustfmt.toml
Normal file
|
|
@ -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"
|
||||||
149
src/app.rs
149
src/app.rs
|
|
@ -5,25 +5,21 @@ mod figfont;
|
||||||
use crate::core::{extension, extension::ExtensionRef};
|
use crate::core::{extension, extension::ExtensionRef};
|
||||||
use crate::html::Markup;
|
use crate::html::Markup;
|
||||||
use crate::locale::Locale;
|
use crate::locale::Locale;
|
||||||
use crate::response::page::{ErrorPage, ResultPage};
|
use crate::response::page::ErrorPage;
|
||||||
use crate::service::HttpRequest;
|
use crate::web::{HttpRequest, Router};
|
||||||
use crate::{global, service, trace, PAGETOP_VERSION};
|
use crate::{PAGETOP_VERSION, global, trace};
|
||||||
|
|
||||||
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
|
|
||||||
use actix_session::storage::CookieSessionStore;
|
|
||||||
use actix_session::SessionMiddleware;
|
|
||||||
|
|
||||||
use substring::Substring;
|
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
/// Punto de entrada de una aplicación PageTop.
|
/// Punto de entrada de una aplicación PageTop.
|
||||||
///
|
///
|
||||||
/// No almacena datos, **encapsula** el inicio completo de configuración y puesta en marcha. Para
|
/// No almacena datos, **encapsula** el inicio completo de la configuración y puesta en marcha de la
|
||||||
/// instanciarla se puede usar [`new()`](Application::new) o [`prepare()`](Application::prepare).
|
/// aplicación. Para instanciarla se puede usar [`new()`](Application::new) o
|
||||||
/// Después sólo hay que llamar a [`run()`](Application::run) para ejecutar la aplicación (o a
|
/// [`prepare()`](Application::prepare). Después sólo hay que llamar a [`run()`](Application::run)
|
||||||
/// [`test()`](Application::test) si se está preparando un entorno de pruebas).
|
/// para ejecutar la aplicación (o a [`test()`](Application::test) si se está preparando un entorno
|
||||||
|
/// de pruebas).
|
||||||
pub struct Application;
|
pub struct Application;
|
||||||
|
|
||||||
impl Default for Application {
|
impl Default for Application {
|
||||||
|
|
@ -33,24 +29,24 @@ impl Default for Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn new() -> Self {
|
||||||
Self::internal_prepare(None)
|
Self::internal_prepare(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepara una instancia de la aplicación a partir de una extensión raíz.
|
/// Prepara una instancia de la aplicación a partir de una extensión raíz.
|
||||||
///
|
///
|
||||||
/// Esa extensión suele declarar:
|
/// 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
|
||||||
/// - Sus propias dependencias (que se habilitarán automáticamente).
|
/// la extensión raíz.
|
||||||
/// - Una lista de extensiones que deben deshabilitarse si estuvieran activadas.
|
|
||||||
///
|
|
||||||
/// Esto simplifica el arranque en escenarios complejos.
|
|
||||||
pub fn prepare(root_extension: ExtensionRef) -> Self {
|
pub fn prepare(root_extension: ExtensionRef) -> Self {
|
||||||
Self::internal_prepare(Some(root_extension))
|
Self::internal_prepare(Some(root_extension))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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<ExtensionRef>) -> Self {
|
fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self {
|
||||||
// Al arrancar muestra una cabecera para la aplicación.
|
// Al arrancar muestra una cabecera para la aplicación.
|
||||||
Self::show_banner();
|
Self::show_banner();
|
||||||
|
|
@ -73,10 +69,10 @@ impl Application {
|
||||||
Self
|
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() {
|
fn show_banner() {
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use terminal_size::{terminal_size, Width};
|
use terminal_size::{Width, terminal_size};
|
||||||
|
|
||||||
if global::SETTINGS.app.startup_banner != global::StartupBanner::Off {
|
if global::SETTINGS.app.startup_banner != global::StartupBanner::Off {
|
||||||
// Nombre de la aplicación, ajustado al ancho del terminal si es necesario.
|
// Nombre de la aplicación, ajustado al ancho del terminal si es necesario.
|
||||||
|
|
@ -85,8 +81,8 @@ impl Application {
|
||||||
if let Some((Width(term_width), _)) = terminal_size() {
|
if let Some((Width(term_width), _)) = terminal_size() {
|
||||||
if term_width >= 80 {
|
if term_width >= 80 {
|
||||||
let maxlen: usize = ((term_width / 10) - 2).into();
|
let maxlen: usize = ((term_width / 10) - 2).into();
|
||||||
let mut app = app_name.substring(0, maxlen).to_string();
|
let mut app: String = app_name.chars().take(maxlen).collect();
|
||||||
if app_name.len() > maxlen {
|
if app_name.chars().count() > maxlen {
|
||||||
app = format!("{app}...");
|
app = format!("{app}...");
|
||||||
}
|
}
|
||||||
if let Some(ff) = figfont::FIGFONT.convert(&app) {
|
if let Some(ff) = figfont::FIGFONT.convert(&app) {
|
||||||
|
|
@ -103,7 +99,7 @@ impl Application {
|
||||||
// Descripción de la aplicación.
|
// Descripción de la aplicación.
|
||||||
if !global::SETTINGS.app.description.is_empty() {
|
if !global::SETTINGS.app.description.is_empty() {
|
||||||
println!("{}", global::SETTINGS.app.description.cyan());
|
println!("{}", global::SETTINGS.app.description.cyan());
|
||||||
};
|
}
|
||||||
|
|
||||||
// Versión de PageTop.
|
// Versión de PageTop.
|
||||||
println!(
|
println!(
|
||||||
|
|
@ -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.
|
/// Arranca el servidor web de la aplicación.
|
||||||
///
|
///
|
||||||
/// Devuelve [`std::io::Error`] si el *socket* no puede enlazarse (por puerto en uso, permisos,
|
/// Enlaza el puerto del servidor web de forma síncrona (puede fallar con [`std::io::Error`] si
|
||||||
/// etc.).
|
/// el puerto ya está en uso o el proceso carece de permisos) y devuelve un [`Future`] que
|
||||||
pub fn run(self) -> Result<service::Server, Error> {
|
/// ejecuta el bucle de atención de peticiones. El patrón habitual es:
|
||||||
// Genera clave secreta para firmar y verificar cookies.
|
///
|
||||||
let secret_key = service::cookie::Key::generate();
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
// Prepara el servidor web.
|
///
|
||||||
Ok(service::HttpServer::new(move || {
|
/// struct MyApp;
|
||||||
Self::service_app()
|
///
|
||||||
.wrap(tracing_actix_web::TracingLogger::default())
|
/// impl Extension for MyApp {}
|
||||||
.wrap(
|
///
|
||||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
/// #[pagetop::main]
|
||||||
.session_lifecycle(match global::SETTINGS.server.session_lifetime {
|
/// async fn main() -> std::io::Result<()> {
|
||||||
0 => SessionLifecycle::BrowserSession(BrowserSession::default()),
|
/// Application::prepare(&MyApp).run()?.await
|
||||||
_ => SessionLifecycle::PersistentSession(
|
/// }
|
||||||
PersistentSession::default().session_ttl(
|
/// ```
|
||||||
service::cookie::time::Duration::seconds(
|
pub fn run(self) -> Result<impl Future<Output = Result<(), Error>>, Error> {
|
||||||
global::SETTINGS.server.session_lifetime,
|
let addr = format!(
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.bind(format!(
|
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
&global::SETTINGS.server.bind_address,
|
global::SETTINGS.server.bind_address,
|
||||||
&global::SETTINGS.server.bind_port
|
global::SETTINGS.server.bind_port
|
||||||
))?
|
);
|
||||||
.run())
|
|
||||||
|
// 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.
|
/// Devuelve el servidor web configurado para usarlo en pruebas de integración.
|
||||||
pub fn test(
|
pub fn test(self) -> Router {
|
||||||
self,
|
Self::build_router()
|
||||||
) -> service::App<
|
|
||||||
impl service::Factory<
|
|
||||||
service::Request,
|
|
||||||
Config = (),
|
|
||||||
Response = service::Response<service::BoxBody>,
|
|
||||||
Error = service::Error,
|
|
||||||
InitError = (),
|
|
||||||
>,
|
|
||||||
> {
|
|
||||||
Self::service_app()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configura el servicio web de la aplicación.
|
|
||||||
fn service_app() -> service::App<
|
|
||||||
impl service::Factory<
|
|
||||||
service::Request,
|
|
||||||
Config = (),
|
|
||||||
Response = service::Response<service::BoxBody>,
|
|
||||||
Error = service::Error,
|
|
||||||
InitError = (),
|
|
||||||
>,
|
|
||||||
> {
|
|
||||||
service::App::new()
|
|
||||||
.configure(extension::all::configure_services)
|
|
||||||
.default_service(service::web::route().to(service_not_found))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn service_not_found(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn route_not_found(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Err(ErrorPage::NotFound(request))
|
Err(ErrorPage::NotFound(request))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ impl Component for Intro {
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
cx.alter_assets(AssetsOp::AddStyleSheet(
|
cx.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/intro.css").with_version(PAGETOP_VERSION),
|
StyleSheet::from("/pagetop/css/intro.css").with_version(PAGETOP_VERSION),
|
||||||
));
|
));
|
||||||
if *self.opening() == IntroOpening::PageTop {
|
if *self.opening() == IntroOpening::PageTop {
|
||||||
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
cx.alter_assets(AssetsOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ use crate::prelude::*;
|
||||||
|
|
||||||
/// Página de bienvenida de PageTop.
|
/// Página de bienvenida de PageTop.
|
||||||
///
|
///
|
||||||
/// Esta extensión se instala por defecto si el ajuste de configuración [`global::App::welcome`] es
|
/// Se registra automáticamente cuando la aplicación arranca sin extensión raíz. Muestra una página
|
||||||
/// `true`. Muestra una página de bienvenida de PageTop en la ruta raíz (`/`).
|
/// 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
|
/// También puede incluirse explícitamente como dependencia de la extensión raíz o de cualquier otra
|
||||||
/// rutas.
|
/// 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.
|
/// Resulta útil en demos o para comprobar rápidamente que el servidor ha arrancado correctamente.
|
||||||
pub struct Welcome;
|
pub struct Welcome;
|
||||||
|
|
@ -20,12 +20,12 @@ impl Extension for Welcome {
|
||||||
L10n::l("welcome_extension_description")
|
L10n::l("welcome_extension_description")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
scfg.route("/", service::web::get().to(home));
|
router.route("/", web::get(home))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn home(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn home(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
let app = &global::SETTINGS.app.name;
|
let app = &global::SETTINGS.app.name;
|
||||||
|
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ impl Extension for Basic {
|
||||||
impl Theme for Basic {
|
impl Theme for Basic {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/normalize.css")
|
StyleSheet::from("/pagetop/css/normalize.css")
|
||||||
.with_version("8.0.1")
|
.with_version("8.0.1")
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_assets(AssetsOp::AddStyleSheet(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/css/basic.css")
|
StyleSheet::from("/pagetop/css/basic.css")
|
||||||
.with_version(PAGETOP_VERSION)
|
.with_version(PAGETOP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@
|
||||||
//! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada
|
//! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada
|
||||||
//! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente.
|
//! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente.
|
||||||
//!
|
//!
|
||||||
//!
|
|
||||||
//! # Orden de carga
|
//! # Orden de carga
|
||||||
//!
|
//!
|
||||||
//! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del
|
//! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del
|
||||||
//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación.
|
//! 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:
|
//! 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
|
//! Los archivos se combinan en el orden anterior, cada archivo sobrescribe a los anteriores en caso
|
||||||
//! de conflicto.
|
//! de conflicto.
|
||||||
//!
|
//!
|
||||||
//!
|
|
||||||
//! # Cómo añadir opciones de configuración a tu código
|
//! # Cómo añadir opciones de configuración a tu código
|
||||||
//!
|
//!
|
||||||
//! Añade [*serde*](https://docs.rs/serde) en tu archivo *Cargo.toml* con la *feature* `derive`:
|
//! Añade [*serde*](https://docs.rs/serde) en tu archivo *Cargo.toml* con la *feature* `derive`:
|
||||||
|
|
@ -91,7 +90,6 @@
|
||||||
//!
|
//!
|
||||||
//! Las estructuras de configuración son de **sólo lectura** durante la ejecución.
|
//! Las estructuras de configuración son de **sólo lectura** durante la ejecución.
|
||||||
//!
|
//!
|
||||||
//!
|
|
||||||
//! # Usando tus opciones de configuración
|
//! # Usando tus opciones de configuración
|
||||||
//!
|
//!
|
||||||
//! ```rust,ignore
|
//! ```rust,ignore
|
||||||
|
|
@ -131,9 +129,14 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
||||||
let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into());
|
let dir = env::var_os("CONFIG_DIR").unwrap_or_else(|| DEFAULT_CONFIG_DIR.into());
|
||||||
let config_dir = util::resolve_absolute_dir(&dir).unwrap_or_else(|_| PathBuf::from(&dir));
|
let config_dir = util::resolve_absolute_dir(&dir).unwrap_or_else(|_| PathBuf::from(&dir));
|
||||||
|
|
||||||
// Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Si no está definida, se usa
|
// Modo de ejecución. Con la *feature* `testing` activa (usada por `cargo ts` y `cargo tw`), se
|
||||||
// por defecto DEFAULT_RUN_MODE (p. ej. PAGETOP_RUN_MODE=production).
|
// fija en "test" en tiempo de compilación, sin manipular el entorno. En caso contrario se lee
|
||||||
let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into());
|
// 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()
|
Config::builder()
|
||||||
// 1. Configuración común para todos los entornos (common.toml).
|
// 1. Configuración común para todos los entornos (common.toml).
|
||||||
|
|
@ -158,7 +161,7 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
||||||
/// Hay que añadir en nuestra librería el siguiente código:
|
/// Hay que añadir en nuestra librería el siguiente código:
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// include_config!(SETTINGS: Settings => [
|
/// include_config!(SETTINGS_NAME: SettingsType => [
|
||||||
/// "ruta.clave" => valor,
|
/// "ruta.clave" => valor,
|
||||||
/// // ...
|
/// // ...
|
||||||
/// ]);
|
/// ]);
|
||||||
|
|
@ -168,8 +171,8 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
||||||
///
|
///
|
||||||
/// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los
|
/// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los
|
||||||
/// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio.
|
/// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio.
|
||||||
/// * **`Settings_Type`** es la referencia a la estructura que define los tipos para deserializar la
|
/// * **`SettingsType`** es la estructura que define los tipos para deserializar la configuración.
|
||||||
/// configuración. Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`).
|
/// Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`).
|
||||||
/// * **Lista de pares** con las claves TOML que requieran valores por defecto. Siguen la notación
|
/// * **Lista de pares** con las claves TOML que requieran valores por defecto. Siguen la notación
|
||||||
/// `"seccion.subclave"` para coincidir con el árbol TOML.
|
/// `"seccion.subclave"` para coincidir con el árbol TOML.
|
||||||
///
|
///
|
||||||
|
|
@ -211,7 +214,7 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
||||||
/// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p. ej. `[blog]`) para
|
/// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p. ej. `[blog]`) para
|
||||||
/// evitar colisiones con otras librerías.
|
/// evitar colisiones con otras librerías.
|
||||||
///
|
///
|
||||||
/// * **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
|
/// configurar distintos entornos (*dev*, *staging*, *prod*) usa los archivos TOML descritos en la
|
||||||
/// documentación de [`config`](crate::config).
|
/// documentación de [`config`](crate::config).
|
||||||
///
|
///
|
||||||
|
|
@ -220,8 +223,8 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
||||||
///
|
///
|
||||||
/// # Requisitos
|
/// # Requisitos
|
||||||
///
|
///
|
||||||
/// * Dependencia `serde` con la *feature* `derive`.
|
/// * Las claves deben coincidir con los campos (*snake case*) de la estructura de ajustes.
|
||||||
/// * Las claves deben coincidir con los campos (*snake case*) de tu estructura `Settings_Type`.
|
/// * Añade `serde` con la *feature* `derive` en *Cargo.toml*:
|
||||||
///
|
///
|
||||||
/// ```toml
|
/// ```toml
|
||||||
/// [dependencies]
|
/// [dependencies]
|
||||||
|
|
@ -229,10 +232,10 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! include_config {
|
macro_rules! include_config {
|
||||||
( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => {
|
( $SETTINGS_NAME:ident : $settings_type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => {
|
||||||
#[doc = concat!(
|
#[doc = concat!(
|
||||||
"Ajustes de configuración y **valores por defecto** para ",
|
"Carga [`", stringify!($settings_type), "`] ",
|
||||||
"[`", stringify!($Settings_Type), "`]."
|
"(y aplica **valores por defecto** en claves no definidas)."
|
||||||
)]
|
)]
|
||||||
#[doc = ""]
|
#[doc = ""]
|
||||||
#[doc = "Valores predeterminados que se aplican en ausencia de configuración:"]
|
#[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 = concat!($k, " = ", stringify!($v))]
|
||||||
)*
|
)*
|
||||||
#[doc = "```"]
|
#[doc = "```"]
|
||||||
pub static $SETTINGS_NAME: std::sync::LazyLock<$Settings_Type> =
|
pub static $SETTINGS_NAME: std::sync::LazyLock<$settings_type> =
|
||||||
std::sync::LazyLock::new(|| {
|
std::sync::LazyLock::new(|| {
|
||||||
let mut settings = $crate::config::CONFIG_VALUES.clone();
|
let mut settings = $crate::config::CONFIG_VALUES.clone();
|
||||||
$(
|
$(
|
||||||
settings = settings.set_default($k, $v).unwrap();
|
settings = settings.set_default($k, $v)
|
||||||
|
.expect(concat!("Failed to set default for key ", $k));
|
||||||
)*
|
)*
|
||||||
settings
|
settings
|
||||||
.build()
|
.build()
|
||||||
.expect(concat!("Failed to build config for ", stringify!($Settings_Type)))
|
.expect(concat!("Failed to build config for ", stringify!($settings_type)))
|
||||||
.try_deserialize::<$Settings_Type>()
|
.try_deserialize::<$settings_type>()
|
||||||
.expect(concat!("Error parsing settings for ", stringify!($Settings_Type)))
|
.expect(concat!("Error parsing settings for ", stringify!($settings_type)))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue