Compare commits

..

12 commits

Author SHA1 Message Date
026448e511 ♻️ (seaorm): Separa módulo migration de db
- `db::*` queda como API de consultas (connection, fetch_*).
- `migration::*` sube a primer nivel con su propia documentación.
- `DBCONN` y `run_now` se trasladan a la raíz de la extensión.
- Actualiza README.md y docs para reflejar la nueva estructura.
2026-05-15 00:22:55 +02:00
796ae5ce81 ♻️ (seaorm): Revisa y mejora la API pública 2026-05-11 15:10:49 +02:00
aa931ea052 ⬆️ (seaorm): Actualiza sea-orm a 1.1 2026-05-10 21:42:19 +02:00
8c861bff05 📝 (seaorm): Corrige ejemplos de documentación 2026-05-10 00:43:35 +02:00
fa5489dbb0 ♻️ (seaorm): Elimina prelude para usar db 2026-05-10 00:38:00 +02:00
a0805ed0fb ♻️ (bootsier): Elimina prelude para usar theme 2026-05-10 00:31:33 +02:00
bd8a34341d 🚧 Retoques en documentación y código 2026-05-09 13:33:20 +02:00
23d4fd8a80 (seaorm): Añade acceso a bases de datos 2026-05-09 13:07:49 +02:00
b4284f74f8 🌐 (bootsier): Localiza nombre y descripción 2026-05-09 10:43:04 +02:00
50abfe3b56 🌐 (aliner): Localiza nombre y descripción 2026-05-09 10:42:48 +02:00
35883bdcde Añade alias cargo td y aclara doc de pruebas 2026-05-09 09:35:59 +02:00
9e625c2b46 📝 Retoques en README's y documentación 2026-05-09 08:18:28 +02:00
72 changed files with 4495 additions and 422 deletions

View file

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

View file

@ -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).

1628
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -69,6 +69,7 @@ members = [
# Extensions # Extensions
"extensions/pagetop-aliner", "extensions/pagetop-aliner",
"extensions/pagetop-bootsier", "extensions/pagetop-bootsier",
"extensions/pagetop-seaorm",
] ]
[workspace.package] [workspace.package]
@ -88,5 +89,6 @@ 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 = "." }

View file

@ -11,9 +11,10 @@
[![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop) [![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop#licencia) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](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
@ -109,6 +110,9 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y 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
@ -116,15 +120,19 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
`.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

View file

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

View file

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

View file

@ -9,7 +9,6 @@
[![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner) [![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
<br>
</div> </div>
## 🧭 Sobre PageTop ## 🧭 Sobre PageTop

View file

@ -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,6 +97,14 @@ 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)
} }

View file

@ -0,0 +1,2 @@
extension_name = Aliner
extension_description = Minimal theme that schematically shows the HTML page composition.

View 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.

View file

@ -9,7 +9,6 @@
[![Descargas](https://img.shields.io/crates/d/pagetop-bootsier.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier) [![Descargas](https://img.shields.io/crates/d/pagetop-bootsier.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
<br>
</div> </div>
## 🧭 Sobre PageTop ## 🧭 Sobre PageTop
@ -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

View file

@ -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,6 +128,14 @@ 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)
} }

View file

@ -0,0 +1,2 @@
extension_name = Bootsier
extension_description = Bootstrap-based theme with flexible styles and components.

View file

@ -0,0 +1,2 @@
extension_name = Bootsier
extension_description = Tema basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.

View file

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

View file

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

View file

@ -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(), "");

View file

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

View file

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

View file

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

View file

@ -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));
/// ///

View file

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

View file

@ -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");
/// ``` /// ```

View file

@ -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");
/// ///

View file

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

View file

@ -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};

View file

@ -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)]

View file

@ -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};

View file

@ -1,14 +1,14 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::prelude::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
/// Componente para crear un **menú desplegable**. /// Componente para crear un **menú desplegable** ([`dropdown`]).
/// ///
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split)) /// 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)]

View file

@ -1,6 +1,6 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::prelude::*; use crate::theme::*;
// **< AutoClose >********************************************************************************** // **< AutoClose >**********************************************************************************

View file

@ -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};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)]

View file

@ -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};

View file

@ -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)]

View file

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

View file

@ -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};

View file

