Compare commits

...

10 commits

107 changed files with 5511 additions and 846 deletions

View file

@ -1,58 +1,53 @@
# 🔃 Dependencies # 🔃 Dependencies
PageTop is developed in the [Rust programming language](https://www.rust-lang.org/) and stands on PageTop is developed using the [Rust programming language](https://www.rust-lang.org/) and stands on
the shoulders of true giants, using some of the most stable and renowned libraries (*crates*) from the shoulders of giants, leveraging some of the most stable and renowned libraries (*crates*) from
the [Rust ecosystem](https://lib.rs), such as: the [Rust ecosystem](https://lib.rs), including:
* [Actix Web](https://actix.rs/) for web services and server management.
* [Tracing](https://github.com/tokio-rs/tracing) for the diagnostic system and structured logging.
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates) that incorporate
[Fluent](https://projectfluent.org/) for project internationalization.
* Among others, which you can review in the PageTop
[`Cargo.toml`](https://github.com/manuelcillero/pagetop/blob/main/Cargo.toml) file.
* [Actix Web](https://actix.rs/) for web services and server management.
* [Tracing](https://github.com/tokio-rs/tracing) for diagnostics and structured logging.
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), which integrate
[Fluent](https://projectfluent.org/) for internationalization.
* Additional crates, which you can explore in the `Cargo.toml` files of PageTop and its packages.
# ⌨️ Code # ⌨️ Code
PageTop integrates code from various renowned crates to enhance functionality: PageTop incorporates code from several well-regarded crates to enhance its functionality:
* [**Config (v0.11.0)**](https://github.com/mehcode/config-rs/tree/0.11.0): Includes code from * **[Config (v0.11.0)](https://github.com/mehcode/config-rs/tree/0.11.0)**: Includes code from
[config-rs](https://crates.io/crates/config) by [Ryan Leckey](https://crates.io/users/mehcode), [config-rs](https://crates.io/crates/config) by [Ryan Leckey](https://crates.io/users/mehcode),
chosen for its advantages in reading configuration settings and delegating assignment to safe chosen for its advantages in reading configuration settings and delegating assignment to safe
types, tailored to the specific needs of each package, theme, or application. types, tailored to the specific needs of each package, theme, or application.
* [**Maud (v0.25.0)**](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud): An adapted version * **[Maud (v0.25.0)](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud)**: An adapted version
of the excellent [maud](https://crates.io/crates/maud) crate by of the excellent [maud](https://crates.io/crates/maud) crate by
[Chris Wong](https://crates.io/users/lambda-fairy) is incorporated to leverage its functionalities without requiring a reference to `maud` in the `Cargo.toml` files. [Chris Wong](https://crates.io/users/lambda-fairy) is integrated, enabling its functionalities
without requiring a direct dependency in the `Cargo.toml` files.
* **SmartDefault (v0.7.1)**: Embedded [SmartDefault](https://crates.io/crates/smart_default) by
[Jane Doe](https://crates.io/users/jane-doe) as `AutoDefault`to simplify the documentation of
Default implementations and also removes the need to explicitly list `smart_default` in the
`Cargo.toml` files.
* **SmartDefault (v0.7.1)**: The [SmartDefault](https://crates.io/crates/smart_default) crate by
[Jane Doe](https://crates.io/users/jane-doe) has been embedded as `AutoDefault`, simplifying
`Default` implementations and eliminating the need to explicitly reference `smart_default` in
the `Cargo.toml` files.
# 🗚 FIGfonts # 🗚 FIGfonts
PageTop uses the [figlet-rs](https://crates.io/crates/figlet-rs) package by *yuanbohan* to display a PageTop uses the [figlet-rs](https://crates.io/crates/figlet-rs) package by *yuanbohan* to display a
presentation banner in the terminal with the application's name using presentation banner in the terminal featuring the application's name in
[FIGlet](http://www.figlet.org) characters. The fonts included in `src/app` are: [FIGlet](http://www.figlet.org) characters. The fonts included in `pagetop/src/app` are:
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) by *Glenn Chappell* * [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) by *Glenn Chappell*
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) by *Glenn Chappell* (default) * [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) by *Glenn Chappell* (default)
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) by *Claude Martins* * [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) by *Claude Martins*
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) by *Ryan Youck* * [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) by *Ryan Youck*
# 📰 Templates # 📰 Templates
* The default welcome homepage design is based on the The default welcome homepage design is inspired by a tutorial for creating a unique
[Zinc](https://themewagon.com/themes/free-bootstrap-5-html5-business-website-template-zinc) [Neobrutalism](https://www.codewithfaraz.com/content/109/creating-a-unique-neobrutalism-portfolio-page-with-html-css-and-javascript)
template created by [inovatik](https://inovatik.com/) and distributed by portfolio page by [Faraz](https://www.codewithfaraz.com/).
[ThemeWagon](https://themewagon.com).
# 🎨 Icon # 🎨 Icon
"The creature" smiling is a fun creation by [Webalys](https://www.iconfinder.com/webalys). It can be "The Creature" smiling is a playful creation by [Webalys](https://www.iconfinder.com/webalys). It is
found in their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection available on part of their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection, available on
[ICONFINDER](https://www.iconfinder.com). [ICONFINDER](https://www.iconfinder.com).

1489
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,18 +5,16 @@ members = [
"helpers/pagetop-build", "helpers/pagetop-build",
"helpers/pagetop-macros", "helpers/pagetop-macros",
# PageTop
"pagetop",
# Packages # Packages
"packages/pagetop",
"packages/pagetop-aliner", "packages/pagetop-aliner",
"packages/pagetop-bootsier", "packages/pagetop-bootsier",
"packages/pagetop-seaorm",
# App # App
"packages/drust", "drust",
# Examples
# "examples/app-basic",
# "examples/hello-world",
# "examples/hello-name",
] ]
[workspace.package] [workspace.package]
@ -34,7 +32,9 @@ static-files = "0.2.4"
pagetop-build = { version = "0.0", path = "helpers/pagetop-build" } pagetop-build = { version = "0.0", path = "helpers/pagetop-build" }
pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" } pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" }
# PageTop
pagetop = { version = "0.0", path = "pagetop" }
# Packages # Packages
pagetop = { version = "0.0", path = "packages/pagetop" }
pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" } pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" }
pagetop-bootsier = { version = "0.0", path = "packages/pagetop-bootsier" } pagetop-bootsier = { version = "0.0", path = "packages/pagetop-bootsier" }

View file

@ -43,7 +43,7 @@ impl PackageTrait for HelloWorld {
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_component(Html::with(html! { h1 { "Hello World!" } })) .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
.render() .render()
} }

View file

@ -1,5 +1,6 @@
[app] [app]
name = "Samples" name = "Drust"
description = "A modern web Content Management System to share your world."
[log] [database]
tracing = "Debug" db_type = "mysql"

View file

@ -1,37 +0,0 @@
<div align="center">
<h1>PageTop Build</h1>
<p>Simplifies the process of embedding resources in PageTop app binaries.</p>
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](#-license)
[![API Docs](https://img.shields.io/docsrs/pagetop-build?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-build)
[![Crates.io](https://img.shields.io/crates/v/pagetop-build.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-build)
[![Downloads](https://img.shields.io/crates/d/pagetop-build.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-build)
</div>
# 📦 About PageTop
[PageTop](https://docs.rs/pagetop) is an opinionated web framework to build modular *Server-Side
Rendering* web solutions.
# 🚧 Warning
**PageTop** framework is currently in active development. The API is unstable and subject to
frequent changes. Production use is not recommended until version **0.1.0**.
# 📜 License
All code in this crate is dual-licensed under either:
* MIT License
([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
* Apache License, Version 2.0,
([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
at your option. This means you can select the license you prefer! This dual-licensing approach is
the de-facto standard in the Rust ecosystem.

View file

@ -1,5 +1,4 @@
//! **`StaticFilesBundle`** uses [static_files](https://docs.rs/static-files/latest/static_files/) //! Provide an easy way to embed static files or compiled SCSS files into your binary at compile
//! to provide an easy way to embed static files or compiled SCSS files into your binary at compile
//! time. //! time.
//! //!
//! ## Adding to your project //! ## Adding to your project
@ -24,9 +23,9 @@
//! use pagetop_build::StaticFilesBundle; //! use pagetop_build::StaticFilesBundle;
//! //!
//! fn main() -> std::io::Result<()> { //! fn main() -> std::io::Result<()> {
//! StaticFilesBundle::from_dir("./static", None) // Include all files. //! StaticFilesBundle::from_dir("./static", None)
//! .with_name("guides") // Name the generated module. //! .with_name("guides")
//! .build() // Build the bundle. //! .build()
//! } //! }
//! ``` //! ```
//! //!
@ -67,7 +66,7 @@
//! //!
//! ## Generated module //! ## Generated module
//! //!
//! `StaticFilesBundle` generates a file in the standard directory //! [`StaticFilesBundle`] generates a file in the standard directory
//! [OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html) where all //! [OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html) where all
//! intermediate and output artifacts are placed during compilation. For example, if you use //! intermediate and output artifacts are placed during compilation. For example, if you use
//! `with_name("guides")`, it generates a file named `guides.rs`: //! `with_name("guides")`, it generates a file named `guides.rs`:
@ -78,15 +77,15 @@
//! ```rust#ignore //! ```rust#ignore
//! use pagetop::prelude::*; //! use pagetop::prelude::*;
//! //!
//! static_files!(guides); //! include_files!(guides);
//! ``` //! ```
//! //!
//! Or, access the entire bundle as a static `HashMap`: //! Or, access the entire bundle as a global static `HashMap`:
//! //!
//! ```rust#ignore //! ```rust#ignore
//! use pagetop::prelude::*; //! use pagetop::prelude::*;
//! //!
//! static_files!(guides => BUNDLE_GUIDES); //! include_files!(guides => BUNDLE_GUIDES);
//! ``` //! ```
//! //!
//! You can build more than one resources file to compile with your project. //! You can build more than one resources file to compile with your project.
@ -98,6 +97,8 @@ use std::fs::{create_dir_all, remove_dir_all, File};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
/// Generates the resources to embed at compile time using
/// [static_files](https://docs.rs/static-files/latest/static_files/).
pub struct StaticFilesBundle { pub struct StaticFilesBundle {
resource_dir: ResourceDir, resource_dir: ResourceDir,
} }

View file

@ -3,8 +3,110 @@ mod smart_default;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error; use proc_macro_error::proc_macro_error;
use quote::quote; use quote::{quote, quote_spanned, ToTokens};
use syn::{parse_macro_input, DeriveInput}; use syn::{parse_macro_input, parse_str, DeriveInput, ItemFn};
/// Macro attribute to generate builder methods from `set_` methods.
///
/// This macro takes a method with the `set_` prefix and generates a corresponding method with the
/// `with_` prefix to use in the builder pattern.
///
/// # Panics
///
/// This function will panic if a parameter identifier is not found in the argument list.
///
/// # Examples
///
/// ```
/// #[fn_builder]
/// pub fn set_example(&mut self) -> &mut Self {
/// // implementation
/// }
/// ```
///
/// Will generate:
///
/// ```
/// pub fn with_example(mut self) -> Self {
/// self.set_example();
/// self
/// }
/// ```
#[proc_macro_attribute]
pub fn fn_builder(_: TokenStream, item: TokenStream) -> TokenStream {
let fn_set = parse_macro_input!(item as ItemFn);
let fn_set_name = fn_set.sig.ident.to_string();
if !fn_set_name.starts_with("set_") {
let expanded = quote_spanned! {
fn_set.sig.ident.span() =>
compile_error!("expected a \"pub fn set_...() -> &mut Self\" method");
};
return expanded.into();
}
let fn_with_name = fn_set_name.replace("set_", "with_");
let fn_with_generics = if fn_set.sig.generics.params.is_empty() {
fn_with_name.clone()
} else {
let g = &fn_set.sig.generics;
format!("{fn_with_name}{}", quote! { #g }.to_string())
};
let where_clause = fn_set
.sig
.generics
.where_clause
.as_ref()
.map_or(String::new(), |where_clause| {
format!("{} ", quote! { #where_clause }.to_string())
});
let args: Vec<String> = fn_set
.sig
.inputs
.iter()
.skip(1)
.map(|arg| arg.to_token_stream().to_string())
.collect();
let params: Vec<String> = args
.iter()
.map(|arg| {
arg.split_whitespace()
.next()
.unwrap()
.trim_end_matches(':')
.to_string()
})
.collect();
#[rustfmt::skip]
let fn_with = parse_str::<ItemFn>(format!(r#"
pub fn {fn_with_generics}(mut self, {}) -> Self {where_clause} {{
self.{fn_set_name}({});
self
}}
"#, args.join(", "), params.join(", ")
).as_str()).unwrap();
#[rustfmt::skip]
let fn_set_doc = format!(r##"
<p id="method.{fn_with_name}" style="margin-bottom: 12px;">Use
<code class="code-header">pub fn <span class="fn" href="#method.{fn_with_name}">{fn_with_name}</span>(self, ) -> Self</code>
for the <a href="#method.new">builder pattern</a>.
</p>
"##);
let expanded = quote! {
#[doc(hidden)]
#fn_with
#[inline]
#[doc = #fn_set_doc]
#fn_set
};
expanded.into()
}
#[proc_macro] #[proc_macro]
#[proc_macro_error] #[proc_macro_error]

View file

@ -1,6 +0,0 @@
[app]
name = "Drust"
description = "A modern web Content Management System to share your world."
[database]
db_type = "mysql"

View file

@ -19,7 +19,6 @@ pagetop.workspace = true
include_dir.workspace = true include_dir.workspace = true
static-files.workspace = true static-files.workspace = true
tera = "1.20.0" tera = "1.20.0"
[build-dependencies] [build-dependencies]

View file

@ -5,9 +5,9 @@ use tera::Tera;
use std::sync::LazyLock; use std::sync::LazyLock;
static_locales!(LOCALES_ALINER); include_locales!(LOCALES_ALINER);
static_files!(aliner); include_files!(aliner);
// ALINER THEME ************************************************************************************ // ALINER THEME ************************************************************************************
@ -50,7 +50,7 @@ impl PackageTrait for Aliner {
} }
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
static_files_service!(scfg, aliner => "/aliner"); include_files_service!(scfg, aliner => "/aliner");
} }
} }

View file

@ -1,8 +1,8 @@
use pagetop::prelude::*; use pagetop::prelude::*;
static_locales!(LOCALES_BOOTSIER); include_locales!(LOCALES_BOOTSIER);
//static_files!(bootsier); //include_files!(bootsier);
pub struct Bootsier; pub struct Bootsier;
@ -26,7 +26,7 @@ impl PackageTrait for Bootsier {
} }
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
static_files_service!(scfg, bootsier => "/bootsier"); include_files_service!(scfg, bootsier => "/bootsier");
} */ } */
} }

View file

@ -0,0 +1,35 @@
[package]
name = "pagetop-seaorm"
version = "0.0.1"
edition = "2021"
description = """\
Integrate SeaORM as the database framework for PageTop applications.\
"""
categories = ["web-programming", "database"]
keywords = ["pagetop", "database", "sql", "orm"]
homepage = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[dependencies]
pagetop.workspace = true
async-trait = "0.1.83"
futures = "0.3.31"
serde.workspace = true
static-files.workspace = true
url = "2.5.4"
[dependencies.sea-orm]
version = "1.1.1"
features = [
"debug-print", "macros", "runtime-async-std-native-tls",
"sqlx-mysql", "sqlx-postgres", "sqlx-sqlite",
]
default-features = false
[dependencies.sea-schema]
version = "0.16.0"

View file

@ -1,16 +1,22 @@
<div align="center"> <div align="center">
<h1>PageTop Macros</h1> <h1>PageTop SeaORM</h1>
<p>A collection of macros that boost PageTop development.</p> <p>Integrate SeaORM as the database framework for PageTop applications.</p>
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](#-license) [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](#-license)
[![API Docs](https://img.shields.io/docsrs/pagetop-macros?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-macros) [![API Docs](https://img.shields.io/docsrs/pagetop-seaorm?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-seaorm)
[![Crates.io](https://img.shields.io/crates/v/pagetop-macros.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-macros) [![Crates.io](https://img.shields.io/crates/v/pagetop-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm)
[![Downloads](https://img.shields.io/crates/d/pagetop-macros.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-macros) [![Downloads](https://img.shields.io/crates/d/pagetop-seaorm.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm)
</div> </div>
PageTop SeaORM employs [SQLx](https://crates.io/crates/sqlx) and
[SeaQuery](https://crates.io/crates/sea-query), complemented by a custom version of
[SeaORM Migration](https://github.com/SeaQL/sea-orm/tree/1.1.1/sea-orm-migration/src) (v1.1.1). The
modified SeaORM Migration ensures migrations are scoped per package, providing greater control and
reducing coupling between database components.
# 📦 About PageTop # 📦 About PageTop
[PageTop](https://docs.rs/pagetop) is an opinionated web framework to build modular *Server-Side [PageTop](https://docs.rs/pagetop) is an opinionated web framework to build modular *Server-Side
@ -23,20 +29,6 @@ Rendering* web solutions.
frequent changes. Production use is not recommended until version **0.1.0**. frequent changes. Production use is not recommended until version **0.1.0**.
# 🔖 Credits
This crate includes an adapted version of [maud-macros](https://crates.io/crates/maud_macros),
version [0.25.0](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud_macros), by
[Chris Wong](https://crates.io/users/lambda-fairy).
It also includes an adapted version of [SmartDefault](https://crates.io/crates/smart_default)
(version 0.7.1) by [Jane Doe](https://crates.io/users/jane-doe), renamed as `AutoDefault`, to
streamline the implementation of `Default` in **PageTop** projects.
Both adaptations eliminate the need to explicitly add `maud` or `smart_default` as dependencies in
`Cargo.toml` files.
# 📜 License # 📜 License
All code in this crate is dual-licensed under either: All code in this crate is dual-licensed under either:

View file

@ -0,0 +1,71 @@
//! Configuration settings for the SeaORM PageTop package.
//!
//! Example:
//!
//! ```toml
//! [database]
//! db_type = "mysql"
//! db_name = "db"
//! db_user = "user"
//! db_pass = "password"
//! db_host = "localhost"
//! db_port = 3306
//! max_pool_size = 5
//! ```
//!
//! Usage:
//!
//! ```rust
//! use pagetop_seaorm::config;
//!
//! assert_eq!(config::SETTINGS.database.db_host, "localhost");
//! ```
//! See [`pagetop::include_config`] to learn how **PageTop** read configuration files and use
//! settings.
use pagetop::prelude::*;
use serde::Deserialize;
include_config!(SETTINGS: Settings => [
// [database]
"database.db_type" => "",
"database.db_name" => "",
"database.db_user" => "",
"database.db_pass" => "",
"database.db_host" => "localhost",
"database.db_port" => 0,
"database.max_pool_size" => 5,
]);
#[derive(Debug, Deserialize)]
/// Represents configuration settings, specifically the [`[database]`](Database) section (used by
/// [`SETTINGS`]).
pub struct Settings {
pub database: Database,
}
#[derive(Debug, Deserialize)]
/// Represents the `[database]` section in the [`Settings`] type.
pub struct Database {
/// Type of database: *"mysql"*, *"postgres"*, or *"sqlite"*.
/// Default: *""*.
pub db_type: String,
/// Name (for MySQL/Postgres) or reference (for SQLite) of the database.
/// Default: *""*.
pub db_name: String,
/// Username for database connection (for MySQL/Postgres).
/// Default: *""*.
pub db_user: String,
/// Password for database connection (for MySQL/Postgres).
/// Default: *""*.
pub db_pass: String,
/// Hostname for database connection (for MySQL/Postgres).
/// Default: *"localhost"*.
pub db_host: String,
/// Port number for database connection, typically 3306 (MySQL) or 5432 (Postgres).
/// Default: *0*.
pub db_port: u16,
/// Maximum number of allowed connections.
/// Default: *5*.
pub max_pool_size: u32,
}

View file

@ -0,0 +1,132 @@
use pagetop::trace;
use pagetop::util::TypeInfo;
pub use url::Url as DbUri;
pub use sea_orm::error::{DbErr, RuntimeErr};
pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult};
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement};
mod dbconn;
pub(crate) use dbconn::{run_now, DBCONN};
// The migration module is a customized version of the sea_orm_migration module (v1.0.0)
// https://github.com/SeaQL/sea-orm/tree/1.0.0/sea-orm-migration to avoid errors caused by the
// package paradigm of PageTop. Files integrated from original:
//
// lib.rs => db/migration.rs . . . . . . . . . . . . . . (excluding some modules and exports)
// connection.rs => db/migration/connection.rs . . . . . . . . . . . . . . (full integration)
// manager.rs => db/migration/manager.rs . . . . . . . . . . . . . . . . . (full integration)
// migrator.rs => db/migration/migrator.rs . . . . . . . . . . . .(omitting error management)
// prelude.rs => db/migration/prelude.rs . . . . . . . . . . . . . . . . . . . (avoiding CLI)
// seaql_migrations.rs => db/migration/seaql_migrations.rs . . . . . . . . (full integration)
//
mod migration;
pub use migration::prelude::*;
pub use migration::schema::*;
pub async fn query<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, DbErr> {
let dbconn = &*DBCONN;
let dbbackend = dbconn.get_database_backend();
dbconn
.query_all(Statement::from_string(
dbbackend,
match dbbackend {
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
},
))
.await
}
pub async fn exec<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Option<QueryResult>, DbErr> {
let dbconn = &*DBCONN;
let dbbackend = dbconn.get_database_backend();
dbconn
.query_one(Statement::from_string(
dbbackend,
match dbbackend {
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
},
))
.await
}
pub async fn exec_raw(stmt: String) -> Result<ExecResult, DbErr> {
let dbconn = &*DBCONN;
let dbbackend = dbconn.get_database_backend();
dbconn
.execute(Statement::from_string(dbbackend, stmt))
.await
}
pub trait MigratorBase {
fn run_up();
fn run_down();
}
#[rustfmt::skip]
impl<M: MigratorTrait> MigratorBase for M {
fn run_up() {
if let Err(e) = run_now(Self::up(SchemaManagerConnection::Connection(&DBCONN), None)) {
trace::error!("Migration upgrade failed ({})", e);
};
}
fn run_down() {
if let Err(e) = run_now(Self::down(SchemaManagerConnection::Connection(&DBCONN), None)) {
trace::error!("Migration downgrade failed ({})", e);
};
}
}
impl<M: MigrationTrait> MigrationName for M {
fn name(&self) -> &str {
TypeInfo::NameTo(-2).of::<M>()
}
}
pub type MigrationItem = Box<dyn MigrationTrait>;
#[macro_export]
macro_rules! install_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::db::{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();
}};
}
#[macro_export]
macro_rules! uninstall_migrations {
( $($migration_module:ident),+ $(,)? ) => {{
use $crate::db::{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,69 @@
use pagetop::trace;
use crate::config;
use crate::db::{DbConn, DbUri};
use std::sync::LazyLock;
use sea_orm::{ConnectOptions, Database};
pub use futures::executor::block_on as run_now;
pub static DBCONN: LazyLock<DbConn> = LazyLock::new(|| {
trace::info!(
"Connecting to database \"{}\" using a pool of {} connections",
&config::SETTINGS.database.db_name,
&config::SETTINGS.database.max_pool_size
);
let db_uri = match config::SETTINGS.database.db_type.as_str() {
"mysql" | "postgres" => {
let mut tmp_uri = DbUri::parse(
format!(
"{}://{}/{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap();
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.unwrap();
// https://github.com/launchbadge/sqlx/issues/1624
tmp_uri
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.unwrap();
if config::SETTINGS.database.db_port != 0 {
tmp_uri
.set_port(Some(config::SETTINGS.database.db_port))
.unwrap();
}
tmp_uri
}
"sqlite" => DbUri::parse(
format!(
"{}://{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap(),
_ => {
trace::error!(
"Unrecognized database type \"{}\"",
&config::SETTINGS.database.db_type
);
DbUri::parse("").unwrap()
}
};
run_now(Database::connect::<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"))
});

View file

@ -0,0 +1,33 @@
//pub mod cli;
pub mod connection;
pub mod manager;
pub mod migrator;
pub mod prelude;
pub mod schema;
pub mod seaql_migrations;
//pub mod util;
pub use connection::*;
pub use manager::*;
//pub use migrator::*;
pub use async_trait;
//pub use sea_orm;
//pub use sea_orm::sea_query;
use sea_orm::DbErr;
pub trait MigrationName {
fn name(&self) -> &str;
}
/// The migration definition
#[async_trait::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
/// Define actions to perform when rolling back the migration
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Err(DbErr::Migration("We Don't Do That Here".to_owned()))
}
}

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<'c> ConnectionTrait for SchemaManagerConnection<'c> {
fn get_database_backend(&self) -> DbBackend {
match self {
SchemaManagerConnection::Connection(conn) => conn.get_database_backend(),
SchemaManagerConnection::Transaction(trans) => trans.get_database_backend(),
}
}
async fn execute(&self, stmt: Statement) -> Result<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<'c> TransactionTrait for SchemaManagerConnection<'c> {
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::error::Error + 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::error::Error + Send,
{
match self {
SchemaManagerConnection::Connection(conn) => {
conn.transaction_with_config(callback, isolation_level, access_mode)
.await
}
SchemaManagerConnection::Transaction(trans) => {
trans
.transaction_with_config(callback, isolation_level, access_mode)
.await
}
}
}
}
pub trait IntoSchemaManagerConnection<'c>: Send
where
Self: 'c,
{
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c>;
}
impl<'c> IntoSchemaManagerConnection<'c> for SchemaManagerConnection<'c> {
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
self
}
}
impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseConnection {
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
SchemaManagerConnection::Connection(self)
}
}
impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseTransaction {
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
SchemaManagerConnection::Transaction(self)
}
}

View file

@ -0,0 +1,167 @@
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
use sea_orm::sea_query::{
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement,
TableTruncateStatement,
};
use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder};
use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite};
/// Helper struct for writing migration scripts in migration file
pub struct SchemaManager<'c> {
conn: SchemaManagerConnection<'c>,
}
impl<'c> SchemaManager<'c> {
pub fn new<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<'c> SchemaManager<'c> {
pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
}
/// Schema Mutation
impl<'c> SchemaManager<'c> {
pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
}
/// Schema Inspection.
impl<'c> SchemaManager<'c> {
pub async fn has_table<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 = match self.conn.get_database_backend() {
DbBackend::MySql => MySql.has_column(table, column),
DbBackend::Postgres => Postgres.has_column(table, column),
DbBackend::Sqlite => Sqlite.has_column(table, column),
};
let builder = self.conn.get_database_backend();
let res = self
.conn
.query_one(builder.build(&stmt))
.await?
.ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?;
res.try_get("", "has_column")
}
pub async fn has_index<T, I>(&self, table: T, index: I) -> Result<bool, DbErr>
where
T: AsRef<str>,
I: AsRef<str>,
{
let stmt = match self.conn.get_database_backend() {
DbBackend::MySql => MySql.has_index(table, index),
DbBackend::Postgres => Postgres.has_index(table, index),
DbBackend::Sqlite => Sqlite.has_index(table, index),
};
let builder = self.conn.get_database_backend();
let res = self
.conn
.query_one(builder.build(&stmt))
.await?
.ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?;
res.try_get("", "has_index")
}
}
pub(crate) async fn has_table<C, T>(conn: &C, table: T) -> Result<bool, DbErr>
where
C: ConnectionTrait,
T: AsRef<str>,
{
let stmt = match conn.get_database_backend() {
DbBackend::MySql => MySql.has_table(table),
DbBackend::Postgres => Postgres.has_table(table),
DbBackend::Sqlite => Sqlite.has_table(table),
};
let builder = conn.get_database_backend();
let res = conn
.query_one(builder.build(&stmt))
.await?
.ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?;
res.try_get("", "has_table")
}

View file

@ -0,0 +1,593 @@
use futures::Future;
use std::collections::HashSet;
use std::fmt::Display;
use std::pin::Pin;
use std::time::SystemTime;
use pagetop::trace::info;
use sea_orm::sea_query::{
self, extension::postgres::Type, Alias, Expr, ForeignKey, IntoIden, JoinType, Order, Query,
SelectStatement, SimpleExpr, Table,
};
use sea_orm::{
ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden,
DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement,
TransactionTrait,
};
use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite};
use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Status of migration
pub enum MigrationStatus {
/// Not yet applied
Pending,
/// Applied
Applied,
}
impl Display for MigrationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status = match self {
MigrationStatus::Pending => "Pending",
MigrationStatus::Applied => "Applied",
};
write!(f, "{status}")
}
}
pub struct Migration {
migration: Box<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() {
DbBackend::MySql => MySql.query_tables(),
DbBackend::Postgres => Postgres.query_tables(),
DbBackend::Sqlite => Sqlite.query_tables(),
}
}
fn get_current_schema<C>(db: &C) -> SimpleExpr
where
C: ConnectionTrait,
{
match db.get_database_backend() {
DbBackend::MySql => MySql::get_current_schema(),
DbBackend::Postgres => Postgres::get_current_schema(),
DbBackend::Sqlite => unimplemented!(),
}
}
#[derive(DeriveIden)]
enum InformationSchema {
#[sea_orm(iden = "information_schema")]
Schema,
#[sea_orm(iden = "TABLE_NAME")]
TableName,
#[sea_orm(iden = "CONSTRAINT_NAME")]
ConstraintName,
TableConstraints,
TableSchema,
ConstraintType,
}
fn query_mysql_foreign_keys<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(Expr::expr(get_current_schema(db)).equals((
InformationSchema::TableConstraints,
InformationSchema::TableSchema,
)))
.add(
Expr::col((
InformationSchema::TableConstraints,
InformationSchema::ConstraintType,
))
.eq("FOREIGN KEY"),
),
);
stmt
}
#[derive(DeriveIden)]
enum PgType {
Table,
Typname,
Typnamespace,
Typelem,
}
#[derive(DeriveIden)]
enum PgNamespace {
Table,
Oid,
Nspname,
}
fn query_pg_types<C>(db: &C) -> SelectStatement
where
C: ConnectionTrait,
{
let mut stmt = Query::select();
stmt.column(PgType::Typname)
.from(PgType::Table)
.join(
JoinType::LeftJoin,
PgNamespace::Table,
Expr::col((PgNamespace::Table, PgNamespace::Oid))
.equals((PgType::Table, PgType::Typnamespace)),
)
.cond_where(
Condition::all()
.add(
Expr::expr(get_current_schema(db))
.equals((PgNamespace::Table, PgNamespace::Nspname)),
)
.add(Expr::col((PgType::Table, PgType::Typelem)).eq(0)),
);
stmt
}
trait QueryTable {
type Statement;
fn table_name(self, table_name: DynIden) -> Self::Statement;
}
impl QueryTable for SelectStatement {
type Statement = SelectStatement;
fn table_name(mut self, table_name: DynIden) -> SelectStatement {
self.from(table_name);
self
}
}
impl QueryTable for sea_query::TableCreateStatement {
type Statement = sea_query::TableCreateStatement;
fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement {
self.table(table_name);
self
}
}
impl<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,13 @@
//pub use super::cli;
pub use super::connection::IntoSchemaManagerConnection;
pub use super::connection::SchemaManagerConnection;
pub use super::manager::SchemaManager;
pub use super::migrator::MigratorTrait;
pub use super::{MigrationName, MigrationTrait};
pub use async_trait;
pub use sea_orm;
pub use sea_orm::sea_query;
pub use sea_orm::sea_query::*;
pub use sea_orm::DeriveIden;
pub use sea_orm::DeriveMigrationName;

View file

@ -0,0 +1,613 @@
//! > Adapted from <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
//!
//! # Database Table Schema Helpers
//!
//! This module defines functions and helpers for creating database table
//! schemas using the `sea-orm` and `sea-query` libraries.
//!
//! # Example
//!
//! The following example shows how the user migration file should be and using
//! the schema helpers to create the Db fields.
//!
//! ```rust
//! use sea_orm_migration::{prelude::*, schema::*};
//!
//! #[derive(DeriveMigrationName)]
//! pub struct Migration;
//!
//! #[async_trait::async_trait]
//! impl MigrationTrait for Migration {
//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
//! let table = table_auto(Users::Table)
//! .col(pk_auto(Users::Id))
//! .col(uuid(Users::Pid))
//! .col(string_uniq(Users::Email))
//! .col(string(Users::Password))
//! .col(string(Users::Name))
//! .col(string_null(Users::ResetToken))
//! .col(timestamp_null(Users::ResetSentAt))
//! .to_owned();
//! manager.create_table(table).await?;
//! Ok(())
//! }
//!
//! async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
//! manager
//! .drop_table(Table::drop().table(Users::Table).to_owned())
//! .await
//! }
//! }
//!
//! #[derive(Iden)]
//! pub enum Users {
//! Table,
//! Id,
//! Pid,
//! Email,
//! Name,
//! Password,
//! ResetToken,
//! ResetSentAt,
//! }
//! ```
use crate::prelude::Iden;
use sea_orm::sea_query::{
self, Alias, ColumnDef, ColumnType, Expr, IntoIden, PgInterval, Table, TableCreateStatement,
};
#[derive(Iden)]
enum GeneralIds {
CreatedAt,
UpdatedAt,
}
/// Wrapping table schema creation.
pub fn table_auto<T: IntoIden + 'static>(name: T) -> TableCreateStatement {
timestamps(Table::create().table(name).if_not_exists().take())
}
/// Create a primary key column with auto-increment feature.
pub fn pk_auto<T: IntoIden>(name: T) -> ColumnDef {
integer(name).auto_increment().primary_key().take()
}
/// Create a UUID primary key
pub fn pk_uuid<T: IntoIden>(name: T) -> ColumnDef {
uuid(name).primary_key().take()
}
pub fn char_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).char_len(length).not_null().take()
}
pub fn char_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).char_len(length).null().take()
}
pub fn char_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
char_len(col, length).unique_key().take()
}
pub fn char<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).char().not_null().take()
}
pub fn char_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).char().null().take()
}
pub fn char_uniq<T: IntoIden>(col: T) -> ColumnDef {
char(col).unique_key().take()
}
pub fn string_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).string_len(length).not_null().take()
}
pub fn string_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).string_len(length).null().take()
}
pub fn string_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
string_len(col, length).unique_key().take()
}
pub fn string<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).string().not_null().take()
}
pub fn string_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).string().null().take()
}
pub fn string_uniq<T: IntoIden>(col: T) -> ColumnDef {
string(col).unique_key().take()
}
pub fn text<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).text().not_null().take()
}
pub fn text_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).text().null().take()
}
pub fn text_uniq<T: IntoIden>(col: T) -> ColumnDef {
text(col).unique_key().take()
}
pub fn tiny_integer<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).tiny_integer().not_null().take()
}
pub fn tiny_integer_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).tiny_integer().null().take()
}
pub fn tiny_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
tiny_integer(col).unique_key().take()
}
pub fn small_integer<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).small_integer().not_null().take()
}
pub fn small_integer_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).small_integer().null().take()
}
pub fn small_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
small_integer(col).unique_key().take()
}
pub fn integer<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).integer().not_null().take()
}
pub fn integer_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).integer().null().take()
}
pub fn integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
integer(col).unique_key().take()
}
pub fn big_integer<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).big_integer().not_null().take()
}
pub fn big_integer_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).big_integer().null().take()
}
pub fn big_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
big_integer(col).unique_key().take()
}
pub fn tiny_unsigned<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).tiny_unsigned().not_null().take()
}
pub fn tiny_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).tiny_unsigned().null().take()
}
pub fn tiny_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
tiny_unsigned(col).unique_key().take()
}
pub fn small_unsigned<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).small_unsigned().not_null().take()
}
pub fn small_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).small_unsigned().null().take()
}
pub fn small_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
small_unsigned(col).unique_key().take()
}
pub fn unsigned<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).unsigned().not_null().take()
}
pub fn unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).unsigned().null().take()
}
pub fn unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
unsigned(col).unique_key().take()
}
pub fn big_unsigned<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).big_unsigned().not_null().take()
}
pub fn big_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).big_unsigned().null().take()
}
pub fn big_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
big_unsigned(col).unique_key().take()
}
pub fn float<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).float().not_null().take()
}
pub fn float_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).float().null().take()
}
pub fn float_uniq<T: IntoIden>(col: T) -> ColumnDef {
float(col).unique_key().take()
}
pub fn double<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).double().not_null().take()
}
pub fn double_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).double().null().take()
}
pub fn double_uniq<T: IntoIden>(col: T) -> ColumnDef {
double(col).unique_key().take()
}
pub fn decimal_len<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
ColumnDef::new(col)
.decimal_len(precision, scale)
.not_null()
.take()
}
pub fn decimal_len_null<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
ColumnDef::new(col)
.decimal_len(precision, scale)
.null()
.take()
}
pub fn decimal_len_uniq<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
decimal_len(col, precision, scale).unique_key().take()
}
pub fn decimal<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).decimal().not_null().take()
}
pub fn decimal_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).decimal().null().take()
}
pub fn decimal_uniq<T: IntoIden>(col: T) -> ColumnDef {
decimal(col).unique_key().take()
}
pub fn date_time<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).date_time().not_null().take()
}
pub fn date_time_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).date_time().null().take()
}
pub fn date_time_uniq<T: IntoIden>(col: T) -> ColumnDef {
date_time(col).unique_key().take()
}
pub fn interval<T: IntoIden>(
col: T,
fields: Option<PgInterval>,
precision: Option<u32>,
) -> ColumnDef {
ColumnDef::new(col)
.interval(fields, precision)
.not_null()
.take()
}
pub fn interval_null<T: IntoIden>(
col: T,
fields: Option<PgInterval>,
precision: Option<u32>,
) -> ColumnDef {
ColumnDef::new(col)
.interval(fields, precision)
.null()
.take()
}
pub fn interval_uniq<T: IntoIden>(
col: T,
fields: Option<PgInterval>,
precision: Option<u32>,
) -> ColumnDef {
interval(col, fields, precision).unique_key().take()
}
pub fn timestamp<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).timestamp().not_null().take()
}
pub fn timestamp_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).timestamp().null().take()
}
pub fn timestamp_uniq<T: IntoIden>(col: T) -> ColumnDef {
timestamp(col).unique_key().take()
}
pub fn timestamp_with_time_zone<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col)
.timestamp_with_time_zone()
.not_null()
.take()
}
pub fn timestamp_with_time_zone_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).timestamp_with_time_zone().null().take()
}
pub fn timestamp_with_time_zone_uniq<T: IntoIden>(col: T) -> ColumnDef {
timestamp_with_time_zone(col).unique_key().take()
}
pub fn time<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).time().not_null().take()
}
pub fn time_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).time().null().take()
}
pub fn time_uniq<T: IntoIden>(col: T) -> ColumnDef {
time(col).unique_key().take()
}
pub fn date<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).date().not_null().take()
}
pub fn date_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).date().null().take()
}
pub fn date_uniq<T: IntoIden>(col: T) -> ColumnDef {
date(col).unique_key().take()
}
pub fn year<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).year().not_null().take()
}
pub fn year_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).year().null().take()
}
pub fn year_uniq<T: IntoIden>(col: T) -> ColumnDef {
year(col).unique_key().take()
}
pub fn binary_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).binary_len(length).not_null().take()
}
pub fn binary_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).binary_len(length).null().take()
}
pub fn binary_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
binary_len(col, length).unique_key().take()
}
pub fn binary<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).binary().not_null().take()
}
pub fn binary_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).binary().null().take()
}
pub fn binary_uniq<T: IntoIden>(col: T) -> ColumnDef {
binary(col).unique_key().take()
}
pub fn var_binary<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).var_binary(length).not_null().take()
}
pub fn var_binary_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).var_binary(length).null().take()
}
pub fn var_binary_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
var_binary(col, length).unique_key().take()
}
pub fn bit<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
ColumnDef::new(col).bit(length).not_null().take()
}
pub fn bit_null<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
ColumnDef::new(col).bit(length).null().take()
}
pub fn bit_uniq<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
bit(col, length).unique_key().take()
}
pub fn varbit<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).varbit(length).not_null().take()
}
pub fn varbit_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
ColumnDef::new(col).varbit(length).null().take()
}
pub fn varbit_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
varbit(col, length).unique_key().take()
}
pub fn blob<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).blob().not_null().take()
}
pub fn blob_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).blob().null().take()
}
pub fn blob_uniq<T: IntoIden>(col: T) -> ColumnDef {
blob(col).unique_key().take()
}
pub fn boolean<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).boolean().not_null().take()
}
pub fn boolean_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).boolean().null().take()
}
pub fn boolean_uniq<T: IntoIden>(col: T) -> ColumnDef {
boolean(col).unique_key().take()
}
pub fn money_len<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
ColumnDef::new(col)
.money_len(precision, scale)
.not_null()
.take()
}
pub fn money_len_null<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
ColumnDef::new(col)
.money_len(precision, scale)
.null()
.take()
}
pub fn money_len_uniq<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
money_len(col, precision, scale).unique_key().take()
}
pub fn money<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).money().not_null().take()
}
pub fn money_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).money().null().take()
}
pub fn money_uniq<T: IntoIden>(col: T) -> ColumnDef {
money(col).unique_key().take()
}
pub fn json<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).json().not_null().take()
}
pub fn json_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).json().null().take()
}
pub fn json_uniq<T: IntoIden>(col: T) -> ColumnDef {
json(col).unique_key().take()
}
pub fn json_binary<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).json_binary().not_null().take()
}
pub fn json_binary_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).json_binary().null().take()
}
pub fn json_binary_uniq<T: IntoIden>(col: T) -> ColumnDef {
json_binary(col).unique_key().take()
}
pub fn uuid<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).uuid().not_null().take()
}
pub fn uuid_null<T: IntoIden>(col: T) -> ColumnDef {
ColumnDef::new(col).uuid().null().take()
}
pub fn uuid_uniq<T: IntoIden>(col: T) -> ColumnDef {
uuid(col).unique_key().take()
}
pub fn custom<T: IntoIden, N: IntoIden>(col: T, name: N) -> ColumnDef {
ColumnDef::new(col).custom(name).not_null().take()
}
pub fn custom_null<T: IntoIden, N: IntoIden>(col: T, name: N) -> ColumnDef {
ColumnDef::new(col).custom(name).null().take()
}
pub fn enumeration<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
where
T: IntoIden,
N: IntoIden,
S: IntoIden,
V: IntoIterator<Item = S>,
{
ColumnDef::new(col)
.enumeration(name, variants)
.not_null()
.take()
}
pub fn enumeration_null<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
where
T: IntoIden,
N: IntoIden,
S: IntoIden,
V: IntoIterator<Item = S>,
{
ColumnDef::new(col)
.enumeration(name, variants)
.null()
.take()
}
pub fn enumeration_uniq<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
where
T: IntoIden,
N: IntoIden,
S: IntoIden,
V: IntoIterator<Item = S>,
{
enumeration(col, name, variants).unique_key().take()
}
pub fn array<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
ColumnDef::new(col).array(elem_type).not_null().take()
}
pub fn array_null<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
ColumnDef::new(col).array(elem_type).null().take()
}
pub fn array_uniq<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
array(col, elem_type).unique_key().take()
}
/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table.
pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
let mut t = t;
t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp()))
.col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp()))
.take()
}
/// Create an Alias.
pub fn name<T: Into<String>>(name: T) -> Alias {
Alias::new(name)
}

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

