Compare commits

..

10 commits

107 changed files with 5511 additions and 846 deletions

View file

@ -1,58 +1,53 @@
# 🔃 Dependencies
PageTop is developed in 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 [Rust ecosystem](https://lib.rs), such as:
* [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.
PageTop is developed using the [Rust programming language](https://www.rust-lang.org/) and stands on
the shoulders of giants, leveraging some of the most stable and renowned libraries (*crates*) from
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 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
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),
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.
* [**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
[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.
* **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.
[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)**: 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
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
[FIGlet](http://www.figlet.org) characters. The fonts included in `src/app` are:
presentation banner in the terminal featuring the application's name in
[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*
* [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*
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) by *Ryan Youck*
# 📰 Templates
* The default welcome homepage design is based on the
[Zinc](https://themewagon.com/themes/free-bootstrap-5-html5-business-website-template-zinc)
template created by [inovatik](https://inovatik.com/) and distributed by
[ThemeWagon](https://themewagon.com).
The default welcome homepage design is inspired by a tutorial for creating a unique
[Neobrutalism](https://www.codewithfaraz.com/content/109/creating-a-unique-neobrutalism-portfolio-page-with-html-css-and-javascript)
portfolio page by [Faraz](https://www.codewithfaraz.com/).
# 🎨 Icon
"The creature" smiling is a fun creation by [Webalys](https://www.iconfinder.com/webalys). It can be
found in their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection available on
"The Creature" smiling is a playful creation by [Webalys](https://www.iconfinder.com/webalys). It is
part of their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection, available on
[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-macros",
# PageTop
"pagetop",
# Packages
"packages/pagetop",
"packages/pagetop-aliner",
"packages/pagetop-bootsier",
"packages/pagetop-seaorm",
# App
"packages/drust",
# Examples
# "examples/app-basic",
# "examples/hello-world",
# "examples/hello-name",
"drust",
]
[workspace.package]
@ -34,7 +32,9 @@ static-files = "0.2.4"
pagetop-build = { version = "0.0", path = "helpers/pagetop-build" }
pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" }
# PageTop
pagetop = { version = "0.0", path = "pagetop" }
# Packages
pagetop = { version = "0.0", path = "packages/pagetop" }
pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" }
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> {
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello World!" } }))
.with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
.render()
}

View file

@ -1,5 +1,6 @@
[app]
name = "Samples"
name = "Drust"
description = "A modern web Content Management System to share your world."
[log]
tracing = "Debug"
[database]
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/)
//! to provide an easy way to embed static files or compiled SCSS files into your binary at compile
//! Provide an easy way to embed static files or compiled SCSS files into your binary at compile
//! time.
//!
//! ## Adding to your project
@ -24,9 +23,9 @@
//! use pagetop_build::StaticFilesBundle;
//!
//! fn main() -> std::io::Result<()> {
//! StaticFilesBundle::from_dir("./static", None) // Include all files.
//! .with_name("guides") // Name the generated module.
//! .build() // Build the bundle.
//! StaticFilesBundle::from_dir("./static", None)
//! .with_name("guides")
//! .build()
//! }
//! ```
//!
@ -67,7 +66,7 @@
//!
//! ## 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
//! intermediate and output artifacts are placed during compilation. For example, if you use
//! `with_name("guides")`, it generates a file named `guides.rs`:
@ -78,15 +77,15 @@
//! ```rust#ignore
//! 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
//! 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.
@ -98,6 +97,8 @@ use std::fs::{create_dir_all, remove_dir_all, File};
use std::io::Write;
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 {
resource_dir: ResourceDir,
}

View file

@ -3,8 +3,110 @@ mod smart_default;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
use quote::{quote, quote_spanned, ToTokens};
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_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
static-files.workspace = true
tera = "1.20.0"
[build-dependencies]

View file

@ -5,9 +5,9 @@ use tera::Tera;
use std::sync::LazyLock;
static_locales!(LOCALES_ALINER);
include_locales!(LOCALES_ALINER);
static_files!(aliner);
include_files!(aliner);
// ALINER THEME ************************************************************************************
@ -50,7 +50,7 @@ impl PackageTrait for Aliner {
}
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::*;
static_locales!(LOCALES_BOOTSIER);
include_locales!(LOCALES_BOOTSIER);
//static_files!(bootsier);
//include_files!(bootsier);
pub struct Bootsier;
@ -26,7 +26,7 @@ impl PackageTrait for Bootsier {
}
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">
<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)
[![API Docs](https://img.shields.io/docsrs/pagetop-macros?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-macros)
[![Crates.io](https://img.shields.io/crates/v/pagetop-macros.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-macros)
[![Downloads](https://img.shields.io/crates/d/pagetop-macros.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/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-seaorm.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-seaorm)
[![Downloads](https://img.shields.io/crates/d/pagetop-seaorm.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-seaorm)
</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
[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**.
# 🔖 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
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"]
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
readme = "../../README.md"
readme = "../README.md"
homepage = { workspace = true }
repository = { workspace = true }
@ -22,18 +22,20 @@ name = "pagetop"
colored = "2.1.0"
concat-string = "1.0.1"
figlet-rs = "0.1.5"
fluent-bundle = "0.15.3"
fluent-templates = "0.11.0"
itoa = "1.0.11"
nom = "7.1.3"
paste = "1.0.15"
substring = "1.4.5"
terminal_size = "0.4.0"
toml = "0.8.19"
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
tracing-actix-web = "0.7.15"
fluent-bundle = "0.15.3"
fluent-templates = "0.11.0"
unic-langid = { version = "0.9.5", features = ["macros"] }
actix-web = "4.9.0"
@ -45,3 +47,6 @@ serde.workspace = true
static-files.workspace = true
pagetop-macros.workspace = true
[build-dependencies]
pagetop-build.workspace = true

View file

@ -1,7 +1,7 @@
use pagetop_build::StaticFilesBundle;
fn main() -> std::io::Result<()> {
StaticFilesBundle::from_dir("./static/base")
.with_name("base")
StaticFilesBundle::from_dir("../static", None)
.with_name("assets")
.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> {
let name = path.into_inner();
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello " (name) "!" } }))
.with_body(PrepareMarkup::With(html! { h1 { "Hello " (name) "!" } }))
.render()
}

View file

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

View file

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

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::util::path;
use crate::util::source::Source;
use crate::util::value::Value;
use crate::config::error::*;
use crate::config::path;
use crate::config::source::Source;
use crate::config::value::Value;
use serde::de::Deserialize;

View file

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

View file

@ -1,9 +1,9 @@
mod source;
mod toml;
use crate::util::error::*;
use crate::util::source::Source;
use crate::util::value::Value;
use crate::config::error::*;
use crate::config::source::Source;
use crate::config::value::Value;
use std::collections::HashMap;
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;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
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::{service, trace};
use crate::{include_files, include_files_service, service, trace};
use std::sync::{LazyLock, RwLock};
@ -23,8 +23,6 @@ pub fn register_packages(root_package: Option<PackageRef>) {
if let Some(package) = root_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.
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) {
// 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()) {
// Add the package to the enabled list.
list.push(package);
// Reverse dependencies to add them in correct order (dependencies first).
let mut dependencies = package.dependencies();
dependencies.reverse();
for d in &dependencies {
// Add the package dependencies in reverse order first.
for d in package.dependencies().iter().rev() {
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.
if let Some(theme) = package.theme() {
let mut registered_themes = THEMES.write().unwrap();
@ -119,8 +115,14 @@ pub fn init_packages() {
// CONFIGURE SERVICES ******************************************************************************
include_files!(assets);
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
for m in ENABLED_PACKAGES.read().unwrap().iter() {
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 {
L10n::none()
L10n::default()
}
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};
@ -6,7 +8,7 @@ use std::sync::{LazyLock, RwLock};
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
/* DEFAULT THEME ***********************************************************************************
// DEFAULT THEME ***********************************************************************************
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) {
Some(theme) => theme,
None => &NoTheme,
@ -39,4 +40,3 @@ pub fn theme_by_short_name(short_name: &str) -> Option<ThemeRef> {
_ => 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.
use crate::static_config;
use crate::include_config;
use serde::Deserialize;
static_config!(SETTINGS: Settings => [
include_config!(SETTINGS: Settings => [
// [app]
"app.name" => "My App",
"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> {
//! Page::new(request)
//! .with_component(Html::with(html! { h1 { "Hello World!" } }))
//! .with_body(PrepareMarkup::With(html! { h1 { "Hello World!" } }))
//! .render()
//! }
//!
@ -53,7 +53,7 @@
//! ```
//! 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
//! 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).
//!
//! # 🧩 Dependency Management
@ -79,7 +79,7 @@ pub use concat_string::concat_string;
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
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>;
@ -91,6 +91,8 @@ pub type Weight = i8;
// Useful functions and macros.
pub mod util;
// Load configuration settings.
pub mod config;
// Application tracing and event logging.
pub mod trace;
// HTML in code.

View file

@ -67,13 +67,13 @@
//! # How to apply localization in your code
//!
//! 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:
//!
//! ```
//! use pagetop::prelude::*;
//!
//! static_locales!(LOCALES_SAMPLE);
//! include_locales!(LOCALES_SAMPLE);
//! ```
//!
//! But if they are in another directory, then you can use:
@ -81,14 +81,15 @@
//! ```
//! 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};
pub use fluent_bundle::FluentValue;
pub use fluent_templates;
pub use unic_langid::LanguageIdentifier;
pub use unic_langid::{CharacterDirection, LanguageIdentifier};
use fluent_templates::Loader;
use fluent_templates::StaticLoader as Locales;
@ -100,43 +101,62 @@ use std::sync::LazyLock;
use std::fmt;
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
/// 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(|| {
kv![
"en" => (langid!("en-US"), "English"),
"en-GB" => (langid!("en-GB"), "English (British)"),
"en-US" => (langid!("en-US"), "English (United States)"),
"es" => (langid!("es-ES"), "Spanish"),
"es-ES" => (langid!("es-ES"), "Spanish (Spain)"),
"en" => ( langid!("en-US"), "english" ),
"en-GB" => ( langid!("en-GB"), "english_british" ),
"en-US" => ( langid!("en-US"), "english_united_states" ),
"es" => ( langid!("es-ES"), "spanish" ),
"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
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
/// through `SETTINGS.app.language`.
pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK));
pub static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
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();
if language.is_empty() {
return Ok(&LANGID_FALLBACK);
return Err(LangError::EmptyLang);
}
LANGUAGES
.get(&language)
.map(|(langid, _)| langid)
.ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\"."))
// Attempt to match the full language code (e.g., "es-MX").
if let Some(langid) = LANGUAGES.get(&language).map(|(langid, _)| langid) {
return Ok(langid);
}
// 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]
/// 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.
macro_rules! static_locales {
macro_rules! include_locales {
( $LOCALES:ident $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
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! {
static $LOCALES = {
locales: $dir_locales,
@ -161,7 +181,7 @@ macro_rules! static_locales {
};
}
static_locales!(LOCALES_PAGETOP);
include_locales!(LOCALES_PAGETOP);
#[derive(AutoDefault)]
enum L10nOp {
@ -174,18 +194,15 @@ enum L10nOp {
#[derive(AutoDefault)]
pub struct L10n {
op: L10nOp,
locales: Option<&'static Locales>,
#[default(&LOCALES_PAGETOP)]
locales: &'static Locales,
args: HashMap<String, FluentValue<'static>>,
}
impl L10n {
pub fn none() -> Self {
L10n::default()
}
pub fn n(text: impl Into<String>) -> Self {
L10n {
op: L10nOp::Text(text.into()),
op: L10nOp::Text(text.into().to_string()),
..Default::default()
}
}
@ -193,7 +210,6 @@ impl L10n {
pub fn l(key: impl Into<String>) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
locales: Some(&LOCALES_PAGETOP),
..Default::default()
}
}
@ -201,68 +217,60 @@ impl L10n {
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
locales: Some(locales),
locales,
..Default::default()
}
}
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
self.args
.insert(arg.into(), FluentValue::from(value.into()));
let value = FluentValue::from(value.into());
self.args.insert(arg.into(), value);
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> {
match &self.op {
L10nOp::None => None,
L10nOp::Text(text) => Some(text.to_owned()),
L10nOp::Translate(key) => match self.locales {
Some(locales) => {
L10nOp::Translate(key) => {
if self.args.is_empty() {
locales.try_lookup(langid, key)
self.locales.try_lookup(langid, key)
} 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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.op {
L10nOp::None => write!(f, ""),
L10nOp::Text(text) => write!(f, "{text}"),
L10nOp::Translate(key) => {
if let Some(locales) = self.locales {
write!(
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}")
}
}
}
let content = match &self.op {
L10nOp::None => "".to_string(),
L10nOp::Text(text) => text.clone(),
L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("No <{}>", key)),
};
write!(f, "{content}")
}
}

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.
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};
// MACROS.
// crate::util
pub use crate::{kv, static_config};
pub use crate::kv;
// crate::config
pub use crate::include_config;
// crate::locale
pub use crate::static_locales;
pub use crate::include_locales;
// crate::service
pub use crate::{static_files, static_files_service};
pub use crate::{include_files, include_files_service};
// crate::core::action
pub use crate::actions;
@ -36,7 +38,7 @@ pub use crate::core::action::*;
pub use crate::core::package::*;
pub use crate::core::theme::*;
pub use crate::response::{json::*, redirect::*, ResponseError};
pub use crate::response::{json::*, page::*, redirect::*, ResponseError};
pub use crate::global;

View file

@ -2,6 +2,8 @@
pub use actix_web::ResponseError;
pub mod page;
pub mod json;
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