@ -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`].
/// ///

View file

@ -1,19 +1,139 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::prelude::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
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)]

View file

@ -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).
/// ///

View file

@ -1,6 +1,6 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::prelude::*; use crate::theme::*;
// **< Layout >************************************************************************************* // **< Layout >*************************************************************************************

View file

@ -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};

View file

@ -1,9 +1,9 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::prelude::*; use crate::theme::*;
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
/// Componente para crear un **panel lateral deslizante** con contenidos adicionales. /// Componente para crear un **panel lateral deslizante** ([`offcanvas`]).
/// ///
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes /// Ú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)]

View file

@ -0,0 +1,36 @@
[package]
name = "pagetop-seaorm"
version = "0.0.4"
edition = "2021"
description = """
Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales.
"""
categories = ["database", "development-tools", "asynchronous"]
keywords = ["pagetop", "database", "sql", "orm", "ssr"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
authors.workspace = true
[features]
mysql = ["sea-orm/sqlx-mysql"]
postgres = ["sea-orm/sqlx-postgres"]
sqlite = ["sea-orm/sqlx-sqlite"]
[dependencies]
pagetop.workspace = true
serde.workspace = true
async-trait = "0.1"
futures = "0.3"
url = "2.5"
[dependencies.sea-orm]
version = "1.1"
features = ["debug-print", "macros", "runtime-async-std-native-tls"]
default-features = false
[dependencies.sea-schema]
version = "0.16"

View 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.

View 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.

View file

@ -0,0 +1,150 @@
<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>
[![Doc API](https://img.shields.io/docsrs/pagetop-seaorm?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-seaorm)
[![Crates.io](https://img.shields.io/crates/v/pagetop-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm)
[![Descargas](https://img.shields.io/crates/d/pagetop-seaorm.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia)
</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::*;
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 SeaORM:
```rust,no_run
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))
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
}
```
## 📚 Créditos
Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/SeaQL) como:
* [SeaORM](https://www.sea-ql.org/SeaORM), ORM asíncrono que usa internamente
[SQLx](https://github.com/launchbadge/sqlx) para el acceso y la ejecución de consultas a la base
de datos.
* [SeaQuery](https://github.com/SeaQL/sea-query), generador de consultas SQL sobre el que se
construye el motor de migraciones y los *helpers* de esquema.
* [sea-schema](https://github.com/SeaQL/sea-schema), librería de introspección de esquemas SQL,
usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas,
columnas, índices y claves externas).
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, original 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.

View file

@ -0,0 +1,64 @@
//! Opciones de configuración de la extensión.
//!
//! Ejemplo:
//!
//! ```toml
//! [database]
//! db_type = "mysql"
//! db_name = "db"
//! db_user = "user"
//! db_pass = "password"
//! db_host = "localhost"
//! db_port = 3306
//! max_pool_size = 5
//! ```
//!
//! Uso:
//!
//! ```rust
//! # use pagetop_seaorm::config;
//! assert_eq!(config::SETTINGS.database.db_host, "localhost");
//! ```
//!
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
//! los valores a los ajustes.
use pagetop::prelude::*;
use serde::Deserialize;
include_config!(SETTINGS: Settings => [
// [database]
"database.db_type" => "",
"database.db_name" => "",
"database.db_user" => "",
"database.db_pass" => "",
"database.db_host" => "localhost",
"database.max_pool_size" => 5,
]);
#[derive(Debug, Deserialize)]
/// Tipos para la sección [`[database]`](Database) de [`SETTINGS`].
pub struct Settings {
pub database: Database,
}
#[derive(Debug, Deserialize)]
/// Sección `[database]` de la configuración. Forma parte de [`Settings`].
pub struct Database {
/// Tipo de base de datos: *"mysql"*, *"postgres"* ó *"sqlite"*.
pub db_type: String,
/// 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 es `None` 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,
}

View file