@ -0,0 +1,31 @@
use pagetop::prelude::*;
use std::sync::LazyLock;
pub mod config;
pub mod db;
/// The package Prelude.
pub mod prelude {
pub use crate::db::*;
pub use crate::install_migrations;
}
include_locales!(LOCALES_SEAORM);
/// Implements [`PackageTrait`] and specific package API.
pub struct SeaORM;
impl PackageTrait for SeaORM {
fn name(&self) -> L10n {
L10n::t("package_name", &LOCALES_SEAORM)
}
fn description(&self) -> L10n {
L10n::t("package_description", &LOCALES_SEAORM)
}
fn init(&self) {
LazyLock::force(&db::DBCONN);
}
}

View file

@ -0,0 +1,2 @@
package_name = SeaORM support
package_description = Integrate SeaORM as the database framework for PageTop applications.

View file

@ -0,0 +1,2 @@
package_name = Soporte a SeaORM
package_description = Integra SeaORM como framework de base de datos para aplicaciones PageTop.

View file

@ -1,104 +0,0 @@
use crate::core::package::PackageTrait;
pub type ThemeRef = &'static dyn ThemeTrait;
/// Los temas deben implementar este "trait".
pub trait ThemeTrait: PackageTrait + Send + Sync {
/*
#[rustfmt::skip]
fn regions(&self) -> Vec<(&'static str, L10n)> {
vec![
("header", L10n::l("header")),
("pagetop", L10n::l("pagetop")),
("sidebar_left", L10n::l("sidebar_left")),
("content", L10n::l("content")),
("sidebar_right", L10n::l("sidebar_right")),
("footer", L10n::l("footer")),
]
}
#[allow(unused_variables)]
fn before_prepare_body(&self, page: &mut Page) {}
fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
div class="skip__to_content" {
a href=(concat_string!("#", skip_to_id)) { (skip) }
}
}
(flex::Container::new()
.with_id("body__wrapper")
.with_direction(flex::Direction::Column(BreakPoint::None))
.with_align(flex::Align::Center)
.add_item(flex::Item::region().with_id("header"))
.add_item(flex::Item::region().with_id("pagetop"))
.add_item(
flex::Item::with(
flex::Container::new()
.with_direction(flex::Direction::Row(BreakPoint::None))
.add_item(
flex::Item::region()
.with_id("sidebar_left")
.with_grow(flex::Grow::Is1),
)
.add_item(
flex::Item::region()
.with_id("content")
.with_grow(flex::Grow::Is3),
)
.add_item(
flex::Item::region()
.with_id("sidebar_right")
.with_grow(flex::Grow::Is1),
),
)
.with_id("flex__wrapper"),
)
.add_item(flex::Item::region().with_id("footer"))
.render(page.context()))
}
})
}
fn after_prepare_body(&self, page: &mut Page) {
page.set_assets(AssetsOp::SetFaviconIfNone(
Favicon::new().with_icon("/base/favicon.ico"),
));
}
fn prepare_head(&self, page: &mut Page) -> PrepareMarkup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
PrepareMarkup::With(html! {
head {
meta charset="utf-8";
@if let Some(title) = page.title() {
title { (global::SETTINGS.app.name) (" - ") (title) }
} @else {
title { (global::SETTINGS.app.name) }
}
@if let Some(description) = page.description() {
meta name="description" content=(description);
}
meta name="viewport" content=(viewport);
@for (name, content) in page.metadata() {
meta name=(name) content=(content) {}
}
meta http-equiv="X-UA-Compatible" content="IE=edge";
@for (property, content) in page.properties() {
meta property=(property) content=(content) {}
}
(page.context().prepare_assets())
}
})
}
*/
}

View file

@ -1,4 +0,0 @@
//! HTML in code.
mod maud;
pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE};

View file

@ -1,13 +0,0 @@
# Branding component.
site_home = Home
# PoweredBy component.
poweredby_pagetop = Powered by {$pagetop_link}
pagetop_logo = PageTop logo
# Menu component.
menu_toggle = Toggle menu visibility
# Form components.
button_submit = Submit
button_reset = Reset

View file

@ -1,8 +0,0 @@
header = Header
pagetop = Page Top
content = Content
sidebar_left = Sidebar Left
sidebar_right = Sidebar Right
footer = Footer
skip_to_content = Skip to main content (Press Enter)

View file

@ -1,26 +0,0 @@
welcome_package_name = Default homepage
welcome_package_description = Displays a demo homepage when none is configured.
welcome_title = Hello world!
welcome_intro = This page is used to check the proper operation of the { $app } installation.
welcome_powered = This web solution is powered by { $pagetop }.
welcome_code = Code
welcome = Welcome
welcome_page = Welcome page
welcome_subtitle = Are you user of { $app }?
welcome_text1 = If you don't know what this page is about, this probably means that the site is either experiencing problems or is undergoing routine maintenance.
welcome_text2 = If the problem persists, please contact your system administrator.
welcome_pagetop_title = About PageTop
welcome_pagetop_text1 = If you can read this page, it means that the PageTop server is working properly, but has not yet been configured.
welcome_pagetop_text2 = PageTop is a <a href="https://www.rust-lang.org" target="_blank">Rust</a>-based web development framework to build modular, extensible, and configurable web solutions.
welcome_pagetop_text3 = For more information on PageTop please visit the <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">technical documentation</a>.
welcome_promo_title = Promoting PageTop
welcome_promo_text1 = You are free to use the image below on applications powered by { $pagetop }. Thanks for using PageTop!
welcome_issues_title = Reporting problems
welcome_issues_text1 = Please use <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub to report any issues</a> with PageTop. However, check the existing error reports before submitting a new issue.
welcome_issues_text2 = If the issues are specific to { $app }, please refer to its official repository or support channel, rather than directly to PageTop.