@ -0,0 +1,140 @@
//! API completa de SeaORM para operaciones con la base de datos.
//!
//! Re-exporta el *prelude* de SeaORM (entidades, traits, tipos de valor, macros de derivación…)
//! y expone tres funciones de consulta propias. Con una sola importación tienes todo lo necesario
//! para definir entidades y realizar operaciones CRUD:
//!
//! ```rust,ignore
//! use pagetop_seaorm::db::*;
//! ```
//!
//! Para definir el esquema de la base de datos o escribir migraciones usa además
//! [`crate::migration`].
pub use sea_orm::prelude::*;
use sea_orm::sea_query::{
MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder,
};
use sea_orm::{DatabaseBackend, ExecResult, Statement};
/// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades.
///
/// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades
/// de SeaORM. El coste de esta llamada es prácticamente nulo: sólo devuelve una referencia a un
/// valor inicializado una sola vez al arrancar la aplicación.
///
/// ```rust,no_run
/// use pagetop_seaorm::db::*;
///
/// // Consultas tipadas con el sistema de entidades de SeaORM:
/// // let users = User::find().all(connection()).await?;
/// // let user = User::find_by_id(1).one(connection()).await?;
/// // User::insert(model).exec(connection()).await?;
/// let _conn = connection();
/// ```
pub fn connection() -> &'static DatabaseConnection {
&super::DBCONN
}
/// Ejecuta una consulta para devolver todas las filas resultantes.
///
/// Acepta cualquier tipo que implemente [`crate::migration::QueryStatementWriter`] (p. ej. [`crate::migration::SelectStatement`]) y
/// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada
/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con
/// [`QueryResult::try_get`].
///
/// ```rust,no_run
/// use pagetop_seaorm::db::*;
/// use pagetop_seaorm::migration::*;
///
/// async fn example() -> Result<(), DbErr> {
/// let mut stmt = Query::select()
/// .column(Asterisk)
/// .from(Alias::new("users"))
/// .to_owned();
/// let rows = fetch_all(&mut stmt).await?;
/// for row in rows {
/// let name: String = row.try_get("", "name")?;
/// println!("{name}");
/// }
/// Ok(())
/// }
/// ```
pub async fn fetch_all<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, DbErr> {
let dbconn = &*super::DBCONN;
let dbbackend = dbconn.get_database_backend();
dbconn
.query_all(Statement::from_string(
dbbackend,
match dbbackend {
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
},
))
.await
}
/// Ejecuta una consulta y devuelve sólo la primera fila, si existe.
///
/// Funciona igual que [`fetch_all`] pero detiene la ejecución tras la primera fila y devuelve
/// `None` si la consulta no produce resultados.
///
/// ```rust,no_run
/// use pagetop_seaorm::db::*;
/// use pagetop_seaorm::migration::*;
///
/// async fn example() -> Result<(), DbErr> {
/// let mut stmt = Query::select()
/// .column(Asterisk)
/// .from(Alias::new("users"))
/// .and_where(Expr::col(Alias::new("id")).eq(1))
/// .to_owned();
/// if let Some(row) = fetch_one(&mut stmt).await? {
/// let name: String = row.try_get("", "name")?;
/// println!("{name}");
/// }
/// Ok(())
/// }
/// ```
pub async fn fetch_one<Q: QueryStatementWriter>(
stmt: &mut Q,
) -> Result<Option<QueryResult>, DbErr> {
let dbconn = &*super::DBCONN;
let dbbackend = dbconn.get_database_backend();
dbconn
.query_one(Statement::from_string(
dbbackend,
match dbbackend {
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
},
))
.await
}
/// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de
/// la operación.
///
/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe
/// como cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El
/// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado.
///
/// ```rust,no_run
/// 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 dbconn = &*super::DBCONN;
let dbbackend = dbconn.get_database_backend();
dbconn
.execute(Statement::from_string(dbbackend, stmt.into()))
.await
}

View file