View file

@ -1,13 +0,0 @@
# Branding component.
site_home = Inicio
# PoweredBy component.
poweredby_pagetop = Funciona con {$pagetop_link}
pagetop_logo = Logotipo de PageTop
# Menu component.
menu_toggle = Alternar visibilidad del menú
# Form components.
button_submit = Enviar
button_reset = Reiniciar

View file

@ -1,8 +0,0 @@
header = Cabecera
pagetop = Superior
content = Contenido
sidebar_left = Barra lateral izquierda
sidebar_right = Barra lateral derecha
footer = Pie
skip_to_content = Ir al contenido principal (Pulsar Intro)

View file

@ -1,26 +0,0 @@
welcome_package_name = Página de inicio predeterminada
welcome_package_description = Muestra una página de demostración predeterminada cuando no hay ninguna configurada.
welcome_title = ¡Hola mundo!
welcome_intro = Esta página se utiliza para verificar el correcto funcionamiento de la instalación de { $app }.
welcome_powered = Esta solución web funciona con { $pagetop }.
welcome_code = Código
welcome = Bienvenida
welcome_page = Página de bienvenida
welcome_subtitle = ¿Eres usuario de { $app }?
welcome_text1 = Si no sabes de qué trata esta página, probablemente significa que el sitio está experimentando problemas o está pasando por un mantenimiento de rutina.
welcome_text2 = Si el problema persiste, póngase en contacto con el administrador del sistema.
welcome_pagetop_title = Sobre PageTop
welcome_pagetop_text1 = Si puedes leer esta página, significa que el servidor PageTop funciona correctamente, pero aún no se ha configurado.
welcome_pagetop_text2 = PageTop es un entorno de desarrollo web basado en <a href="https://www.rust-lang.org/es" target="_blank">Rust</a> para construir soluciones web modulares, extensibles y configurables.
welcome_pagetop_text3 = Para más información sobre PageTop, por favor visita la <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">documentación técnica</a>.
welcome_promo_title = Promociona PageTop
welcome_promo_text1 = Eres libre de usar la siguiente imagen en aplicaciones desarrolladas con { $pagetop }. ¡Gracias por usar PageTop!
welcome_issues_title = Informando problemas
welcome_issues_text1 = Por favor, utiliza <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub para reportar cualquier problema</a> con PageTop. No obstante, comprueba los informes de errores existentes antes de enviar uno nuevo.
welcome_issues_text2 = Si son fallos específicos de { $app }, por favor acude a su repositorio o canal de soporte oficial y no al de PageTop directamente.

View file

@ -1,306 +0,0 @@
//! Useful functions and macros.
pub mod config;
mod data;
mod de;
mod error;
mod file;
mod path;
mod source;
mod value;
use crate::trace;
use std::io;
use std::path::PathBuf;
// USEFUL FUNCTIONS ********************************************************************************
pub enum TypeInfo {
FullName,
ShortName,
NameFrom(isize),
NameTo(isize),
PartialName(isize, isize),
}
impl TypeInfo {
pub fn of<T: ?Sized>(&self) -> &'static str {
let type_name = std::any::type_name::<T>();
match self {
TypeInfo::FullName => type_name,
TypeInfo::ShortName => Self::partial(type_name, -1, None),
TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None),
TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)),
TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)),
}
}
fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
let maxlen = type_name.len();
let mut segments = Vec::new();
let mut segment_start = 0; // Start position of the current segment.
let mut angle_brackets = 0; // Counter for tracking '<' and '>'.
let mut previous_char = '\0'; // Initializes to a null character, no previous character.
for (idx, c) in type_name.char_indices() {
match c {
':' if angle_brackets == 0 => {
if previous_char == ':' {
if segment_start < idx - 1 {
segments.push((segment_start, idx - 1)); // Do not include last '::'.
}
segment_start = idx + 1; // Next segment starts after '::'.
}
}
'<' => angle_brackets += 1,
'>' => angle_brackets -= 1,
_ => {}
}
previous_char = c;
}
// Include the last segment if there's any.
if segment_start < maxlen {
segments.push((segment_start, maxlen));
}
// Calculates the start position.
let start_pos = segments
.get(if start >= 0 {
start as usize
} else {
segments.len() - start.unsigned_abs()
})
.map_or(0, |&(s, _)| s);
// Calculates the end position.
let end_pos = segments
.get(if let Some(end) = end {
if end >= 0 {
end as usize
} else {
segments.len() - end.unsigned_abs()
}
} else {
segments.len() - 1
})
.map_or(maxlen, |&(_, e)| e);
// Returns the partial string based on the calculated positions.
&type_name[start_pos..end_pos]
}
}
/// Calculates the absolute directory given a root path and a relative path.
///
/// # Arguments
///
/// * `root_path` - A string slice that holds the root path.
/// * `relative_path` - A string slice that holds the relative path.
///
/// # Returns
///
/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`.
/// * `Err` - If an I/O error occurs, returns an `io::Error`.
///
/// # Errors
///
/// This function will return an error if:
/// - The root path or relative path are invalid.
/// - There is an issue with file system operations, such as reading the directory.
///
/// # Examples
///
/// ```
/// let root = "/home/user";
/// let relative = "documents";
/// let abs_dir = absolute_dir(root, relative).unwrap();
/// println!("{}", abs_dir);
/// ```
pub fn absolute_dir(
root_path: impl Into<String>,
relative_path: impl Into<String>,
) -> Result<String, io::Error> {
let root_path = PathBuf::from(root_path.into());
let full_path = root_path.join(relative_path.into());
let absolute_dir = full_path.to_string_lossy().into();
if !full_path.is_absolute() {
let message = format!("Path \"{absolute_dir}\" is not absolute");
trace::warn!(message);
return Err(io::Error::new(io::ErrorKind::InvalidInput, message));
}
if !full_path.exists() {
let message = format!("Path \"{absolute_dir}\" does not exist");
trace::warn!(message);
return Err(io::Error::new(io::ErrorKind::NotFound, message));
}
if !full_path.is_dir() {
let message = format!("Path \"{absolute_dir}\" is not a directory");
trace::warn!(message);
return Err(io::Error::new(io::ErrorKind::InvalidInput, message));
}
Ok(absolute_dir)
}
// USEFUL MACROS ***********************************************************************************
#[macro_export]
/// Macro para construir grupos de pares clave-valor.
///
/// ```rust#ignore
/// let args = kv![
/// "userName" => "Roberto",
/// "photoCount" => 3,
/// "userGender" => "male",
/// ];
/// ```
macro_rules! kv {
( $($key:expr => $value:expr),* $(,)? ) => {{
let mut a = std::collections::HashMap::new();
$(
a.insert($key.into(), $value.into());
)*
a
}};
}
#[macro_export]
/// Define un conjunto de ajustes de configuración usando tipos seguros y valores predefinidos.
///
/// Detiene la aplicación con un panic! si no pueden asignarse los ajustes de configuración.
///
/// Carga la configuración de la aplicación en forma de pares `clave = valor` recogidos en archivos
/// [TOML](https://toml.io).
///
/// La metodología [The Twelve-Factor App](https://12factor.net/es/) define **la configuración de
/// una aplicación como todo lo que puede variar entre despliegues**, diferenciando entre entornos
/// de desarrollo, pre-producción, producción, etc.
///
/// A veces las aplicaciones guardan configuraciones como constantes en el código, lo que implica
/// una violación de esta metodología. `PageTop` recomienda una **estricta separación entre código y
/// configuración**. La configuración variará en cada tipo de despliegue, y el código no.
///
///
/// # Cómo cargar los ajustes de configuración
///
/// Si tu aplicación requiere archivos de configuración debes crear un directorio *config* al mismo
/// nivel del archivo *Cargo.toml* de tu proyecto (o del ejecutable binario de la aplicación).
///
/// `PageTop` se encargará de cargar todos los ajustes de configuración de tu aplicación leyendo los
/// siguientes archivos TOML en este orden (todos los archivos son opcionales):
///
/// 1. **config/common.toml**, útil para los ajustes comunes a cualquier entorno. Estos valores
/// podrán ser sobrescritos al fusionar los archivos de configuración restantes.
///
/// 2. **config/{file}.toml**, donde *{file}* se define con la variable de entorno
/// `PAGETOP_RUN_MODE`:
///
/// * Si no está definida se asumirá *default* por defecto y `PageTop` intentará cargar el
/// archivo *config/default.toml* si existe.
///
/// * De esta manera podrás tener diferentes ajustes de configuración para diferentes entornos
/// de ejecución. Por ejemplo, para *devel.toml*, *staging.toml* o *production.toml*. O
/// también para *server1.toml* o *server2.toml*. Sólo uno será cargado.
///
/// * Normalmente estos archivos suelen ser idóneos para incluir contraseñas o configuración
/// sensible asociada al entorno correspondiente. Estos archivos no deberían ser publicados en
/// el repositorio Git por razones de seguridad.
///
/// 3. **config/local.toml**, para añadir o sobrescribir ajustes de los archivos anteriores.
///
///
/// # Cómo añadir ajustes de configuración
///
/// Para proporcionar a tu **módulo** sus propios ajustes de configuración, añade
/// [*serde*](https://docs.rs/serde) en las dependencias de tu archivo *Cargo.toml* habilitando la
/// característica `derive`:
///
/// ```toml
/// [dependencies]
/// serde = { version = "1.0", features = ["derive"] }
/// ```
///
/// Y luego inicializa con la macro [`static_config!`](crate::static_config) tus ajustes, usando
/// tipos seguros y asignando los valores predefinidos para la estructura asociada:
///
/// ```
/// use pagetop::prelude::*;
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize)]
/// pub struct Settings {
/// pub myapp: MyApp,
/// }
///
/// #[derive(Debug, Deserialize)]
/// pub struct MyApp {
/// pub name: String,
/// pub description: Option<String>,
/// pub width: u16,
/// pub height: u16,
/// }
///
/// static_config!(SETTINGS: Settings => [
/// // [myapp]
/// "myapp.name" => "Value Name",
/// "myapp.width" => 900,
/// "myapp.height" => 320,
/// ]);
/// ```
///
/// De hecho, así se declaran los ajustes globales de la configuración (ver [`SETTINGS`]).
///
/// Puedes usar la [sintaxis TOML](https://toml.io/en/v1.0.0#table) para añadir tu nueva sección
/// `[myapp]` en los archivos de configuración, del mismo modo que se añaden `[log]` o `[server]` en
/// los ajustes globales (ver [`Settings`]).
///
/// Se recomienda inicializar todos los ajustes con valores predefinidos, o utilizar la notación
/// `Option<T>` si van a ser tratados en el código como opcionales.
///
/// Si no pueden inicializarse correctamente los ajustes de configuración, entonces la aplicación
/// ejecutará un panic! y detendrá la ejecución.
///
/// Los ajustes de configuración siempre serán de sólo lectura.
///
///
/// # Cómo usar tus nuevos ajustes de configuración
///
/// ```
/// use pagetop::prelude::*;
/// use crate::config;
///
/// fn global_settings() {
/// println!("App name: {}", &global::SETTINGS.app.name);
/// println!("App description: {}", &global::SETTINGS.app.description);
/// println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode);
/// }
///
/// fn package_settings() {
/// println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
/// println!("{}", &config::SETTINGS.myapp.width);
/// }
/// ```
macro_rules! static_config {
( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => {
#[doc = concat!(
"Assigned or predefined values for configuration settings associated to the ",
"[`", stringify!($Settings), "`] type."
)]
pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| {
let mut settings = $crate::util::config::CONFIG_DATA.clone();
$(
settings.set_default($key, $value).unwrap();
)*
match settings.try_into() {
Ok(s) => s,
Err(e) => panic!("Error parsing settings: {}", e),
}
});
};
}

View file

@ -1,51 +0,0 @@
//! Retrieve settings values from configuration files.
use crate::concat_string;
use crate::util::data::ConfigData;
use crate::util::file::File;
use std::sync::LazyLock;
use std::env;
use std::path::Path;
/// Original configuration values in `key = value` pairs gathered from configuration files.
pub static CONFIG_DATA: LazyLock<ConfigData> = LazyLock::new(|| {
// Identify the configuration directory.
let config_dir = env::var("CARGO_MANIFEST_DIR")
.map(|manifest_dir| {
let manifest_config = Path::new(&manifest_dir).join("config");
if manifest_config.exists() {
manifest_config.to_string_lossy().to_string()
} else {
"config".to_string()
}
})
.unwrap_or_else(|_| "config".to_string());
// Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'.
let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
// Initialize settings.
let mut settings = ConfigData::default();
// Merge (optional) configuration files and set the execution mode.
settings
// First, add the common configuration for all environments. Defaults to 'common.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/common.toml")).required(false))
.expect("Failed to merge common configuration (common.toml)")
// Add the environment-specific configuration. Defaults to 'default.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false))
.expect(&format!("Failed to merge {rm}.toml configuration"))
// Add reserved local configuration for the environment. Defaults to 'local.default.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false))
.expect("Failed to merge reserved local environment configuration")
// Add the general reserved local configuration. Defaults to 'local.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false))
.expect("Failed to merge general reserved local configuration")
// Save the execution mode.
.set("app.run_mode", rm)
.expect("Failed to set application run mode");
settings
});

View file