@ -0,0 +1,190 @@
/*!
<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>
[![Doc API](https://img.shields.io/docsrs/pagetop-seaorm?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-seaorm)
[![Crates.io](https://img.shields.io/crates/v/pagetop-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm)
[![Descargas](https://img.shields.io/crates/d/pagetop-seaorm.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia)
</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::*;
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 SeaORM:
```rust,no_run
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))
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
}
```
*/
#![doc(
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
)]
use pagetop::prelude::*;
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use url::Url;
use std::sync::LazyLock;
include_locales!(LOCALES_SEAORM);
pub mod config;
pub mod db;
pub mod migration;
pub(crate) use futures::executor::block_on as run_now;
pub(crate) static DBCONN: LazyLock<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 = match config::SETTINGS.database.db_type.as_str() {
"mysql" | "postgres" => {
let mut tmp_uri = Url::parse(
format!(
"{}://{}/{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap();
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.unwrap();
// https://github.com/launchbadge/sqlx/issues/1624
tmp_uri
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.unwrap();
if let Some(port) = config::SETTINGS.database.db_port {
tmp_uri.set_port(Some(port)).unwrap();
}
tmp_uri
}
"sqlite" => Url::parse(
format!(
"{}://{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap(),
_ => panic!(
"Unrecognized database type \"{}\"",
config::SETTINGS.database.db_type
),
};
run_now(Database::connect::<ConnectOptions>({
let mut db_opt = ConnectOptions::new(db_uri.to_string());
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
db_opt
}))
.unwrap_or_else(|_| panic!("Failed to connect to database"))
});
/// Implementa la extensión.
pub struct SeaORM;
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);
}
}

View file

@ -0,0 +1,2 @@
extension_name = SeaORM support
extension_description = Provides SeaORM-based access to relational databases.

View file

@ -0,0 +1,2 @@
extension_name = Soporte a SeaORM
extension_description = Proporciona acceso basado en SeaORM a bases de datos relacionales.

View file

@ -0,0 +1,156 @@
//! API para definir y ejecutar migraciones de base de datos.
//!
//! Re-exporta los tipos de SeaORM necesarios para escribir migraciones y ofrece las macros
//! [`crate::install_migrations`] y [`crate::uninstall_migrations`] para aplicarlas o revertirlas al
//! arrancar la extensión.
//!
//! ```rust,ignore
//! use pagetop_seaorm::db::*;
//! use pagetop_seaorm::migration::*;
//! ```
// **< Adaptación de `sea-orm-migration` (ver §Créditos en README.md) >*****************************
//pub mod cli;
pub mod connection;
pub mod manager;
pub mod migrator;
//pub mod prelude;
pub mod schema;
pub mod seaql_migrations;
//pub mod util;
pub use connection::*;
pub use manager::*;
//pub use migrator::*;
pub use async_trait;
//pub use sea_orm;
//pub use sea_orm::sea_query;
pub use sea_orm::DbErr;
pub trait MigrationName {
fn name(&self) -> &str;
}
/// The migration definition
#[async_trait::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
/// Define actions to perform when rolling back the migration
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Err(DbErr::Migration(
"Rollback not implemented for this migration".to_owned(),
))
}
}
// *************************************************************************************************
pub use migrator::MigratorTrait;
pub use schema::*;
pub use sea_orm::sea_query::*;
pub use sea_orm::DeriveIden;
use pagetop::core::TypeInfo;
use pagetop::trace;
impl<M: MigrationTrait> MigrationName for M {
fn name(&self) -> &str {
TypeInfo::NameTo(-2).of::<M>()
}
}
pub type MigrationItem = Box<dyn MigrationTrait>;
pub trait MigratorBase {
fn run_up();
fn run_down();
}
#[rustfmt::skip]
impl<M: MigratorTrait> MigratorBase for M {
fn run_up() {
if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
trace::error!("Migration upgrade failed ({})", e);
};
}
fn run_down() {
if let Err(e) = super::run_now(Self::down(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
trace::error!("Migration downgrade failed ({})", e);
};
}
}
/// Aplica las migraciones pendientes al arrancar una extensión.
///
/// Recibe uno o más módulos de migración y ejecuta el método `up` de los que aún no estén
/// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde
/// [`Extension::initialize`](pagetop::core::extension::Extension::initialize).
///
/// ```rust,ignore
/// impl Extension for MyExt {
/// fn initialize(&self) {
/// install_migrations!(
/// m20240101_000001_create_users_table,
/// m20240115_000002_add_email_index,
/// );
/// }
/// }
/// ```
#[macro_export]
macro_rules! install_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait};
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<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 alguna no implementa `down`,
/// detiene el proceso con un error. Complementario a [`crate::install_migrations`].
///
/// ```rust,ignore
/// impl Extension for MyExt {
/// fn uninitialize(&self) {
/// uninstall_migrations!(
/// m20240101_000001_create_users_table,
/// m20240115_000002_add_email_index,
/// );
/// }
/// }
/// ```
#[macro_export]
macro_rules! uninstall_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait};
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut m = Vec::<MigrationItem>::new();
$(
m.push(Box::new(migration::$migration_module::Migration));
)*
m
}
}
Migrator::run_down();
}};
}