@ -8,7 +8,7 @@ description = """\
""" """
categories = ["web-programming", "gui", "development-tools", "asynchronous"] categories = ["web-programming", "gui", "development-tools", "asynchronous"]
keywords = ["pagetop", "web", "framework", "frontend", "ssr"] keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
readme = "../../README.md" readme = "../README.md"
homepage = { workspace = true } homepage = { workspace = true }
repository = { workspace = true } repository = { workspace = true }
@ -22,18 +22,20 @@ name = "pagetop"
colored = "2.1.0" colored = "2.1.0"
concat-string = "1.0.1" concat-string = "1.0.1"
figlet-rs = "0.1.5" figlet-rs = "0.1.5"
fluent-bundle = "0.15.3"
fluent-templates = "0.11.0"
itoa = "1.0.11" itoa = "1.0.11"
nom = "7.1.3" nom = "7.1.3"
paste = "1.0.15" paste = "1.0.15"
substring = "1.4.5" substring = "1.4.5"
terminal_size = "0.4.0" terminal_size = "0.4.0"
toml = "0.8.19" toml = "0.8.19"
tracing = "0.1.40" tracing = "0.1.40"
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
tracing-actix-web = "0.7.15" tracing-actix-web = "0.7.15"
fluent-bundle = "0.15.3"
fluent-templates = "0.11.0"
unic-langid = { version = "0.9.5", features = ["macros"] } unic-langid = { version = "0.9.5", features = ["macros"] }
actix-web = "4.9.0" actix-web = "4.9.0"
@ -45,3 +47,6 @@ serde.workspace = true
static-files.workspace = true static-files.workspace = true
pagetop-macros.workspace = true pagetop-macros.workspace = true
[build-dependencies]
pagetop-build.workspace = true

View file

@ -1,7 +1,7 @@
use pagetop_build::StaticFilesBundle; use pagetop_build::StaticFilesBundle;
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
StaticFilesBundle::from_dir("./static/base") StaticFilesBundle::from_dir("../static", None)
.with_name("base") .with_name("assets")
.build() .build()
} }

View file

@ -0,0 +1,5 @@
[app]
name = "Samples"
[log]
tracing = "Debug"

View file

@ -15,7 +15,7 @@ async fn hello_name(
) -> ResultPage<Markup, ErrorPage> { ) -> ResultPage<Markup, ErrorPage> {
let name = path.into_inner(); let name = path.into_inner();
Page::new(request) Page::new(request)
.with_component(Html::with(html! { h1 { "Hello " (name) "!" } })) .with_body(PrepareMarkup::With(html! { h1 { "Hello " (name) "!" } }))
.render() .render()
} }

View file

@ -10,7 +10,7 @@ impl PackageTrait for HelloWorld {
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> { async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_component(Html::with(html! { h1 { "Hello World!" } })) .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
.render() .render()
} }

View file

@ -3,6 +3,8 @@
mod figfont; mod figfont;
use crate::core::{package, package::PackageRef}; use crate::core::{package, package::PackageRef};
use crate::html::Markup;
use crate::response::page::{ErrorPage, ResultPage};
use crate::{global, locale, service, trace}; use crate::{global, locale, service, trace};
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle}; use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
@ -42,7 +44,7 @@ impl Application {
LazyLock::force(&trace::TRACING); LazyLock::force(&trace::TRACING);
// Validates the default language identifier. // Validates the default language identifier.
LazyLock::force(&locale::LANGID_DEFAULT); LazyLock::force(&locale::DEFAULT_LANGID);
// Registers the application's packages. // Registers the application's packages.
package::all::register_packages(root_package); package::all::register_packages(root_package);
@ -154,12 +156,12 @@ impl Application {
InitError = (), InitError = (),
>, >,
> { > {
service::App::new().configure(package::all::configure_services) service::App::new()
// .default_service(service::web::route().to(service_not_found)) .configure(package::all::configure_services)
.default_service(service::web::route().to(service_not_found))
} }
} }
/*
async fn service_not_found(request: HttpRequest) -> ResultPage<Markup, ErrorPage> { async fn service_not_found(request: service::HttpRequest) -> ResultPage<Markup, ErrorPage> {
Err(ErrorPage::NotFound(request)) Err(ErrorPage::NotFound(request))
} }
*/

196
pagetop/src/config.rs Normal file
View file

@ -0,0 +1,196 @@
//! Load configuration settings.
//!
//! These settings are loaded from [TOML](https://toml.io) files as `key = value` pairs and mapped
//! into type-safe structures with predefined values.
//!
//! Following the [Twelve-Factor App](https://12factor.net/config) methodology, `PageTop` separates
//! code from configuration. This approach allows configurations to vary across deployments, such as
//! development, staging, or production, without changing the codebase.
//!
//!
//! # Loading configuration settings
//!
//! If your application requires configuration files, create a `config` directory in the root of
//! your project, at the same level as the *Cargo.toml* file or the application's binary.
//!
//! `PageTop` automatically loads configuration settings by reading the following TOML files in
//! order (all files are optional):
//!
//! 1. **config/common.toml**, for settings shared across all environments. This approach simplifies
//! maintenance by centralizing common configuration values.
//!
//! 2. **config/{rm}.toml**, where `{rm}` corresponds to the environment variable
//! `PAGETOP_RUN_MODE`:
//!
//! * If `PAGETOP_RUN_MODE` is not set, it defaults to `default`, and `PageTop` attempts to load
//! *config/default.toml* if available.
//!
//! * Useful for environment-specific configurations, ensuring that each environment
//! (e.g., development, staging, production) has its own settings without affecting others,
//! such as API keys, URLs, or performance-related adjustments.
//!
//! 3. **config/local.{rm}.toml**, useful for local machine-specific configurations:
//!
//! * This file allows you to add or override settings specific to the environment. For example,
//! `local.devel.toml` for development or `local.production.toml` for production tweaks.
//!
//! * It enables developers to tailor settings for their machines within a given environment and
//! is typically not shared or committed to version control systems.
//!
//! 4. **config/local.toml**, for general local settings across all environments, ideal for quick
//! adjustments or temporary values not tied to any specific environment.
//!
//! The configuration settings are merged in the order listed above, with later files overriding
//! earlier ones if there are conflicts.
//!
//!
//! # Adding configuration settings
//!
//! To give your **module** its own configuration settings, add [*serde*](https://docs.rs/serde) as
//! a dependency in your *Cargo.toml* file with the `derive` feature enabled:
//!
//! ```toml
//! [dependencies]
//! serde = { version = "1.0", features = ["derive"] }
//! ```
//!
//! Then, use the [`include_config!`](crate::include_config) macro to initialize your settings with
//! type-safe structures and predefined values:
//!
//! ```
//! use pagetop::prelude::*;
//! use serde::Deserialize;
//!
//! include_config!(SETTINGS: Settings => [
//! // [myapp]
//! "myapp.name" => "Value Name",
//! "myapp.width" => 900,
//! "myapp.height" => 320,
//! ]);
//!
//! #[derive(Debug, Deserialize)]
//! pub struct Settings {
//! pub myapp: MyApp,
//! }
//!
//! #[derive(Debug, Deserialize)]
//! pub struct MyApp {
//! pub name: String,
//! pub description: Option<String>,
//! pub width: u16,
//! pub height: u16,
//! }
//! ```
//!
//! This is how global configuration settings are declared (see [`SETTINGS`](crate::global::SETTINGS)).
//!
//! You can add a new `[myapp]` section in the configuration files using the
//! [TOML syntax](https://toml.io/en/v1.0.0#table), just like the `[log]` or `[server]` sections in
//! the global settings (see [`Settings`](crate::global::Settings)).
//!
//! It is recommended to initialize all settings with predefined values or use `Option<T>` for
//! optional settings handled within the code.
//!
//! If configuration settings fail to initialize correctly, the application will panic and stop
//! execution.
//!
//! Configuration settings are always read-only.
//!
//!
//! # Using your new configuration settings
//!
//! Access the settings directly in your code:
//!
//! ```
//! use pagetop::prelude::*;
//! use crate::config;
//!
//! fn global_settings() {
//! println!("App name: {}", &global::SETTINGS.app.name);
//! println!("App description: {}", &global::SETTINGS.app.description);
//! println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode);
//! }
//!
//! fn package_settings() {
//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
//! println!("{}", &config::SETTINGS.myapp.width);
//! }
//! ```
mod data;
mod de;
mod error;
mod file;
mod path;
mod source;
mod value;
use crate::concat_string;
use crate::config::data::ConfigData;
use crate::config::file::File;
use std::sync::LazyLock;
use std::env;
use std::path::Path;
/// Original values read from configuration files in `key = value` pairs.
pub static CONFIG_VALUES: LazyLock<ConfigData> = LazyLock::new(|| {
// Identify the configuration directory.
let config_dir = env::var("CARGO_MANIFEST_DIR")
.map(|manifest_dir| {
let manifest_config = Path::new(&manifest_dir).join("config");
if manifest_config.exists() {
manifest_config.to_string_lossy().to_string()
} else {
"config".to_string()
}
})
.unwrap_or_else(|_| "config".to_string());
// Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'.
let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
// Initialize config values.
let mut values = ConfigData::default();
// Merge (optional) configuration files and set the execution mode.
values
// First, add the common configuration for all environments. Defaults to 'common.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/common.toml")).required(false))
.expect("Failed to merge common configuration (common.toml)")
// Add the environment-specific configuration. Defaults to 'default.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false))
.expect(&format!("Failed to merge {rm}.toml configuration"))
// Add reserved local configuration for the environment. Defaults to 'local.default.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false))
.expect("Failed to merge reserved local environment configuration")
// Add common reserved local configuration. Defaults to 'local.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false))
.expect("Failed to merge general reserved local configuration")
// Save the execution mode.
.set("app.run_mode", rm)
.expect("Failed to set application run mode");
values
});
#[macro_export]
macro_rules! include_config {
( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => {
#[doc = concat!(
"Assigned or predefined values for configuration settings associated to the ",
"[`", stringify!($Settings), "`] type."
)]
pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| {
let mut settings = $crate::config::CONFIG_VALUES.clone();
$(
settings.set_default($key, $value).unwrap();
)*
match settings.try_into() {
Ok(s) => s,
Err(e) => panic!("Error parsing settings: {}", e),
}
});
};
}

View file

@ -1,7 +1,7 @@
use crate::util::error::*; use crate::config::error::*;
use crate::util::path; use crate::config::path;
use crate::util::source::Source; use crate::config::source::Source;
use crate::util::value::Value; use crate::config::value::Value;
use serde::de::Deserialize; use serde::de::Deserialize;

View file

@ -1,6 +1,6 @@
use crate::util::data::ConfigData; use crate::config::data::ConfigData;
use crate::util::error::*; use crate::config::error::*;
use crate::util::value::{Table, Value, ValueKind}; use crate::config::value::{Table, Value, ValueKind};
use serde::de; use serde::de;
use serde::forward_to_deserialize_any; use serde::forward_to_deserialize_any;

View file

@ -1,9 +1,9 @@
mod source; mod source;
mod toml; mod toml;
use crate::util::error::*; use crate::config::error::*;
use crate::util::source::Source; use crate::config::source::Source;
use crate::util::value::Value; use crate::config::value::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};

View file

@ -1,4 +1,4 @@
use crate::util::value::{Value, ValueKind}; use crate::config::value::{Value, ValueKind};
use toml; use toml;

View file

@ -1,5 +1,5 @@
use crate::util::error::*; use crate::config::error::*;
use crate::util::value::{Value, ValueKind}; use crate::config::value::{Value, ValueKind};
use std::collections::HashMap; use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;

View file

@ -1,6 +1,6 @@
use crate::util::error::*; use crate::config::error::*;
use crate::util::path; use crate::config::path;
use crate::util::value::{Value, ValueKind}; use crate::config::value::{Value, ValueKind};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;

View file

@ -1,4 +1,4 @@
use crate::util::error::*; use crate::config::error::*;
use serde::de::{Deserialize, Deserializer, Visitor}; use serde::de::{Deserialize, Deserializer, Visitor};

View file

@ -2,3 +2,4 @@ mod definition;
pub use definition::{PackageRef, PackageTrait}; pub use definition::{PackageRef, PackageTrait};
pub(crate) mod all; pub(crate) mod all;
pub(crate) mod welcome;

View file

@ -1,7 +1,7 @@
use crate::core::action::add_action; use crate::core::action::add_action;
use crate::core::package::PackageRef; use crate::core::package::{welcome, PackageRef};
use crate::core::theme::all::THEMES; use crate::core::theme::all::THEMES;
use crate::{service, trace}; use crate::{include_files, include_files_service, service, trace};
use std::sync::{LazyLock, RwLock}; use std::sync::{LazyLock, RwLock};
@ -23,8 +23,6 @@ pub fn register_packages(root_package: Option<PackageRef>) {
if let Some(package) = root_package { if let Some(package) = root_package {
add_to_enabled(&mut enabled_list, package); add_to_enabled(&mut enabled_list, package);
} }
// Reverse the order to ensure packages are sorted from none to most dependencies.
enabled_list.reverse();
// Save the final list of enabled packages. // Save the final list of enabled packages.
ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list); ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list);
@ -41,16 +39,14 @@ pub fn register_packages(root_package: Option<PackageRef>) {
fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) { fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
// Check if the package is not already in the enabled list to avoid duplicates. // Check if the package is not already in the enabled list to avoid duplicates.
if !list.iter().any(|p| p.type_id() == package.type_id()) { if !list.iter().any(|p| p.type_id() == package.type_id()) {
// Add the package to the enabled list. // Add the package dependencies in reverse order first.
list.push(package); for d in package.dependencies().iter().rev() {
// Reverse dependencies to add them in correct order (dependencies first).
let mut dependencies = package.dependencies();
dependencies.reverse();
for d in &dependencies {
add_to_enabled(list, *d); add_to_enabled(list, *d);
} }
// Add the package itself to the enabled list.
list.push(package);
// Check if the package has an associated theme to register. // Check if the package has an associated theme to register.
if let Some(theme) = package.theme() { if let Some(theme) = package.theme() {
let mut registered_themes = THEMES.write().unwrap(); let mut registered_themes = THEMES.write().unwrap();
@ -119,8 +115,14 @@ pub fn init_packages() {
// CONFIGURE SERVICES ****************************************************************************** // CONFIGURE SERVICES ******************************************************************************
include_files!(assets);
pub fn configure_services(scfg: &mut service::web::ServiceConfig) { pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
for m in ENABLED_PACKAGES.read().unwrap().iter() { for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.configure_service(scfg); m.configure_service(scfg);
} }
// Default welcome homepage.
scfg.route("/", service::web::get().to(welcome::homepage));
// Default assets.
include_files_service!(scfg, assets => "/");
} }

View file

@ -13,7 +13,7 @@ pub trait PackageTrait: AnyBase + Send + Sync {
} }
fn description(&self) -> L10n { fn description(&self) -> L10n {
L10n::none() L10n::default()
} }
fn theme(&self) -> Option<ThemeRef> { fn theme(&self) -> Option<ThemeRef> {

View file

@ -0,0 +1,116 @@
use crate::html::{html, Markup, PrepareMarkup, StyleSheet};
use crate::locale::L10n;
use crate::response::page::{AssetsOp, ErrorPage, Page, ResultPage};
use crate::{global, service};
pub async fn homepage(request: service::HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_title(L10n::l("welcome_page"))
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::inline("styles", r#"
body {
background-color: #f3d060;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 20px;
}
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0;
}
.container {
padding: 0 16px;
}
.title {
font-size: clamp(3rem, 10vw, 10rem);
letter-spacing: -0.05em;
line-height: 1.2;
margin: 0;
}
.subtitle {
font-size: clamp(1.8rem, 2vw, 3rem);
letter-spacing: -0.02em;
line-height: 1.2;
margin: 0;
}
.powered {
margin: .5em 0 1em;
}
.box-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
gap: 1.5em;
}
.box {
flex: 1 1 280px;
border: 3px solid #25282a;
box-shadow: 5px 5px 0px #25282a;
box-sizing: border-box;
padding: 0 16px;
}
footer {
margin-top: 5em;
font-size: 14px;
font-weight: 500;
color: #a5282c;
}
"#)))
.with_body(PrepareMarkup::With(html! {
div class="wrapper" {
div class="container" {
h1 class="title" { (L10n::l("welcome_title").markup()) }
p class="subtitle" {
(L10n::l("welcome_intro").with_arg("app", format!(
"<span style=\"font-weight: bold;\">{}</span>",
&global::SETTINGS.app.name
)).markup())
}
p class="powered" {
(L10n::l("welcome_powered").with_arg("pagetop", format!(
"<a href=\"{}\" target=\"_blank\">{}</a>",
"https://crates.io/crates/pagetop", "PageTop"
)).markup())
}
h2 { (L10n::l("welcome_page").markup()) }
div class="box-container" {
section class="box" style="background-color: #5eb0e5;" {
h3 {
(L10n::l("welcome_subtitle")
.with_arg("app", &global::SETTINGS.app.name)
.markup())
}
p { (L10n::l("welcome_text1").markup()) }
p { (L10n::l("welcome_text2").markup()) }
}
section class="box" style="background-color: #aee1cd;" {
h3 {
(L10n::l("welcome_pagetop_title").markup())
}
p { (L10n::l("welcome_pagetop_text1").markup()) }
p { (L10n::l("welcome_pagetop_text2").markup()) }
p { (L10n::l("welcome_pagetop_text3").markup()) }
}
section class="box" style="background-color: #ebebe3;" {
h3 {
(L10n::l("welcome_issues_title").markup())
}
p { (L10n::l("welcome_issues_text1").markup()) }
p {
(L10n::l("welcome_issues_text2")
.with_arg("app", &global::SETTINGS.app.name)
.markup())
}
}
}
footer { "[ " (L10n::l("welcome_have_fun").markup()) " ]" }
}
}
}))
.render()
}

View file

@ -1,4 +1,6 @@
use crate::core::theme::ThemeRef; use crate::core::package::PackageTrait;
use crate::core::theme::{ThemeRef, ThemeTrait};
use crate::global;
use std::sync::{LazyLock, RwLock}; use std::sync::{LazyLock, RwLock};
@ -6,7 +8,7 @@ use std::sync::{LazyLock, RwLock};
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new())); pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
/* DEFAULT THEME *********************************************************************************** // DEFAULT THEME ***********************************************************************************
pub struct NoTheme; pub struct NoTheme;
@ -16,10 +18,9 @@ impl PackageTrait for NoTheme {
} }
} }
impl ThemeTrait for NoTheme { impl ThemeTrait for NoTheme {}
}
pub static THEME_DEFAULT: LazyLock<ThemeRef> = pub static DEFAULT_THEME: LazyLock<ThemeRef> =
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) { LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
Some(theme) => theme, Some(theme) => theme,
None => &NoTheme, None => &NoTheme,
@ -39,4 +40,3 @@ pub fn theme_by_short_name(short_name: &str) -> Option<ThemeRef> {
_ => None, _ => None,
} }
} }
*/

View file

@ -0,0 +1,82 @@
use crate::core::package::PackageTrait;
use crate::html::{html, PrepareMarkup};
use crate::locale::L10n;
use crate::response::page::Page;
use crate::{global, service};
pub type ThemeRef = &'static dyn ThemeTrait;
/// Los temas deben implementar este "trait".
pub trait ThemeTrait: PackageTrait + Send + Sync {
/*
#[rustfmt::skip]
fn regions(&self) -> Vec<(&'static str, L10n)> {
vec![
("header", L10n::l("header")),
("pagetop", L10n::l("pagetop")),
("sidebar_left", L10n::l("sidebar_left")),
("content", L10n::l("content")),
("sidebar_right", L10n::l("sidebar_right")),
("footer", L10n::l("footer")),
]
} */
fn prepare_page_head(&self, page: &mut Page) -> PrepareMarkup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
PrepareMarkup::With(html! {
head {
meta charset="utf-8";
@if let Some(title) = page.title() {
title { (global::SETTINGS.app.name) (" | ") (title) }
} @else {
title { (global::SETTINGS.app.name) }
}
@if let Some(description) = page.description() {
meta name="description" content=(description);
}
meta name="viewport" content=(viewport);
@for (name, content) in page.metadata() {
meta name=(name) content=(content) {}
}
meta http-equiv="X-UA-Compatible" content="IE=edge";
@for (property, content) in page.properties() {
meta property=(property) content=(content) {}
}
(page.context().prepare_assets())
}
})
}
fn prepare_page_body(&self, page: &mut Page) -> PrepareMarkup {
PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
(page.body_content().render())
}
})
}
fn error_403(&self, request: service::HttpRequest) -> Page {
Page::new(request)
.with_title(L10n::n("Error FORBIDDEN"))
.with_body(PrepareMarkup::With(html! {
div {
h1 { ("FORBIDDEN ACCESS") }
}
}))
}
fn error_404(&self, request: service::HttpRequest) -> Page {
Page::new(request)
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
.with_body(PrepareMarkup::With(html! {
div {
h1 { ("RESOURCE NOT FOUND") }
}
}))
}
}

View file

@ -1,10 +1,10 @@
//! Global settings. //! Global settings.
use crate::static_config; use crate::include_config;
use serde::Deserialize; use serde::Deserialize;
static_config!(SETTINGS: Settings => [ include_config!(SETTINGS: Settings => [
// [app] // [app]
"app.name" => "My App", "app.name" => "My App",
"app.description" => "Developed with the amazing PageTop framework.", "app.description" => "Developed with the amazing PageTop framework.",

47
pagetop/src/html.rs Normal file
View file

@ -0,0 +1,47 @@
//! HTML in code.
mod maud;
pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE};
mod assets;
pub use assets::favicon::Favicon;
pub use assets::javascript::JavaScript;
pub use assets::stylesheet::{StyleSheet, TargetMedia};
pub(crate) use assets::Assets;
mod opt_id;
pub use opt_id::OptionId;
mod opt_name;
pub use opt_name::OptionName;
mod opt_string;
pub use opt_string::OptionString;
mod opt_translated;
pub use opt_translated::OptionTranslated;
mod opt_classes;
pub use opt_classes::{ClassesOp, OptionClasses};
pub mod unit;
use crate::AutoDefault;
#[derive(AutoDefault)]
pub enum PrepareMarkup {
#[default]
None,
Escaped(String),
With(Markup),
}
impl PrepareMarkup {
pub fn render(&self) -> Markup {
match self {
PrepareMarkup::None => html! {},
PrepareMarkup::Escaped(string) => html! { (PreEscaped(string)) },
PrepareMarkup::With(markup) => html! { (markup) },
}
}
}

View file

@ -0,0 +1,53 @@
pub mod favicon;
pub mod javascript;
pub mod stylesheet;
use crate::html::{html, Markup};
use crate::{AutoDefault, Weight};
pub trait AssetsTrait {
fn name(&self) -> &String;
fn weight(&self) -> Weight;
fn prepare(&self) -> Markup;
}
#[derive(AutoDefault)]
pub(crate) struct Assets<T>(Vec<T>);
impl<T: AssetsTrait> Assets<T> {
pub fn new() -> Self {
Assets::<T>(Vec::<T>::new())
}
pub fn add(&mut self, asset: T) -> &mut Self {
match self.0.iter().position(|x| x.name() == asset.name()) {
Some(index) => {
if self.0[index].weight() > asset.weight() {
self.0.remove(index);
self.0.push(asset);
}
}
_ => self.0.push(asset),
};
self
}
pub fn remove(&mut self, name: &'static str) -> &mut Self {
if let Some(index) = self.0.iter().position(|x| x.name() == name) {
self.0.remove(index);
};
self
}
pub fn prepare(&mut self) -> Markup {
let assets = &mut self.0;
assets.sort_by_key(AssetsTrait::weight);
html! {
@for a in assets {
(a.prepare())
}
}
}
}

View file

@ -0,0 +1,93 @@
use crate::html::{html, Markup};
use crate::AutoDefault;
#[derive(AutoDefault)]
pub struct Favicon(Vec<Markup>);
impl Favicon {
pub fn new() -> Self {
Favicon::default()
}
// Favicon BUILDER.
pub fn with_icon(self, image: &str) -> Self {
self.add_icon_item("icon", image, None, None)
}
pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self {
self.add_icon_item("icon", image, Some(sizes), None)
}
pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self {
self.add_icon_item("apple-touch-icon", image, Some(sizes), None)
}
pub fn with_mask_icon(self, image: &str, color: &str) -> Self {
self.add_icon_item("mask-icon", image, None, Some(color))
}
pub fn with_manifest(self, file: &str) -> Self {
self.add_icon_item("manifest", file, None, None)
}
pub fn with_theme_color(mut self, color: &str) -> Self {
self.0.push(html! {
meta name="theme-color" content=(color);
});
self
}
pub fn with_ms_tile_color(mut self, color: &str) -> Self {
self.0.push(html! {
meta name="msapplication-TileColor" content=(color);
});
self
}
pub fn with_ms_tile_image(mut self, image: &str) -> Self {
self.0.push(html! {
meta name="msapplication-TileImage" content=(image);
});
self
}
fn add_icon_item(
mut self,
icon_rel: &str,
icon_source: &str,
icon_sizes: Option<&str>,
icon_color: Option<&str>,
) -> Self {
let icon_type = match icon_source.rfind('.') {
Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
".gif" => Some("image/gif"),
".ico" => Some("image/x-icon"),
".jpg" => Some("image/jpg"),
".png" => Some("image/png"),
".svg" => Some("image/svg+xml"),
_ => None,
},
_ => None,
};
self.0.push(html! {
link
rel=(icon_rel)
type=[(icon_type)]
sizes=[(icon_sizes)]
color=[(icon_color)]
href=(icon_source);
});
self
}
// Favicon PREPARE.
pub(crate) fn prepare(&self) -> Markup {
html! {
@for item in &self.0 {
(item)
}
}
}
}

View file

@ -0,0 +1,111 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup};
use crate::{concat_string, AutoDefault, Weight};
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Defer(String),
Async(String),
Inline(String, String),
OnLoad(String, String),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct JavaScript {
source : Source,
prefix : &'static str,
version: &'static str,
weight : Weight,
}
impl AssetsTrait for JavaScript {
fn name(&self) -> &String {
match &self.source {
Source::From(path) => path,
Source::Defer(path) => path,
Source::Async(path) => path,
Source::Inline(name, _) => name,
Source::OnLoad(name, _) => name,
}
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) {};
},
Source::Defer(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) defer {};
},
Source::Async(path) => html! {
script src=(concat_string!(path, self.prefix, self.version)) async {};
},
Source::Inline(_, code) => html! {
script { (code) };
},
Source::OnLoad(_, code) => html! { (concat_string!(
"document.addEventListener('DOMContentLoaded',function(){",
code,
"});"
)) },
}
}
}
impl JavaScript {
pub fn from(path: impl Into<String>) -> Self {
JavaScript {
source: Source::From(path.into()),
..Default::default()
}
}
pub fn defer(path: impl Into<String>) -> Self {
JavaScript {
source: Source::Defer(path.into()),
..Default::default()
}
}
pub fn asynchronous(path: impl Into<String>) -> Self {
JavaScript {
source: Source::Async(path.into()),
..Default::default()
}
}
pub fn inline(name: impl Into<String>, script: impl Into<String>) -> Self {
JavaScript {
source: Source::Inline(name.into(), script.into()),
..Default::default()
}
}
pub fn on_load(name: impl Into<String>, script: impl Into<String>) -> Self {
JavaScript {
source: Source::OnLoad(name.into(), script.into()),
..Default::default()
}
}
pub fn with_version(mut self, version: &'static str) -> Self {
(self.prefix, self.version) = if version.is_empty() {
("", "")
} else {
("?v=", version)
};
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
}

View file

@ -0,0 +1,95 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup, PreEscaped};
use crate::{concat_string, AutoDefault, Weight};
#[derive(AutoDefault)]
enum Source {
#[default]
From(String),
Inline(String, String),
}
pub enum TargetMedia {
Default,
Print,
Screen,
Speech,
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct StyleSheet {
source : Source,
prefix : &'static str,
version: &'static str,
media : Option<&'static str>,
weight : Weight,
}
impl AssetsTrait for StyleSheet {
fn name(&self) -> &String {
match &self.source {
Source::From(path) => path,
Source::Inline(name, _) => name,
}
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
match &self.source {
Source::From(path) => html! {
link
rel="stylesheet"
href=(concat_string!(path, self.prefix, self.version))
media=[self.media];
},
Source::Inline(_, code) => html! {
style { (PreEscaped(code)) };
},
}
}
}
impl StyleSheet {
pub fn from(path: impl Into<String>) -> Self {
StyleSheet {
source: Source::From(path.into()),
..Default::default()
}
}
pub fn inline(name: impl Into<String>, styles: impl Into<String>) -> Self {
StyleSheet {
source: Source::Inline(name.into(), styles.into()),
..Default::default()
}
}
pub fn with_version(mut self, version: &'static str) -> Self {
(self.prefix, self.version) = if version.is_empty() {
("", "")
} else {
("?v=", version)
};
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[rustfmt::skip]
pub fn for_media(mut self, media: &TargetMedia) -> Self {
self.media = match media {
TargetMedia::Default => None,
TargetMedia::Print => Some("print"),
TargetMedia::Screen => Some("screen"),
TargetMedia::Speech => Some("speech"),
};
self
}
}

View file

@ -0,0 +1,111 @@
//! **OptionClasses** implements a *helper* for dynamically adding class names to components.
//!
//! This *helper* differentiates between default classes (generally associated with styles provided
//! by the theme) and user classes (for customizing components based on application styles).
//!
//! Classes can be added using [Add]. Operations to [Remove], [Replace] or [Toggle] a class, as well
//! as [Clear] all classes, are also provided.
//!
//! **OptionClasses** assumes that the order of the classes is irrelevant
//! (<https://stackoverflow.com/a/1321712>), and duplicate classes will not be allowed.
use crate::{fn_builder, AutoDefault};
pub enum ClassesOp {
Add,
Prepend,
Remove,
Replace(String),
Toggle,
Set,
}
#[derive(AutoDefault)]
pub struct OptionClasses(Vec<String>);
impl OptionClasses {
pub fn new(classes: impl Into<String>) -> Self {
OptionClasses::default().with_value(ClassesOp::Prepend, classes)
}
// OptionClasses BUILDER.
#[fn_builder]
pub fn set_value(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
let classes: String = classes.into();
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
if classes.is_empty() {
return self;
}
match op {
ClassesOp::Add => {
self.add(&classes, self.0.len());
}
ClassesOp::Prepend => {
self.add(&classes, 0);
}
ClassesOp::Remove => {
for class in classes {
self.0.retain(|c| c.ne(&class.to_string()));
}
}
ClassesOp::Replace(classes_to_replace) => {
let mut pos = self.0.len();
let replace: Vec<&str> = classes_to_replace.split_ascii_whitespace().collect();
for class in replace {
if let Some(replace_pos) = self.0.iter().position(|c| c.eq(class)) {
self.0.remove(replace_pos);
if pos > replace_pos {
pos = replace_pos;
}
}
}
self.add(&classes, pos);
}
ClassesOp::Toggle => {
for class in classes {
if !class.is_empty() {
if let Some(pos) = self.0.iter().position(|c| c.eq(class)) {
self.0.remove(pos);
} else {
self.0.push(class.to_string());
}
}
}
}
ClassesOp::Set => {
self.0.clear();
self.add(&classes, 0);
}
}
self
}
#[inline]
fn add(&mut self, classes: &[&str], mut pos: usize) {
for &class in classes {
if !class.is_empty() && !self.0.iter().any(|c| c == class) {
self.0.insert(pos, class.to_string());
pos += 1;
}
}
}
// OptionClasses GETTERS.
pub fn get(&self) -> Option<String> {
if self.0.is_empty() {
None
} else {
Some(self.0.join(" "))
}
}
pub fn contains(&self, class: impl Into<String>) -> bool {
let class: String = class.into();
self.0.iter().any(|c| c.eq(&class))
}
}

View file

@ -0,0 +1,29 @@
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionId(Option<String>);
impl OptionId {
pub fn new(value: impl Into<String>) -> Self {
OptionId::default().with_value(value)
}
// OptionId BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().replace(' ', "_"));
self
}
// OptionId GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -0,0 +1,29 @@
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionName(Option<String>);
impl OptionName {
pub fn new(value: impl Into<String>) -> Self {
OptionName::default().with_value(value)
}
// OptionName BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().replace(' ', "_"));
self
}
// OptionName GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -0,0 +1,29 @@
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionString(Option<String>);
impl OptionString {
pub fn new(value: impl Into<String>) -> Self {
OptionString::default().with_value(value)
}
// OptionString BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().to_owned());
self
}
// OptionString GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

View file

@ -0,0 +1,30 @@
use crate::html::Markup;
use crate::locale::{L10n, LanguageIdentifier};
use crate::{fn_builder, AutoDefault};
#[derive(AutoDefault)]
pub struct OptionTranslated(L10n);
impl OptionTranslated {
pub fn new(value: L10n) -> Self {
OptionTranslated(value)
}
// OptionTranslated BUILDER.
#[fn_builder]
pub fn set_value(&mut self, value: L10n) -> &mut Self {
self.0 = value;
self
}
// OptionTranslated GETTERS.
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
self.0.using(langid)
}
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
self.0.escaped(langid)
}
}

56
pagetop/src/html/unit.rs Normal file
View file

@ -0,0 +1,56 @@
use crate::AutoDefault;
use std::fmt;
// About pixels: Pixels (px) are relative to the viewing device. For low-dpi devices, 1px is one
// device pixel (dot) of the display. For printers and high resolution screens 1px implies multiple
// device pixels.
// About em: 2em means 2 times the size of the current font. The em and rem units are practical in
// creating perfectly scalable layout!
// About viewport: If the browser window size is 50cm wide, 1vw = 0.5cm.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub enum Value {
#[default]
None,
Auto,
Cm(isize), // Centimeters.
In(isize), // Inches (1in = 96px = 2.54cm).
Mm(isize), // Millimeters.
Pc(isize), // Picas (1pc = 12pt).
Pt(isize), // Points (1pt = 1/72 of 1in).
Px(isize), // Pixels (1px = 1/96th of 1in).
RelEm(f32), // Relative to the font-size of the element.
RelPct(f32), // Percentage relative to the parent element.
RelRem(f32), // Relative to font-size of the root element.
RelVh(f32), // Relative to 1% of the height of the viewport.
RelVw(f32), // Relative to 1% of the value of the viewport.
}
#[rustfmt::skip]
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::None => write!(f, ""),
Value::Auto => write!(f, "auto"),
// Absolute value.
Value::Cm(av) => write!(f, "{av}cm"),
Value::In(av) => write!(f, "{av}in"),
Value::Mm(av) => write!(f, "{av}mm"),
Value::Pc(av) => write!(f, "{av}pc"),
Value::Pt(av) => write!(f, "{av}pt"),
Value::Px(av) => write!(f, "{av}px"),
// Relative value.
Value::RelEm(rv) => write!(f, "{rv}em"),
Value::RelPct(rv) => write!(f, "{rv}%"),
Value::RelRem(rv) => write!(f, "{rv}rem"),
Value::RelVh(rv) => write!(f, "{rv}vh"),
Value::RelVw(rv) => write!(f, "{rv}vw"),
}
}
}

View file

@ -42,7 +42,7 @@
//! //!
//! async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> { //! async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
//! Page::new(request) //! Page::new(request)
//! .with_component(Html::with(html! { h1 { "Hello World!" } })) //! .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
//! .render() //! .render()
//! } //! }
//! //!
@ -53,7 +53,7 @@
//! ``` //! ```
//! This program implements a package named `HelloWorld` with one service that returns a web page //! This program implements a package named `HelloWorld` with one service that returns a web page
//! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using //! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using
//! the [default configuration settings](`config::Server`)). You can find this code in the `PageTop` //! the [default configuration settings](`global::Server`)). You can find this code in the `PageTop`
//! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples). //! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples).
//! //!
//! # 🧩 Dependency Management //! # 🧩 Dependency Management
@ -79,7 +79,7 @@ pub use concat_string::concat_string;
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers. /// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
pub use paste::paste; pub use paste::paste;
pub use pagetop_macros::{main, test, AutoDefault}; pub use pagetop_macros::{fn_builder, html, main, test, AutoDefault};
pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>; pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;
@ -91,6 +91,8 @@ pub type Weight = i8;
// Useful functions and macros. // Useful functions and macros.
pub mod util; pub mod util;
// Load configuration settings.
pub mod config;
// Application tracing and event logging. // Application tracing and event logging.
pub mod trace; pub mod trace;
// HTML in code. // HTML in code.

View file

@ -67,13 +67,13 @@
//! # How to apply localization in your code //! # How to apply localization in your code
//! //!
//! Once you have created your FTL resource directory, use the //! Once you have created your FTL resource directory, use the
//! [`static_locales!`](crate::static_locales) macro to integrate them into your module or //! [`include_locales!`](crate::include_locales) macro to integrate them into your module or
//! application. If your resources are located in the `"src/locale"` directory, simply declare: //! application. If your resources are located in the `"src/locale"` directory, simply declare:
//! //!
//! ``` //! ```
//! use pagetop::prelude::*; //! use pagetop::prelude::*;
//! //!
//! static_locales!(LOCALES_SAMPLE); //! include_locales!(LOCALES_SAMPLE);
//! ``` //! ```
//! //!
//! But if they are in another directory, then you can use: //! But if they are in another directory, then you can use:
@ -81,14 +81,15 @@
//! ``` //! ```
//! use pagetop::prelude::*; //! use pagetop::prelude::*;
//! //!
//! static_locales!(LOCALES_SAMPLE in "path/to/locale"); //! include_locales!(LOCALES_SAMPLE from "path/to/locale");
//! ``` //! ```
use crate::html::{Markup, PreEscaped};
use crate::{global, kv, AutoDefault}; use crate::{global, kv, AutoDefault};
pub use fluent_bundle::FluentValue; pub use fluent_bundle::FluentValue;
pub use fluent_templates; pub use fluent_templates;
pub use unic_langid::LanguageIdentifier; pub use unic_langid::{CharacterDirection, LanguageIdentifier};
use fluent_templates::Loader; use fluent_templates::Loader;
use fluent_templates::StaticLoader as Locales; use fluent_templates::StaticLoader as Locales;
@ -100,43 +101,62 @@ use std::sync::LazyLock;
use std::fmt; use std::fmt;
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`] /// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
/// and human-readable names. /// and locale key names.
static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| { static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
kv![ kv![
"en" => (langid!("en-US"), "English"), "en" => ( langid!("en-US"), "english" ),
"en-GB" => (langid!("en-GB"), "English (British)"), "en-GB" => ( langid!("en-GB"), "english_british" ),
"en-US" => (langid!("en-US"), "English (United States)"), "en-US" => ( langid!("en-US"), "english_united_states" ),
"es" => (langid!("es-ES"), "Spanish"), "es" => ( langid!("es-ES"), "spanish" ),
"es-ES" => (langid!("es-ES"), "Spanish (Spain)"), "es-ES" => ( langid!("es-ES"), "spanish_spain" ),
] ]
}); });
pub static LANGID_FALLBACK: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US")); static FALLBACK: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
/// Sets the application's default /// Sets the application's default
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) /// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
/// through `SETTINGS.app.language`. /// through `SETTINGS.app.language`.
pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK)); LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&FALLBACK));
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> { pub enum LangError {
EmptyLang,
UnknownLang(String),
}
impl fmt::Display for LangError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LangError::EmptyLang => write!(f, "The language identifier is empty."),
LangError::UnknownLang(lang) => write!(f, "Unknown language identifier: {lang}"),
}
}
}
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, LangError> {
let language = language.into(); let language = language.into();
if language.is_empty() { if language.is_empty() {
return Ok(&LANGID_FALLBACK); return Err(LangError::EmptyLang);
} }
LANGUAGES // Attempt to match the full language code (e.g., "es-MX").
.get(&language) if let Some(langid) = LANGUAGES.get(&language).map(|(langid, _)| langid) {
.map(|(langid, _)| langid) return Ok(langid);
.ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\".")) }
// Fallback to the base language if no sublocale is found (e.g., "es").
if let Some((base_lang, _)) = language.split_once('-') {
if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) {
return Ok(langid);
}
}
Err(LangError::UnknownLang(language))
} }
#[macro_export] #[macro_export]
/// Defines a set of localization elements and local translation texts, removing Unicode isolating /// Defines a set of localization elements and local translation texts, removing Unicode isolating
/// marks around arguments to improve readability and compatibility in certain rendering contexts. /// marks around arguments to improve readability and compatibility in certain rendering contexts.
macro_rules! static_locales { macro_rules! include_locales {
( $LOCALES:ident $(, $core_locales:literal)? ) => { ( $LOCALES:ident $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! { $crate::locale::fluent_templates::static_loader! {
static $LOCALES = { static $LOCALES = {
@ -148,7 +168,7 @@ macro_rules! static_locales {
}; };
} }
}; };
( $LOCALES:ident in $dir_locales:literal $(, $core_locales:literal)? ) => { ( $LOCALES:ident from $dir_locales:literal $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! { $crate::locale::fluent_templates::static_loader! {
static $LOCALES = { static $LOCALES = {
locales: $dir_locales, locales: $dir_locales,
@ -161,7 +181,7 @@ macro_rules! static_locales {
}; };
} }
static_locales!(LOCALES_PAGETOP); include_locales!(LOCALES_PAGETOP);
#[derive(AutoDefault)] #[derive(AutoDefault)]
enum L10nOp { enum L10nOp {
@ -174,18 +194,15 @@ enum L10nOp {
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct L10n { pub struct L10n {
op: L10nOp, op: L10nOp,
locales: Option<&'static Locales>, #[default(&LOCALES_PAGETOP)]
locales: &'static Locales,
args: HashMap<String, FluentValue<'static>>, args: HashMap<String, FluentValue<'static>>,
} }
impl L10n { impl L10n {
pub fn none() -> Self {
L10n::default()
}
pub fn n(text: impl Into<String>) -> Self { pub fn n(text: impl Into<String>) -> Self {
L10n { L10n {
op: L10nOp::Text(text.into()), op: L10nOp::Text(text.into().to_string()),
..Default::default() ..Default::default()
} }
} }
@ -193,7 +210,6 @@ impl L10n {
pub fn l(key: impl Into<String>) -> Self { pub fn l(key: impl Into<String>) -> Self {
L10n { L10n {
op: L10nOp::Translate(key.into()), op: L10nOp::Translate(key.into()),
locales: Some(&LOCALES_PAGETOP),
..Default::default() ..Default::default()
} }
} }
@ -201,68 +217,60 @@ impl L10n {
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self { pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
L10n { L10n {
op: L10nOp::Translate(key.into()), op: L10nOp::Translate(key.into()),
locales: Some(locales), locales,
..Default::default() ..Default::default()
} }
} }
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self { pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
self.args let value = FluentValue::from(value.into());
.insert(arg.into(), FluentValue::from(value.into())); self.args.insert(arg.into(), value);
self self
} }
pub fn with_args(mut self, args: HashMap<String, String>) -> Self {
for (k, v) in args {
self.args.insert(k, FluentValue::from(v));
}
self
}
pub fn get(&self) -> Option<String> {
self.using(&DEFAULT_LANGID)
}
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> { pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
match &self.op { match &self.op {
L10nOp::None => None, L10nOp::None => None,
L10nOp::Text(text) => Some(text.to_owned()), L10nOp::Text(text) => Some(text.to_owned()),
L10nOp::Translate(key) => match self.locales { L10nOp::Translate(key) => {
Some(locales) => {
if self.args.is_empty() { if self.args.is_empty() {
locales.try_lookup(langid, key) self.locales.try_lookup(langid, key)
} else { } else {
locales.try_lookup_with_args(langid, key, &self.args) self.locales.try_lookup_with_args(langid, key, &self.args)
} }
} }
None => None,
},
} }
} }
/// Escapes translated text using the default language identifier.
pub fn markup(&self) -> Markup {
PreEscaped(self.get().unwrap_or_default())
}
/// Escapes translated text using the specified language identifier.
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
PreEscaped(self.using(langid).unwrap_or_default())
}
} }
impl fmt::Display for L10n { impl fmt::Display for L10n {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.op { let content = match &self.op {
L10nOp::None => write!(f, ""), L10nOp::None => "".to_string(),
L10nOp::Text(text) => write!(f, "{text}"), L10nOp::Text(text) => text.clone(),
L10nOp::Translate(key) => { L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("No <{}>", key)),
if let Some(locales) = self.locales { };
write!( write!(f, "{content}")
f,
"{}",
if self.args.is_empty() {
locales.lookup(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &LANGID_DEFAULT,
},
key,
)
} else {
locales.lookup_with_args(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &LANGID_DEFAULT,
},
key,
&self.args,
)
}
)
} else {
write!(f, "Unknown localization {key}")
}
}
}
} }
} }

View file

@ -0,0 +1,5 @@
english = English
english_british = English (British)
english_united_states = English (United States)
spanish = Spanish
spanish_spain = Spanish (Spain)

View file

@ -0,0 +1,20 @@
welcome_title = Hello world!
welcome_intro = Verifying the installation of { $app }.
welcome_powered = A web solution powered by { $pagetop }.
welcome_page = Welcome Page
welcome_subtitle = Are you a { $app } user?
welcome_text1 = If you don't know what this page is about, this probably means that the site is either experiencing problems or is undergoing routine maintenance.
welcome_text2 = If the issue persists, please contact your system administrator for assistance.
welcome_pagetop_title = About PageTop
welcome_pagetop_text1 = If you can read this page, it means that the <strong>PageTop</strong> server is working properly, but has not yet been configured.
welcome_pagetop_text2 = <strong>PageTop</strong> is a <a href="https://www.rust-lang.org" target="_blank">Rust</a>-based web development framework designed to create modular, extensible, and configurable web solutions.
welcome_pagetop_text3 = For detailed information, please visit the <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">official technical documentation</a>.
welcome_issues_title = Reporting Issues
welcome_issues_text1 = To report any issues with <strong>PageTop</strong>, please use <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub</a>. However, check the existing error reports to avoid duplicates.
welcome_issues_text2 = For issues specific to <strong>{ $app }</strong>, please refer to its official repository or support channel, rather than directly to <strong>PageTop</strong>.
welcome_have_fun = Coding is creating

View file

@ -0,0 +1,5 @@
english = Inglés
english_british = Inglés (Gran Bretaña)
english_united_states = Inglés (Estados Unidos)
spanish = Español
spanish_spain = Español (España)

View file

@ -0,0 +1,20 @@
welcome_title = ¡Hola mundo!
welcome_intro = Verificando la instalación de { $app }.
welcome_powered = Una solución web creada con { $pagetop }.
welcome_page = Página de Bienvenida
welcome_subtitle = ¿Eres usuario de { $app }?
welcome_text1 = Si no sabes por qué se muestra esta página probablemente significa que el sitio está experimentando problemas o está pasando por un mantenimiento de rutina.
welcome_text2 = Si el problema persiste, por favor póngase en contacto con el administrador del sistema.
welcome_pagetop_title = Sobre PageTop
welcome_pagetop_text1 = Si puedes leer esta página significa que el servidor <strong>PageTop</strong> funciona correctamente, pero aún no se ha configurado.
welcome_pagetop_text2 = <strong>PageTop</strong> es un entorno de desarrollo web basado en <a href="https://www.rust-lang.org/es" target="_blank">Rust</a>, diseñado para crear soluciones web modulares, extensibles y configurables.
welcome_pagetop_text3 = Para más información visita la <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">documentación técnica oficial</a>.
welcome_issues_title = Informando Problemas
welcome_issues_text1 = Para comunicar cualquier problema con <strong>PageTop</strong> utiliza <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub</a>. No obstante, comprueba los informes de errores ya existentes para evitar duplicados.
welcome_issues_text2 = Si son fallos específicos de <strong>{ $app }</strong>, por favor acude a su repositorio oficial o canal de soporte, y no al de <strong>PageTop</strong> directamente.
welcome_have_fun = Programar es crear

View file

@ -2,18 +2,20 @@
// RE-EXPORTED. // RE-EXPORTED.
pub use crate::{concat_string, main, paste, test}; pub use crate::{concat_string, fn_builder, html, main, paste, test};
pub use crate::{AutoDefault, StaticResources, TypeId, Weight}; pub use crate::{AutoDefault, StaticResources, TypeId, Weight};
// MACROS. // MACROS.
// crate::util // crate::util
pub use crate::{kv, static_config}; pub use crate::kv;
// crate::config
pub use crate::include_config;
// crate::locale // crate::locale
pub use crate::static_locales; pub use crate::include_locales;
// crate::service // crate::service
pub use crate::{static_files, static_files_service}; pub use crate::{include_files, include_files_service};
// crate::core::action // crate::core::action
pub use crate::actions; pub use crate::actions;
@ -36,7 +38,7 @@ pub use crate::core::action::*;
pub use crate::core::package::*; pub use crate::core::package::*;
pub use crate::core::theme::*; pub use crate::core::theme::*;
pub use crate::response::{json::*, redirect::*, ResponseError}; pub use crate::response::{json::*, page::*, redirect::*, ResponseError};
pub use crate::global; pub use crate::global;

View file

@ -2,6 +2,8 @@
pub use actix_web::ResponseError; pub use actix_web::ResponseError;
pub mod page;
pub mod json; pub mod json;
pub mod redirect; pub mod redirect;

View file

@ -0,0 +1,195 @@
mod error;
pub use error::ErrorPage;
mod context;
pub use context::{AssetsOp, ContextPage /*, ParamError*/};
/*
pub type FnContextualPath = fn(cx: &Context) -> &str;
*/
use crate::fn_builder;
use crate::html::{html, Markup, PrepareMarkup, DOCTYPE};
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
use crate::locale::L10n;
use crate::service::HttpRequest;
pub use actix_web::Result as ResultPage;
use unic_langid::CharacterDirection;
#[rustfmt::skip]
pub struct Page {
title : OptionTranslated,
description : OptionTranslated,
metadata : Vec<(&'static str, &'static str)>,
properties : Vec<(&'static str, &'static str)>,
context : ContextPage,
body_id : OptionId,
body_classes: OptionClasses,
body_content: PrepareMarkup,
}
impl Page {
#[rustfmt::skip]
pub fn new(request: HttpRequest) -> Self {
Page {
title : OptionTranslated::default(),
description : OptionTranslated::default(),
metadata : Vec::default(),
properties : Vec::default(),
context : ContextPage::new(request),
body_id : OptionId::default(),
body_classes: OptionClasses::default(),
body_content: PrepareMarkup::default(),
}
}
// Page BUILDER.
#[fn_builder]
pub fn set_title(&mut self, title: L10n) -> &mut Self {
self.title.set_value(title);
self
}
#[fn_builder]
pub fn set_description(&mut self, description: L10n) -> &mut Self {
self.description.set_value(description);
self
}
#[fn_builder]
pub fn set_metadata(&mut self, name: &'static str, content: &'static str) -> &mut Self {
self.metadata.push((name, content));
self
}
#[fn_builder]
pub fn set_property(&mut self, property: &'static str, content: &'static str) -> &mut Self {
self.metadata.push((property, content));
self
}
#[fn_builder]
pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self {
self.context.set_assets(op);
self
}
#[fn_builder]
pub fn set_body_id(&mut self, id: impl Into<String>) -> &mut Self {
self.body_id.set_value(id);
self
}
#[fn_builder]
pub fn set_body_classes(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
self.body_classes.set_value(op, classes);
self
}
#[fn_builder]
pub fn set_body(&mut self, content: PrepareMarkup) -> &mut Self {
self.body_content = content;
self
}
/*
#[fn_builder]
pub fn set_layout(&mut self, layout: &'static str) -> &mut Self {
self.context.set_assets(AssetsOp::Layout(layout));
self
}
#[fn_builder]
pub fn set_regions(&mut self, region: &'static str, op: AnyOp) -> &mut Self {
self.context.set_regions(region, op);
self
}
pub fn with_component(mut self, component: impl ComponentTrait) -> Self {
self.context
.set_regions("content", AnyOp::Add(AnyComponent::with(component)));
self
}
pub fn with_component_in(
mut self,
region: &'static str,
component: impl ComponentTrait,
) -> Self {
self.context
.set_regions(region, AnyOp::Add(AnyComponent::with(component)));
self
}
*/
// Page GETTERS.
pub fn title(&mut self) -> Option<String> {
self.title.using(self.context.langid())
}
pub fn description(&mut self) -> Option<String> {
self.description.using(self.context.langid())
}
pub fn metadata(&self) -> &Vec<(&str, &str)> {
&self.metadata
}
pub fn properties(&self) -> &Vec<(&str, &str)> {
&self.properties
}
pub fn context(&mut self) -> &mut ContextPage {
&mut self.context
}
pub fn body_id(&self) -> &OptionId {
&self.body_id
}
pub fn body_classes(&self) -> &OptionClasses {
&self.body_classes
}
pub fn body_content(&self) -> &PrepareMarkup {
&self.body_content
}
// Page RENDER.
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
// Theme operations before preparing the page body.
//self.context.theme().before_prepare_body(self);
// Packages actions before preparing the page body.
//action::page::BeforePrepareBody::dispatch(self);
// Prepare page body.
let body = self.context.theme().prepare_page_body(self);
// Theme operations after preparing the page body.
//self.context.theme().after_prepare_body(self);
// Packages actions after preparing the page body.
//action::page::AfterPrepareBody::dispatch(self);
// Prepare page head.
let head = self.context.theme().prepare_page_head(self);
// Render the page.
let lang = self.context.langid().language.as_str();
let dir = match self.context.langid().character_direction() {
CharacterDirection::LTR => "ltr",
CharacterDirection::RTL => "rtl",
CharacterDirection::TTB => "auto",
};
Ok(html! {
(DOCTYPE)
html lang=(lang) dir=(dir) {
(head.render())
(body.render())
}
})
}
}

Some files were not shown because too many files have changed in this diff Show more