View file

@ -0,0 +1,148 @@
use futures::Future;
use sea_orm::{
AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr,
ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait,
};
use std::pin::Pin;
pub enum SchemaManagerConnection<'c> {
Connection(&'c DatabaseConnection),
Transaction(&'c DatabaseTransaction),
}
#[async_trait::async_trait]
impl 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)
}
}

View file

@ -0,0 +1,186 @@
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
use sea_orm::sea_query::{
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement,
TableRenameStatement, TableTruncateStatement,
};
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")
}

View file

@ -0,0 +1,617 @@
use futures::Future;
use std::collections::HashSet;
use std::fmt::Display;
use std::pin::Pin;
use std::time::SystemTime;
use pagetop::trace::info;
use sea_orm::sea_query::{
self, extension::postgres::Type, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query,
SelectStatement, SimpleExpr, Table,
};
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::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Status of migration
pub enum MigrationStatus {
/// Not yet applied
Pending,
/// Applied
Applied,
}
impl Display for MigrationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status = match self {
MigrationStatus::Pending => "Pending",
MigrationStatus::Applied => "Applied",
};
write!(f, "{status}")
}
}
pub struct Migration {
migration: Box<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
}
}

View file

@ -0,0 +1,611 @@
//! Adaptación de <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
//!
//! # Ayudantes de esquema de base de datos
//!
//! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`.
//!
//! # Ejemplo
//!
//! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes
//! de esquema.
//!
//! ```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()
}
/// Añade las columnas de timestamp (`CreatedAt` y `UpdatedAt`) a una tabla existente.
pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
let mut t = t;
t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp()))
.col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp()))
.take()
}
/// Crea un alias.
pub fn name<T: Into<String>>(name: T) -> Alias {
Alias::new(name)
}

View 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 {}

View file

@ -65,6 +65,7 @@ case "$CRATE" in
# Extensions # Extensions
--exclude-path "extensions/pagetop-aliner/**/*" --exclude-path "extensions/pagetop-aliner/**/*"
--exclude-path "extensions/pagetop-bootsier/**/*" --exclude-path "extensions/pagetop-bootsier/**/*"
--exclude-path "extensions/pagetop-seaorm/**/*"
) )
;; ;;
pagetop-aliner) pagetop-aliner)
@ -75,6 +76,10 @@ case "$CRATE" in
CHANGELOG_FILE="extensions/pagetop-bootsier/CHANGELOG.md" CHANGELOG_FILE="extensions/pagetop-bootsier/CHANGELOG.md"
PATH_FLAGS=(--include-path "extensions/pagetop-bootsier/**/*") PATH_FLAGS=(--include-path "extensions/pagetop-bootsier/**/*")
;; ;;
pagetop-seaorm)
CHANGELOG_FILE="extensions/pagetop-seaorm/CHANGELOG.md"
PATH_FLAGS=(--include-path "extensions/pagetop-seaorm/**/*")
;;
*) *)
echo "Error: unsupported crate '$CRATE'" >&2 echo "Error: unsupported crate '$CRATE'" >&2
exit 1 exit 1
@ -120,7 +125,9 @@ read -r -p "Do you want to proceed with the release of $CRATE? [y/N] " REPLY
echo "" echo ""
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
echo "Aborting release process." >&2 echo "Aborting release process." >&2
if [[ -n "${PAGETOP_RESTORE_TREE:-}" ]]; then
git restore --worktree -- . git restore --worktree -- .
fi
exit 1 exit 1
fi fi

View file

@ -43,6 +43,8 @@ cd "$(dirname "$0")/.." || exit 1
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# DRY-RUN (por defecto) o ejecución real con --execute # DRY-RUN (por defecto) o ejecución real con --execute
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
export PAGETOP_RESTORE_TREE=1
if [[ "$EXECUTE" != "--execute" ]]; then if [[ "$EXECUTE" != "--execute" ]]; then
echo "Running dry-run (default mode). Add --execute to publish" echo "Running dry-run (default mode). Add --execute to publish"
cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL" cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL"