[pagetop] Añade la librería principal de PageTop

El crate pagetop reúne algunos de los crates más estables y populares
del ecosistema Rust para proporcionar un conjunto completo de
funcionalidades que pueden extenderse y adaptarse a las necesidades
específicas de cada aplicación para crear soluciones web modulares,
extensibles y configurables.
This commit is contained in:
Manuel Cillero 2024-12-04 08:20:48 +01:00
parent 87aac362d3
commit 514a8d89bc
102 changed files with 13606 additions and 18 deletions

2307
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ members = [
"helpers/pagetop-macros",
# PageTop
#"pagetop",
"pagetop",
# Packages
#"packages/pagetop-aliner",
@ -25,7 +25,7 @@ authors = ["Manuel Cillero <manuel@cillero.es>"]
[workspace.dependencies]
#include_dir = "0.7.4"
#serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
static-files = "0.2.4"
# Helpers
@ -33,7 +33,7 @@ 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" }
pagetop = { version = "0.0", path = "pagetop" }
# Packages
#pagetop-aliner = { version = "0.0", path = "packages/pagetop-aliner" }

115
README.md
View file

@ -1 +1,114 @@
# Un nuevo comienzo
<div align="center">
<img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
<h1>PageTop</h1>
<p>Entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.</p>
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license)
[![Doc API](https://img.shields.io/docsrs/pagetop?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop)
[![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop)
[![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
</div>
## Presentación
**PageTop** reúne algunos de los *crates* más estables y populares del ecosistema Rust para
proporcionar un conjunto completo de funcionalidades que pueden extenderse y adaptarse a las
necesidades específicas de cada aplicación web.
PageTop reivindica la sencillez de la web clásica aplicando *renderizado en el servidor* (SSR),
HTML, CSS y JS, mediante acciones, componentes, paquetes y temas:
* **Acciones**. Las funciones y procedimientos que incorporen *acciones* en su lógica de programa
estarán proporcionando a los desarrolladores herramientas para alterar su comportamiento interno
interceptando su flujo de ejecución.
* **Componentes**. Encapsulan HTML, CSS y JavaScript en unidades funcionales, configurables y bien
definidas.
* **Paquetes**. Extienden o personalizan funcionalidades existentes interactuando con las APIs de
PageTop o de paquetes de terceros.
* **Temas**. Permiten a los desarrolladores alterar la apariencia de las páginas y componentes sin
afectar su funcionalidad.
# ⚡️ Inicio rápido
La aplicación más sencilla de PageTop se ve así:
```rust
use pagetop::prelude::*;
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::new().run()?.await
}
```
Proporciona una página de bienvenida en `http://localhost:8088` según la configuración predefinida.
Para personalizar el servicio puedes crear un paquete de PageTop:
```rust
use pagetop::prelude::*;
struct HelloWorld;
impl PackageTrait for HelloWorld {
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
scfg.route("/", service::web::get().to(hello_world));
}
}
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello world!" } }))
.render()
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await
}
```
Este programa prepara un paquete personalizado llamado `HelloWorld` que sirve una página web en la
ruta raíz (`/`) mostrando el mensaje "Hello world!" en un elemento HTML `<h1>`.
# 📂 Crates de ayuda
* [pagetop-macros](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-macros):
Proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop.
* [pagetop-build](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-build):
Permite incluir fácilmente archivos estáticos o archivos SCSS compilados, directamente en el
binario de las aplicaciones PageTop.
# 🚧 Advertencia
**PageTop** es un proyecto personal que se encuentra en desarrollo activo. Actualmente su API es
inestable y está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta
que alcance la versión **0.1.0**.
# 📜 Licencia
El código está disponible bajo una doble licencia:
* **Licencia MIT**
([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
* **Licencia Apache, Versión 2.0**
([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
el ecosistema Rust.
# ✨ Contribuciones
Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia
indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la
licencia *Apache v2.0*.

View file

@ -14,7 +14,7 @@
# 📦 Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo con convenciones que reivindican la
web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS.
sencillez de la web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS.
# 🚧 Advertencia

View file

@ -4,7 +4,7 @@ version = "0.0.14"
edition = "2021"
description = """\
Una colección de macros que impulsan el desarrollo con PageTop.\
Una colección de macros que mejoran la experiencia de desarrollo con PageTop.\
"""
categories = ["development-tools::procedural-macro-helpers", "web-programming"]
keywords = ["pagetop", "macros", "proc-macros", "codegen"]

View file

@ -2,7 +2,7 @@
<h1>PageTop Macros</h1>
<p>Una colección de macros que impulsan el desarrollo con <strong>PageTop</strong>.</p>
<p>Una colección de macros que mejoran la experiencia de desarrollo con <strong>PageTop</strong>.</p>
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license)
[![Doc API](https://img.shields.io/docsrs/pagetop-macros?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-macros)
@ -14,7 +14,7 @@
# 📦 Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo con convenciones que reivindican la
web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS.
sencillez de la web clásica aplicando *renderizado en el servidor* (SSR), HTML, CSS y JS.
# 🚧 Advertencia

48
pagetop/Cargo.toml Normal file
View file

@ -0,0 +1,48 @@
[package]
name = "pagetop"
version = "0.0.57"
edition = "2021"
description = """\
Entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.\
"""
categories = ["web-programming", "gui", "development-tools", "asynchronous"]
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
readme = "../README.md"
repository.workspace = true
homepage.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
chrono = "0.4.38"
colored = "2.1.0"
concat-string = "1.0.1"
figlet-rs = "0.1.5"
itoa = "1.0.14"
nom = "7.1.3"
paste = "1.0.15"
serde.workspace = true
substring = "1.4.5"
terminal_size = "0.4.1"
toml = "0.8.19"
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", features = ["json", "env-filter"] }
tracing-actix-web = "0.7.15"
fluent-templates = "0.11.0"
unic-langid = { version = "0.9.5", features = ["macros"] }
actix-web = "4.9.0"
actix-session = { version = "0.10.1", features = ["cookie-session"] }
actix-web-files = { package = "actix-files", version = "0.6.6" }
actix-web-static-files = "4.0.1"
static-files.workspace = true
pagetop-macros.workspace = true
[build-dependencies]
pagetop-build.workspace = true

7
pagetop/build.rs Normal file
View file

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

View file

@ -0,0 +1,6 @@
[app]
name = "Samples"
#language = "es-ES"
[log]
tracing = "Debug"

View file

@ -0,0 +1,40 @@
[app]
name = "My App"
description = "Developed with the amazing PageTop framework."
# Default theme.
theme = "Default"
# Default language (localization).
language = "en-US"
# Default text direction: "ltr", "rtl", or "auto".
direction = "ltr"
# Startup banner: "Off", "Slant", "Small", "Speed", or "Starwars".
startup_banner = "Slant"
[dev]
# Static files required by the app are integrated by default into the executable
# binary. However, during development, it can be useful to serve these files
# from their own directory to avoid recompiling every time they are modified. In
# this case, just indicate the full path to the project's root directory.
pagetop_project_dir = ""
[log]
# Execution trace: "Error", "Warn", "Info", "Debug", or "Trace".
# For example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
tracing = "Info"
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
rolling = "Stdout"
# Directory for trace files (if rolling != "Stdout").
path = "log"
# Prefix for trace files (if rolling != "Stdout").
prefix = "tracing.log"
# Traces format: "Full", "Compact", "Pretty", or "Json".
format = "Full"
[server]
# Web server config.
bind_address = "localhost"
bind_port = 8088
# Session cookie duration (in seconds), i.e., the time from when the session is
# created until the cookie expires. A value of 0 indicates "until the browser is
# closed". By default, it is one week.
session_lifetime = 604800

View file

@ -0,0 +1,6 @@
use pagetop::prelude::*;
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::new().run()?.await
}

View file

@ -0,0 +1,25 @@
use pagetop::prelude::*;
struct HelloName;
impl PackageTrait for HelloName {
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
scfg.service(hello_name);
}
}
#[service::get("/hello/{name}")]
async fn hello_name(
request: HttpRequest,
path: service::web::Path<String>,
) -> ResultPage<Markup, ErrorPage> {
let name = path.into_inner();
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello " (name) "!" } }))
.render()
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&HelloName).run()?.await
}

View file

@ -0,0 +1,20 @@
use pagetop::prelude::*;
struct HelloWorld;
impl PackageTrait for HelloWorld {
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
scfg.route("/", service::web::get().to(hello_world));
}
}
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_component(Html::with(html! { h1 { "Hello World!" } }))
.render()
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&HelloWorld).run()?.await
}

168
pagetop/src/app.rs Normal file
View file

@ -0,0 +1,168 @@
//! Prepare and run an application created with **Pagetop**.
mod figfont;
use crate::core::{package, package::PackageRef};
use crate::html::Markup;
use crate::response::page::{ErrorPage, ResultPage};
use crate::service::HttpRequest;
use crate::{global, locale, service, trace};
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use substring::Substring;
use std::io::Error;
use std::sync::LazyLock;
pub struct Application;
impl Default for Application {
fn default() -> Self {
Self::new()
}
}
impl Application {
/// Creates a new application instance without any package.
pub fn new() -> Self {
Self::internal_prepare(None)
}
/// Prepares an application instance with a specific package.
pub fn prepare(root_package: PackageRef) -> Self {
Self::internal_prepare(Some(root_package))
}
// Internal method to prepare the application, optionally with a package.
fn internal_prepare(root_package: Option<PackageRef>) -> Self {
// On startup, show the application banner.
Self::show_banner();
// Starts logging and event tracing.
LazyLock::force(&trace::TRACING);
// Validates the default language identifier.
LazyLock::force(&locale::DEFAULT_LANGID);
// Registers the application's packages.
package::all::register_packages(root_package);
// Registers package actions.
package::all::register_actions();
// Initializes the packages.
package::all::init_packages();
Self
}
// Displays the application banner based on the configuration.
fn show_banner() {
use colored::Colorize;
use terminal_size::{terminal_size, Width};
if global::SETTINGS.app.startup_banner.to_lowercase() != "off" {
// Application name, formatted for the terminal width if necessary.
let mut app_ff = "".to_string();
let app_name = &global::SETTINGS.app.name;
if let Some((Width(term_width), _)) = terminal_size() {
if term_width >= 80 {
let maxlen: usize = ((term_width / 10) - 2).into();
let mut app = app_name.substring(0, maxlen).to_owned();
if app_name.len() > maxlen {
app = format!("{app}...");
}
if let Some(ff) = figfont::FIGFONT.convert(&app) {
app_ff = ff.to_string();
}
}
}
if app_ff.is_empty() {
println!("\n{app_name}");
} else {
print!("\n{app_ff}");
}
// Application description.
if !global::SETTINGS.app.description.is_empty() {
println!("{}", global::SETTINGS.app.description.cyan());
};
// PageTop version.
println!(
"{} {}\n",
"Powered by PageTop".yellow(),
env!("CARGO_PKG_VERSION").yellow()
);
}
}
/// Starts the web server.
pub fn run(self) -> Result<service::Server, Error> {
// Generate the cookie key.
let secret_key = service::cookie::Key::generate();
// Prepares the web server.
Ok(service::HttpServer::new(move || {
Self::service_app()
.wrap(tracing_actix_web::TracingLogger::default())
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
.session_lifecycle(match global::SETTINGS.server.session_lifetime {
0 => SessionLifecycle::BrowserSession(BrowserSession::default()),
_ => SessionLifecycle::PersistentSession(
PersistentSession::default().session_ttl(
service::cookie::time::Duration::seconds(
global::SETTINGS.server.session_lifetime,
),
),
),
})
.build(),
)
})
.bind(format!(
"{}:{}",
&global::SETTINGS.server.bind_address,
&global::SETTINGS.server.bind_port
))?
.run())
}
/// Method for testing, returns a service application instance.
pub fn test(
self,
) -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
Error = service::Error,
InitError = (),
>,
> {
Self::service_app()
}
// Configures the service application.
fn service_app() -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
Error = service::Error,
InitError = (),
>,
> {
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> {
Err(ErrorPage::NotFound(request))
}

View file

@ -0,0 +1,30 @@
use crate::global;
use std::sync::LazyLock;
use figlet_rs::FIGfont;
pub static FIGFONT: LazyLock<FIGfont> = LazyLock::new(|| {
let slant = include_str!("slant.flf");
let small = include_str!("small.flf");
let speed = include_str!("speed.flf");
let starwars = include_str!("starwars.flf");
FIGfont::from_content(
match global::SETTINGS.app.startup_banner.to_lowercase().as_str() {
"off" => slant,
"slant" => slant,
"small" => small,
"speed" => speed,
"starwars" => starwars,
_ => {
println!(
"\n FIGfont \"{}\" not found for banner. Using \"Slant\". Check settings files.",
global::SETTINGS.app.startup_banner,
);
slant
}
},
)
.unwrap()
});

1295
pagetop/src/app/slant.flf Normal file

File diff suppressed because it is too large Load diff

1097
pagetop/src/app/small.flf Normal file

File diff suppressed because it is too large Load diff

1301
pagetop/src/app/speed.flf Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,719 @@
flf2a$ 7 6 22 15 4
starwars.flf by Ryan Youck (youck@cs.uregina.ca) Dec 25/1994
I am not responsible for use of this font
Based on Big.flf by Glenn Chappell
$ $@
$ $@
$ $@
$ $@
$ $@
$ $@
$ $@@
__ $@
| |$@
| |$@
| |$@
|__|$@
(__)$@
$@@
_ _ @
( | )@
V V @
$ @
$ @
$ @
@@
_ _ @
_| || |_$@
|_ __ _|@
_| || |_ @
|_ __ _|@
|_||_| $@
@@
__,--,_.@
/ |@
| (----`@
\ \ $@
.----) | $@
|_ __/ $@
'--' $@@
_ ___$ @
/ \ / /$ @
( o ) / / $ @
\_/ / / _$ @
/ / / \ @
/ / ( o )@
/__/ \_/ @@
@
___ @
( _ ) $@
/ _ \/\@
| (_> <@
\___/\/@
$@@
__ @
(_ )@
|/ @
$ @
$ @
$ @
@@
___@
/ /@
| |$@
| |$@
| |$@
| |$@
\__\@@
___ @
\ \ @
| |@
| |@
| |@
| |@
/__/ @@
_ @
/\| |/\ @
\ ` ' /$@
|_ _|@
/ , . \$@
\/|_|\/ @
@@
@
_ @
_| |_$@
|_ _|@
|_| $@
$ @
@@
@
@
$ @
$ @
__ @
(_ )@
|/ @@
@
@
______ @
|______|@
$ @
$ @
@@
@
@
@
$ @
__ @
(__)@
@@
___@
/ /@
/ / @
/ /$ @
/ /$ @
/__/$ @
@@
___ $@
/ _ \ $@
| | | |$@
| | | |$@
| |_| |$@
\___/ $@
$@@
__ $@
/_ |$@
| |$@
| |$@
| |$@
|_|$@
$@@
___ $@
|__ \ $@
$) |$@
/ / $@
/ /_ $@
|____|$@
$@@
____ $@
|___ \ $@
__) |$@
|__ < $@
___) |$@
|____/ $@
$@@
_ _ $@
| || | $@
| || |_ $@
|__ _|$@
| | $@
|_| $@
$@@
_____ $@
| ____|$@
| |__ $@
|___ \ $@
___) |$@
|____/ $@
$@@
__ $@
/ / $@
/ /_ $@
| '_ \ $@
| (_) |$@
\___/ $@
$@@
______ $@
|____ |$@
$/ / $@
/ / $@
/ / $@
/_/ $@
$@@
___ $@
/ _ \ $@
| (_) |$@
> _ < $@
| (_) |$@
\___/ $@
$@@
___ $@
/ _ \ $@
| (_) |$@
\__, |$@
/ / $@
/_/ $@
$@@
@
_ @
(_)@
$ @
_ @
(_)@
@@
@
_ @
(_)@
$ @
_ @
( )@
|/ @@
___@
/ /@
/ /$@
< <$ @
\ \$@
\__\@
@@
@
______ @
|______|@
______ @
|______|@
@
@@
___ @
\ \$ @
\ \ @
> >@
/ / @
/__/$ @
@@
______ $@
| \ $@
`----) |$@
/ / $@
|__| $@
__ $@
(__) $@@
____ @
/ __ \ @
/ / _` |@
| | (_| |@
\ \__,_|@
\____/ @
@@
___ $ @
/ \ $ @
/ ^ \$ @
/ /_\ \$ @
/ _____ \$ @
/__/ \__\$@
$@@
.______ $@
| _ \ $@
| |_) |$@
| _ < $@
| |_) |$@
|______/ $@
$@@
______$@
/ |@
| ,----'@
| | $@
| `----.@
\______|@
$@@
_______ $@
| \$@
| .--. |@
| | | |@
| '--' |@
|_______/$@
$@@
_______ @
| ____|@
| |__ $@
| __| $@
| |____ @
|_______|@
@@
_______ @
| ____|@
| |__ $@
| __| $@
| | $ @
|__| @
@@
_______ @
/ _____|@
| | __ $@
| | |_ |$@
| |__| |$@
\______|$@
$@@
__ __ $@
| | | |$@
| |__| |$@
| __ |$@
| | | |$@
|__| |__|$@
$@@
__ $@
| |$@
| |$@
| |$@
| |$@
|__|$@
$@@
__ $@
| |$@
| |$@
.--. | |$@
| `--' |$@
\______/ $@
$@@
__ ___$@
| |/ /$@
| ' / $@
| < $@
| . \ $@
|__|\__\$@
$@@
__ $@
| | $@
| | $@
| | $@
| `----.@
|_______|@
$@@
.___ ___.$@
| \/ |$@
| \ / |$@
| |\/| |$@
| | | |$@
|__| |__|$@
$@@
.__ __.$@
| \ | |$@
| \| |$@
| . ` |$@
| |\ |$@
|__| \__|$@
$@@
______ $@
/ __ \ $@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
.______ $@
| _ \ $@
| |_) |$@
| ___/ $@
| | $ @
| _| $ @
$ @@
______ $ @
/ __ \ $ @
| | | | $ @
| | | | $ @
| `--' '--. @
\_____\_____\@
$ @@
.______ $ @
| _ \ $ @
| |_) | $ @
| / $ @
| |\ \----.@
| _| `._____|@
$@@
_______.@
/ |@
| (----`@
\ \ $@
.----) | $@
|_______/ $@
$@@
.___________.@
| |@
`---| |----`@
| | $ @
| | $ @
|__| $ @
$ @@
__ __ $@
| | | |$@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\ /$ @
\ /$ @
\__/$ @
$ @@
____ __ ____$@
\ \ / \ / /$@
\ \/ \/ /$ @
\ /$ @
\ /\ /$ @
\__/ \__/$ @
$ @@
___ ___$@
\ \ / /$@
\ V / $@
> < $@
/ . \ $@
/__/ \__\$@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\_ _/$ @
| |$ @
|__|$ @
$ @@
________ $@
| / $@
`---/ / $@
/ / $@
/ /----.@
/________|@
$@@
____ @
| |@
| |-`@
| | $@
| | $@
| |-.@
|____|@@
___ @
\ \ $ @
\ \$ @
\ \$ @
\ \$@
\__\@
@@
____ @
| |@
`-| |@
| |@
| |@
.-| |@
|____|@@
___ @
/ \ @
/--^--\@
$@
$@
$@
$@@
@
@
@
$ @
$ @
______ @
|______|@@
__ @
( _)@
\| @
$ @
$ @
$ @
@@
___ $ @
/ \ $ @
/ ^ \$ @
/ /_\ \$ @
/ _____ \$ @
/__/ \__\$@
$@@
.______ $@
| _ \ $@
| |_) |$@
| _ < $@
| |_) |$@
|______/ $@
$@@
______$@
/ |@
| ,----'@
| | $@
| `----.@
\______|@
$@@
_______ $@
| \$@
| .--. |@
| | | |@
| '--' |@
|_______/$@
$@@
_______ @
| ____|@
| |__ $@
| __| $@
| |____ @
|_______|@
@@
_______ @
| ____|@
| |__ $@
| __| $@
| | $ @
|__| @
@@
_______ @
/ _____|@
| | __ $@
| | |_ |$@
| |__| |$@
\______|$@
$@@
__ __ $@
| | | |$@
| |__| |$@
| __ |$@
| | | |$@
|__| |__|$@
$@@
__ $@
| |$@
| |$@
| |$@
| |$@
|__|$@
$@@
__ $@
| |$@
| |$@
.--. | |$@
| `--' |$@
\______/ $@
$@@
__ ___$@
| |/ /$@
| ' / $@
| < $@
| . \ $@
|__|\__\$@
$@@
__ $@
| | $@
| | $@
| | $@
| `----.@
|_______|@
$@@
.___ ___.$@
| \/ |$@
| \ / |$@
| |\/| |$@
| | | |$@
|__| |__|$@
$@@
.__ __.$@
| \ | |$@
| \| |$@
| . ` |$@
| |\ |$@
|__| \__|$@
$@@
______ $@
/ __ \ $@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
.______ $@
| _ \ $@
| |_) |$@
| ___/ $@
| | $ @
| _| $ @
$ @@
______ $ @
/ __ \ $ @
| | | | $ @
| | | | $ @
| `--' '--. @
\_____\_____\@
$ @@
.______ $ @
| _ \ $ @
| |_) | $ @
| / $ @
| |\ \----.@
| _| `._____|@
$@@
_______.@
/ |@
| (----`@
\ \ $@
.----) | $@
|_______/ $@
$@@
.___________.@
| |@
`---| |----`@
| | $ @
| | $ @
|__| $ @
$ @@
__ __ $@
| | | |$@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\ /$ @
\ /$ @
\__/$ @
$ @@
____ __ ____$@
\ \ / \ / /$@
\ \/ \/ /$ @
\ /$ @
\ /\ /$ @
\__/ \__/$ @
$ @@
___ ___$@
\ \ / /$@
\ V / $@
> < $@
/ . \ $@
/__/ \__\$@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\_ _/$ @
| |$ @
|__|$ @
$ @@
________ $@
| / $@
`---/ / $@
/ / $@
/ /----.@
/________|@
$@@
___@
/ /@
| |$@
/ /$ @
\ \$ @
| |$@
\__\@@
__ $@
| |$@
| |$@
| |$@
| |$@
| |$@
|__|$@@
___ @
\ \$ @
| | @
\ \@
/ /@
| | @
/__/$ @@
__ _ @
/ \/ |@
|_/\__/ @
$ @
$ @
$ @
@@
_ _ @
(_)_(_) @
/ \ @
/ _ \ @
/ ___ \ @
/_/ \_\@
@@
_ _ @
(_)_(_)@
/ _ \ @
| | | |@
| |_| |@
\___/ @
@@
_ _ @
(_) (_)@
| | | |@
| | | |@
| |_| |@
\___/ @
@@
_ _ @
(_) (_)@
__ _ @
/ _` |@
| (_| |@
\__,_|@
@@
_ _ @
(_) (_)@
___ @
/ _ \ @
| (_) |@
\___/ @
@@
_ _ @
(_) (_)@
_ _ @
| | | |@
| |_| |@
\__,_|@
@@
___ @
/ _ \ @
| | ) |@
| |< < @
| | ) |@
| ||_/ @
|_| @@

9
pagetop/src/base.rs Normal file
View file

@ -0,0 +1,9 @@
//! Base actions, components, packages, and themes.
pub mod action;
pub mod component;
pub mod package;
pub mod theme;

View file

@ -0,0 +1,9 @@
use crate::prelude::*;
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
pub mod page;
pub mod theme;
pub mod component;

View file

@ -0,0 +1,8 @@
mod is_renderable;
pub use is_renderable::*;
mod before_prepare_component;
pub use before_prepare_component::*;
mod after_prepare_component;
pub use after_prepare_component::*;

View file

@ -0,0 +1,65 @@
use crate::prelude::*;
use crate::base::action::FnActionWithComponent;
pub struct AfterPrepare<C: ComponentTrait> {
f: FnActionWithComponent<C>,
referer_type_id: Option<TypeId>,
referer_id: OptionId,
weight: Weight,
}
impl<C: ComponentTrait> ActionTrait for AfterPrepare<C> {
fn referer_type_id(&self) -> Option<TypeId> {
self.referer_type_id
}
fn referer_id(&self) -> Option<String> {
self.referer_id.get()
}
fn weight(&self) -> Weight {
self.weight
}
}
impl<C: ComponentTrait> AfterPrepare<C> {
pub fn new(f: FnActionWithComponent<C>) -> Self {
AfterPrepare {
f,
referer_type_id: Some(TypeId::of::<C>()),
referer_id: OptionId::default(),
weight: 0,
}
}
pub fn filter_by_referer_id(mut self, id: impl Into<String>) -> Self {
self.referer_id.set_value(id);
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
dispatch_actions(
&ActionKey::new(TypeId::of::<Self>(), None, Some(TypeId::of::<C>()), None),
|action: &Self| (action.f)(component, cx),
);
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(
TypeId::of::<Self>(),
None,
Some(TypeId::of::<C>()),
Some(id),
),
|action: &Self| (action.f)(component, cx),
);
}
}
}

View file

@ -0,0 +1,65 @@
use crate::prelude::*;
use crate::base::action::FnActionWithComponent;
pub struct BeforePrepare<C: ComponentTrait> {
f: FnActionWithComponent<C>,
referer_type_id: Option<TypeId>,
referer_id: OptionId,
weight: Weight,
}
impl<C: ComponentTrait> ActionTrait for BeforePrepare<C> {
fn referer_type_id(&self) -> Option<TypeId> {
self.referer_type_id
}
fn referer_id(&self) -> Option<String> {
self.referer_id.get()
}
fn weight(&self) -> Weight {
self.weight
}
}
impl<C: ComponentTrait> BeforePrepare<C> {
pub fn new(f: FnActionWithComponent<C>) -> Self {
BeforePrepare {
f,
referer_type_id: Some(TypeId::of::<C>()),
referer_id: OptionId::default(),
weight: 0,
}
}
pub fn filter_by_referer_id(mut self, id: impl Into<String>) -> Self {
self.referer_id.set_value(id);
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
dispatch_actions(
&ActionKey::new(TypeId::of::<Self>(), None, Some(TypeId::of::<C>()), None),
|action: &Self| (action.f)(component, cx),
);
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(
TypeId::of::<Self>(),
None,
Some(TypeId::of::<C>()),
Some(id),
),
|action: &Self| (action.f)(component, cx),
);
}
}
}

View file

@ -0,0 +1,77 @@
use crate::prelude::*;
pub type FnIsRenderable<C> = fn(component: &C, cx: &mut Context) -> bool;
pub struct IsRenderable<C: ComponentTrait> {
f: FnIsRenderable<C>,
referer_type_id: Option<TypeId>,
referer_id: OptionId,
weight: Weight,
}
impl<C: ComponentTrait> ActionTrait for IsRenderable<C> {
fn referer_type_id(&self) -> Option<TypeId> {
self.referer_type_id
}
fn referer_id(&self) -> Option<String> {
self.referer_id.get()
}
fn weight(&self) -> Weight {
self.weight
}
}
impl<C: ComponentTrait> IsRenderable<C> {
pub fn new(f: FnIsRenderable<C>) -> Self {
IsRenderable {
f,
referer_type_id: Some(TypeId::of::<C>()),
referer_id: OptionId::default(),
weight: 0,
}
}
pub fn filter_by_referer_id(mut self, id: impl Into<String>) -> Self {
self.referer_id.set_value(id);
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> bool {
let mut renderable = true;
dispatch_actions(
&ActionKey::new(TypeId::of::<Self>(), None, Some(TypeId::of::<C>()), None),
|action: &Self| {
if renderable && !(action.f)(component, cx) {
renderable = false;
}
},
);
if renderable {
if let Some(id) = component.id() {
dispatch_actions(
&ActionKey::new(
TypeId::of::<Self>(),
None,
Some(TypeId::of::<C>()),
Some(id),
),
|action: &Self| {
if renderable && !(action.f)(component, cx) {
renderable = false;
}
},
);
}
}
renderable
}
}

View file

@ -0,0 +1,5 @@
mod before_render_body;
pub use before_render_body::*;
mod after_render_body;
pub use after_render_body::*;

View file

@ -0,0 +1,34 @@
use crate::prelude::*;
pub type FnAfterRenderBody = fn(page: &mut Page);
pub struct AfterRenderBody {
f: FnAfterRenderBody,
weight: Weight,
}
impl ActionTrait for AfterRenderBody {
fn weight(&self) -> Weight {
self.weight
}
}
impl AfterRenderBody {
pub fn new(f: FnAfterRenderBody) -> Self {
AfterRenderBody { f, weight: 0 }
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions(
&ActionKey::new(TypeId::of::<Self>(), None, None, None),
|action: &Self| (action.f)(page),
);
}
}

View file

@ -0,0 +1,34 @@
use crate::prelude::*;
pub type FnBeforeRenderBody = fn(page: &mut Page);
pub struct BeforeRenderBody {
f: FnBeforeRenderBody,
weight: Weight,
}
impl ActionTrait for BeforeRenderBody {
fn weight(&self) -> Weight {
self.weight
}
}
impl BeforeRenderBody {
pub fn new(f: FnBeforeRenderBody) -> Self {
BeforeRenderBody { f, weight: 0 }
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions(
&ActionKey::new(TypeId::of::<Self>(), None, None, None),
|action: &Self| (action.f)(page),
);
}
}

View file

@ -0,0 +1,8 @@
mod before_prepare_component;
pub use before_prepare_component::*;
mod after_prepare_component;
pub use after_prepare_component::*;
mod render_component;
pub use render_component::*;

View file

@ -0,0 +1,43 @@
use crate::prelude::*;
use crate::base::action::FnActionWithComponent;
pub struct AfterPrepare<C: ComponentTrait> {
f: FnActionWithComponent<C>,
theme_type_id: Option<TypeId>,
referer_type_id: Option<TypeId>,
}
impl<C: ComponentTrait> ActionTrait for AfterPrepare<C> {
fn theme_type_id(&self) -> Option<TypeId> {
self.theme_type_id
}
fn referer_type_id(&self) -> Option<TypeId> {
self.referer_type_id
}
}
impl<C: ComponentTrait> AfterPrepare<C> {
pub fn new(theme: ThemeRef, f: FnActionWithComponent<C>) -> Self {
AfterPrepare {
f,
theme_type_id: Some(theme.type_id()),
referer_type_id: Some(TypeId::of::<C>()),
}
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
dispatch_actions(
&ActionKey::new(
TypeId::of::<Self>(),
Some(cx.theme().type_id()),
Some(TypeId::of::<C>()),
None,
),
|action: &Self| (action.f)(component, cx),
);
}
}

View file

@ -0,0 +1,43 @@
use crate::prelude::*;
use crate::base::action::FnActionWithComponent;
pub struct BeforePrepare<C: ComponentTrait> {
f: FnActionWithComponent<C>,
theme_type_id: Option<TypeId>,
referer_type_id: Option<TypeId>,
}
impl<C: ComponentTrait> ActionTrait for BeforePrepare<C> {
fn theme_type_id(&self) -> Option<TypeId> {
self.theme_type_id
}
fn referer_type_id(&self) -> Option<TypeId> {
self.referer_type_id
}
}
impl<C: ComponentTrait> BeforePrepare<C> {
pub fn new(theme: ThemeRef, f: FnActionWithComponent<C>) -> Self {
BeforePrepare {
f,
theme_type_id: Some(theme.type_id()),
referer_type_id: Some(TypeId::of::<C>()),
}
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
dispatch_actions(
&ActionKey::new(
TypeId::of::<Self>(),
Some(cx.theme().type_id()),
Some(TypeId::of::<C>()),
None,
),
|action: &Self| (action.f)(component, cx),
);
}
}

View file

@ -0,0 +1,49 @@
use crate::prelude::*;
pub type FnRenderComponent<C> = fn(component: &C, cx: &mut Context) -> Option<Markup>;
pub struct RenderComponent<C: ComponentTrait> {
f: FnRenderComponent<C>,
theme_type_id: Option<TypeId>,
referer_type_id: Option<TypeId>,
}
impl<C: ComponentTrait> ActionTrait for RenderComponent<C> {
fn theme_type_id(&self) -> Option<TypeId> {
self.theme_type_id
}
fn referer_type_id(&self) -> Option<TypeId> {
self.referer_type_id
}
}
impl<C: ComponentTrait> RenderComponent<C> {
pub fn new(theme: ThemeRef, f: FnRenderComponent<C>) -> Self {
RenderComponent {
f,
theme_type_id: Some(theme.type_id()),
referer_type_id: Some(TypeId::of::<C>()),
}
}
#[inline(always)]
#[allow(clippy::inline_always)]
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> Option<Markup> {
let mut render_component: Option<Markup> = None;
dispatch_actions(
&ActionKey::new(
TypeId::of::<Self>(),
Some(cx.theme().type_id()),
Some(TypeId::of::<C>()),
None,
),
|action: &Self| {
if render_component.is_none() {
render_component = (action.f)(component, cx);
}
},
);
render_component
}
}

View file

@ -0,0 +1,11 @@
mod html;
pub use html::Html;
mod fluent;
pub use fluent::Fluent;
mod error403;
pub use error403::Error403;
mod error404;
pub use error404::Error404;

View file

@ -0,0 +1,17 @@
use crate::prelude::*;
pub struct Error403;
impl ComponentTrait for Error403 {
fn new() -> Self {
Error403
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div {
h1 { ("FORBIDDEN ACCESS") }
}
})
}
}

View file

@ -0,0 +1,17 @@
use crate::prelude::*;
pub struct Error404;
impl ComponentTrait for Error404 {
fn new() -> Self {
Error404
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div {
h1 { ("RESOURCE NOT FOUND") }
}
})
}
}

View file

@ -0,0 +1,25 @@
use crate::prelude::*;
#[derive(AutoDefault)]
pub struct Fluent(L10n);
impl ComponentTrait for Fluent {
fn new() -> Self {
Fluent::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(self.0.escaped(cx.langid()))
}
}
impl Fluent {
pub fn with(l10n: L10n) -> Self {
Fluent(l10n)
}
pub fn set_l10n(&mut self, l10n: L10n) -> &mut Self {
self.0 = l10n;
self
}
}

View file

@ -0,0 +1,25 @@
use crate::prelude::*;
#[derive(AutoDefault)]
pub struct Html(Markup);
impl ComponentTrait for Html {
fn new() -> Self {
Html::default()
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! { (self.0) })
}
}
impl Html {
pub fn with(html: Markup) -> Self {
Html(html)
}
pub fn set_html(&mut self, html: Markup) -> &mut Self {
self.0 = html;
self
}
}

133
pagetop/src/base/package.rs Normal file
View file

@ -0,0 +1,133 @@
use crate::prelude::*;
pub struct Welcome;
impl PackageTrait for Welcome {
fn name(&self) -> L10n {
L10n::l("welcome_package_name")
}
fn description(&self) -> L10n {
L10n::l("welcome_package_description")
}
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
scfg.route("/", service::web::get().to(homepage));
}
}
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_title(L10n::l("welcome_page"))
.with_assets(AssetsOp::Theme("Basic"))
.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;
}
.skip__to_content {
display: none;
}
.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_component(Html::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()
}

11
pagetop/src/base/theme.rs Normal file
View file

@ -0,0 +1,11 @@
use crate::prelude::*;
pub struct Basic;
impl PackageTrait for Basic {
fn theme(&self) -> Option<ThemeRef> {
Some(&Basic)
}
}
impl ThemeTrait for Basic {}

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),
}
});
};
}

136
pagetop/src/config/data.rs Normal file
View file

@ -0,0 +1,136 @@
use crate::config::error::*;
use crate::config::path;
use crate::config::source::Source;
use crate::config::value::Value;
use serde::de::Deserialize;
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Clone, Debug)]
enum ConfigKind {
// A mutable configuration. This is the default.
Mutable {
defaults: HashMap<path::Expression, Value>,
overrides: HashMap<path::Expression, Value>,
sources: Vec<Box<dyn Source + Send + Sync>>,
},
}
impl Default for ConfigKind {
fn default() -> Self {
ConfigKind::Mutable {
defaults: HashMap::new(),
overrides: HashMap::new(),
sources: Vec::new(),
}
}
}
/// A prioritized configuration repository. It maintains a set of configuration sources, fetches
/// values to populate those, and provides them according to the source's priority.
#[derive(Default, Clone, Debug)]
pub struct ConfigData {
kind: ConfigKind,
/// Root of the cached configuration.
pub cache: Value,
}
impl ConfigData {
/// Merge in a configuration property source.
pub fn merge<T>(&mut self, source: T) -> Result<&mut ConfigData>
where
T: 'static,
T: Source + Send + Sync,
{
match self.kind {
ConfigKind::Mutable {
ref mut sources, ..
} => {
sources.push(Box::new(source));
}
}
self.refresh()
}
/// Refresh the configuration cache with fresh data from added sources.
///
/// Configuration is automatically refreshed after a mutation operation (`set`, `merge`,
/// `set_default`, etc.).
pub fn refresh(&mut self) -> Result<&mut ConfigData> {
self.cache = match self.kind {
// TODO: We need to actually merge in all the stuff.
ConfigKind::Mutable {
ref overrides,
ref sources,
ref defaults,
} => {
let mut cache: Value = HashMap::<String, Value>::new().into();
// Add defaults.
for (key, val) in defaults {
key.set(&mut cache, val.clone());
}
// Add sources.
sources.collect_to(&mut cache)?;
// Add overrides.
for (key, val) in overrides {
key.set(&mut cache, val.clone());
}
cache
}
};
Ok(self)
}
pub fn set_default<T>(&mut self, key: &str, value: T) -> Result<&mut ConfigData>
where
T: Into<Value>,
{
match self.kind {
ConfigKind::Mutable {
ref mut defaults, ..
} => {
defaults.insert(key.parse()?, value.into());
}
};
self.refresh()
}
pub fn set<T>(&mut self, key: &str, value: T) -> Result<&mut ConfigData>
where
T: Into<Value>,
{
match self.kind {
ConfigKind::Mutable {
ref mut overrides, ..
} => {
overrides.insert(key.parse()?, value.into());
}
};
self.refresh()
}
/// Attempt to deserialize the entire configuration into the requested type.
pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result<T> {
T::deserialize(self)
}
}
impl Source for ConfigData {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
self.cache.clone().into_table()
}
}

462
pagetop/src/config/de.rs Normal file
View file

@ -0,0 +1,462 @@
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;
use std::collections::{HashMap, VecDeque};
use std::iter::Enumerate;
impl<'de> de::Deserializer<'de> for Value {
type Error = ConfigError;
#[inline]
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Deserialize based on the underlying type.
match self.kind {
ValueKind::Nil => visitor.visit_unit(),
ValueKind::Integer(i) => visitor.visit_i64(i),
ValueKind::Boolean(b) => visitor.visit_bool(b),
ValueKind::Float(f) => visitor.visit_f64(f),
ValueKind::String(s) => visitor.visit_string(s),
ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)),
ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)),
}
}
#[inline]
fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_bool(self.into_bool()?)
}
#[inline]
fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i8(self.into_int()? as i8)
}
#[inline]
fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i16(self.into_int()? as i16)
}
#[inline]
fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i32(self.into_int()? as i32)
}
#[inline]
fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_i64(self.into_int()?)
}
#[inline]
fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u8(self.into_int()? as u8)
}
#[inline]
fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u16(self.into_int()? as u16)
}
#[inline]
fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u32(self.into_int()? as u32)
}
#[inline]
fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u64(self.into_int()? as u64)
}
#[inline]
fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f32(self.into_float()? as f32)
}
#[inline]
fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f64(self.into_float()?)
}
#[inline]
fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.into_str()?)
}
#[inline]
fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.into_str()?)
}
#[inline]
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Match an explicit nil as None and everything else as Some.
match self.kind {
ValueKind::Nil => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_newtype_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_enum<V>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
visitor.visit_enum(EnumAccess {
value: self,
name,
variants,
})
}
forward_to_deserialize_any! {
char seq
bytes byte_buf map struct unit
identifier ignored_any unit_struct tuple_struct tuple
}
}
struct StrDeserializer<'a>(&'a str);
impl<'de, 'a> de::Deserializer<'de> for StrDeserializer<'a> {
type Error = ConfigError;
#[inline]
fn deserialize_any<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_str(self.0)
}
forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
bytes byte_buf map struct unit enum newtype_struct
identifier ignored_any unit_struct tuple_struct tuple option
}
}
struct SeqAccess {
elements: Enumerate<::std::vec::IntoIter<Value>>,
}
impl SeqAccess {
fn new(elements: Vec<Value>) -> Self {
SeqAccess {
elements: elements.into_iter().enumerate(),
}
}
}
impl<'de> de::SeqAccess<'de> for SeqAccess {
type Error = ConfigError;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
where
T: de::DeserializeSeed<'de>,
{
match self.elements.next() {
Some((idx, value)) => seed
.deserialize(value)
.map(Some)
.map_err(|e| e.prepend_index(idx)),
None => Ok(None),
}
}
fn size_hint(&self) -> Option<usize> {
match self.elements.size_hint() {
(lower, Some(upper)) if lower == upper => Some(upper),
_ => None,
}
}
}
struct MapAccess {
elements: VecDeque<(String, Value)>,
}
impl MapAccess {
fn new(table: HashMap<String, Value>) -> Self {
MapAccess {
elements: table.into_iter().collect(),
}
}
}
impl<'de> de::MapAccess<'de> for MapAccess {
type Error = ConfigError;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
where
K: de::DeserializeSeed<'de>,
{
if let Some((key_s, _)) = self.elements.front() {
let key_de = Value::new(None, key_s as &str);
let key = de::DeserializeSeed::deserialize(seed, key_de)?;
Ok(Some(key))
} else {
Ok(None)
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
where
V: de::DeserializeSeed<'de>,
{
let (key, value) = self.elements.pop_front().unwrap();
de::DeserializeSeed::deserialize(seed, value).map_err(|e| e.prepend_key(key))
}
}
struct EnumAccess {
value: Value,
name: &'static str,
variants: &'static [&'static str],
}
impl EnumAccess {
fn variant_deserializer(&self, name: &str) -> Result<StrDeserializer> {
self.variants
.iter()
.find(|&&s| s == name)
.map(|&s| StrDeserializer(s))
.ok_or_else(|| self.no_constructor_error(name))
}
fn table_deserializer(&self, table: &Table) -> Result<StrDeserializer> {
if table.len() == 1 {
self.variant_deserializer(table.iter().next().unwrap().0)
} else {
Err(self.structural_error())
}
}
fn no_constructor_error(&self, supposed_variant: &str) -> ConfigError {
ConfigError::Message(format!(
"enum {} does not have variant constructor {}",
self.name, supposed_variant
))
}
fn structural_error(&self) -> ConfigError {
ConfigError::Message(format!(
"value of enum {} should be represented by either string or table with exactly one key",
self.name
))
}
}
impl<'de> de::EnumAccess<'de> for EnumAccess {
type Error = ConfigError;
type Variant = Self;
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant)>
where
V: de::DeserializeSeed<'de>,
{
let value = {
let deserializer = match self.value.kind {
ValueKind::String(ref s) => self.variant_deserializer(s),
ValueKind::Table(ref t) => self.table_deserializer(t),
_ => Err(self.structural_error()),
}?;
seed.deserialize(deserializer)?
};
Ok((value, self))
}
}
impl<'de> de::VariantAccess<'de> for EnumAccess {
type Error = ConfigError;
fn unit_variant(self) -> Result<()> {
Ok(())
}
fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value>
where
T: de::DeserializeSeed<'de>,
{
match self.value.kind {
ValueKind::Table(t) => seed.deserialize(t.into_iter().next().unwrap().1),
_ => unreachable!(),
}
}
fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
match self.value.kind {
ValueKind::Table(t) => {
de::Deserializer::deserialize_seq(t.into_iter().next().unwrap().1, visitor)
}
_ => unreachable!(),
}
}
fn struct_variant<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
match self.value.kind {
ValueKind::Table(t) => {
de::Deserializer::deserialize_map(t.into_iter().next().unwrap().1, visitor)
}
_ => unreachable!(),
}
}
}
impl<'de> de::Deserializer<'de> for ConfigData {
type Error = ConfigError;
#[inline]
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Deserialize based on the underlying type.
match self.cache.kind {
ValueKind::Nil => visitor.visit_unit(),
ValueKind::Integer(i) => visitor.visit_i64(i),
ValueKind::Boolean(b) => visitor.visit_bool(b),
ValueKind::Float(f) => visitor.visit_f64(f),
ValueKind::String(s) => visitor.visit_string(s),
ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)),
ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)),
}
}
#[inline]
fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_bool(self.cache.into_bool()?)
}
#[inline]
fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i8(self.cache.into_int()? as i8)
}
#[inline]
fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i16(self.cache.into_int()? as i16)
}
#[inline]
fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i32(self.cache.into_int()? as i32)
}
#[inline]
fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_i64(self.cache.into_int()?)
}
#[inline]
fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u8(self.cache.into_int()? as u8)
}
#[inline]
fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u16(self.cache.into_int()? as u16)
}
#[inline]
fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u32(self.cache.into_int()? as u32)
}
#[inline]
fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u64(self.cache.into_int()? as u64)
}
#[inline]
fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f32(self.cache.into_float()? as f32)
}
#[inline]
fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f64(self.cache.into_float()?)
}
#[inline]
fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.cache.into_str()?)
}
#[inline]
fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.cache.into_str()?)
}
#[inline]
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Match an explicit nil as None and everything else as Some.
match self.cache.kind {
ValueKind::Nil => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_enum<V>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
visitor.visit_enum(EnumAccess {
value: self.cache,
name,
variants,
})
}
forward_to_deserialize_any! {
char seq
bytes byte_buf map struct unit newtype_struct
identifier ignored_any unit_struct tuple_struct tuple
}
}

222
pagetop/src/config/error.rs Normal file
View file

@ -0,0 +1,222 @@
use nom;
use serde::de;
use serde::ser;
use std::error::Error;
use std::fmt;
use std::result;
#[derive(Debug)]
pub enum Unexpected {
Bool(bool),
Integer(i64),
Float(f64),
Str(String),
Unit,
Seq,
Map,
}
impl fmt::Display for Unexpected {
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
match *self {
Unexpected::Bool(b) => write!(f, "boolean `{}`", b),
Unexpected::Integer(i) => write!(f, "integer `{}`", i),
Unexpected::Float(v) => write!(f, "floating point `{}`", v),
Unexpected::Str(ref s) => write!(f, "string {:?}", s),
Unexpected::Unit => write!(f, "unit value"),
Unexpected::Seq => write!(f, "sequence"),
Unexpected::Map => write!(f, "map"),
}
}
}
/// Represents all possible errors that can occur when working with configuration.
pub enum ConfigError {
/// Configuration is frozen and no further mutations can be made.
Frozen,
/// Configuration property was not found.
NotFound(String),
/// Configuration path could not be parsed.
PathParse(nom::error::ErrorKind),
/// Configuration could not be parsed from file.
FileParse {
/// The URI used to access the file (if not loaded from a string).
/// Example: `/path/to/config.json`
uri: Option<String>,
/// The captured error from attempting to parse the file in its desired format.
/// This is the actual error object from the library used for the parsing.
cause: Box<dyn Error + Send + Sync>,
},
/// Value could not be converted into the requested type.
Type {
/// The URI that references the source that the value came from.
/// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
// TODO: Why is this called Origin but FileParse has a uri field?
origin: Option<String>,
/// What we found when parsing the value.
unexpected: Unexpected,
/// What was expected when parsing the value.
expected: &'static str,
/// The key in the configuration hash of this value (if available where the error is
/// generated).
key: Option<String>,
},
/// Custom message.
Message(String),
/// Unadorned error from a foreign origin.
Foreign(Box<dyn Error + Send + Sync>),
}
impl ConfigError {
// FIXME: pub(crate).
#[doc(hidden)]
pub fn invalid_type(
origin: Option<String>,
unexpected: Unexpected,
expected: &'static str,
) -> Self {
ConfigError::Type {
origin,
unexpected,
expected,
key: None,
}
}
// FIXME: pub(crate).
#[doc(hidden)]
pub fn extend_with_key(self, key: &str) -> Self {
match self {
ConfigError::Type {
origin,
unexpected,
expected,
..
} => ConfigError::Type {
origin,
unexpected,
expected,
key: Some(key.into()),
},
_ => self,
}
}
fn prepend(self, segment: String, add_dot: bool) -> Self {
let concat = |key: Option<String>| {
let key = key.unwrap_or_default();
let dot = if add_dot && key.as_bytes().first().unwrap_or(&b'[') != &b'[' {
"."
} else {
""
};
format!("{}{}{}", segment, dot, key)
};
match self {
ConfigError::Type {
origin,
unexpected,
expected,
key,
} => ConfigError::Type {
origin,
unexpected,
expected,
key: Some(concat(key)),
},
ConfigError::NotFound(key) => ConfigError::NotFound(concat(Some(key))),
_ => self,
}
}
pub(crate) fn prepend_key(self, key: String) -> Self {
self.prepend(key, true)
}
pub(crate) fn prepend_index(self, idx: usize) -> Self {
self.prepend(format!("[{}]", idx), false)
}
}
/// Alias for a `Result` with the error type set to `ConfigError`.
pub type Result<T> = result::Result<T, ConfigError>;
// Forward Debug to Display for readable panic! messages.
impl fmt::Debug for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", *self)
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ConfigError::Frozen => write!(f, "configuration is frozen"),
ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()),
ConfigError::Message(ref s) => write!(f, "{}", s),
ConfigError::Foreign(ref cause) => write!(f, "{}", cause),
ConfigError::NotFound(ref key) => {
write!(f, "configuration property {:?} not found", key)
}
ConfigError::Type {
ref origin,
ref unexpected,
expected,
ref key,
} => {
write!(f, "invalid type: {}, expected {}", unexpected, expected)?;
if let Some(ref key) = *key {
write!(f, " for key `{}`", key)?;
}
if let Some(ref origin) = *origin {
write!(f, " in {}", origin)?;
}
Ok(())
}
ConfigError::FileParse { ref cause, ref uri } => {
write!(f, "{}", cause)?;
if let Some(ref uri) = *uri {
write!(f, " in {}", uri)?;
}
Ok(())
}
}
}
}
impl Error for ConfigError {}
impl de::Error for ConfigError {
fn custom<T: fmt::Display>(msg: T) -> Self {
ConfigError::Message(msg.to_string())
}
}
impl ser::Error for ConfigError {
fn custom<T: fmt::Display>(msg: T) -> Self {
ConfigError::Message(msg.to_string())
}
}

View file

@ -0,0 +1,85 @@
mod source;
mod toml;
use crate::config::error::*;
use crate::config::source::Source;
use crate::config::value::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use self::source::FileSource;
#[derive(Clone, Debug)]
pub struct File<T>
where
T: FileSource,
{
source: T,
/// A required File will error if it cannot be found.
required: bool,
}
impl File<source::FileSourceFile> {
/// Given the basename of a file, will attempt to locate a file by setting its extension to a
/// registered format.
pub fn with_name(name: &str) -> Self {
File {
source: source::FileSourceFile::new(name.into()),
required: true,
}
}
}
impl<'a> From<&'a Path> for File<source::FileSourceFile> {
fn from(path: &'a Path) -> Self {
File {
source: source::FileSourceFile::new(path.to_path_buf()),
required: true,
}
}
}
impl From<PathBuf> for File<source::FileSourceFile> {
fn from(path: PathBuf) -> Self {
File {
source: source::FileSourceFile::new(path),
required: true,
}
}
}
impl<T: FileSource> File<T> {
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
}
impl<T: FileSource> Source for File<T>
where
T: 'static,
T: Sync + Send,
{
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
// Coerce the file contents to a string.
let (uri, contents) = match self.source.resolve().map_err(ConfigError::Foreign) {
Ok((uri, contents)) => (uri, contents),
Err(error) => {
if !self.required {
return Ok(HashMap::new());
}
return Err(error);
}
};
// Parse the string using the given format.
toml::parse(uri.as_ref(), &contents).map_err(|cause| ConfigError::FileParse { uri, cause })
}
}

View file

@ -0,0 +1,126 @@
use std::env;
use std::error::Error;
use std::fmt::Debug;
use std::fs;
use std::io::{self, Read};
use std::iter::Iterator;
use std::path::{Path, PathBuf};
/// Describes where the file is sourced.
pub trait FileSource: Debug + Clone {
fn resolve(&self) -> Result<(Option<String>, String), Box<dyn Error + Send + Sync>>;
}
/// Describes a file sourced from a file.
#[derive(Clone, Debug)]
pub struct FileSourceFile {
/// Path of configuration file.
name: PathBuf,
}
impl FileSourceFile {
pub fn new(name: PathBuf) -> FileSourceFile {
FileSourceFile { name }
}
fn find_file(&self) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
// First check for an _exact_ match.
let mut filename = env::current_dir()?.as_path().join(self.name.clone());
if filename.is_file() {
if ["toml"].contains(
&filename
.extension()
.unwrap_or_default()
.to_string_lossy()
.as_ref(),
) {
return Ok(filename);
}
Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
format!(
"configuration file \"{}\" is not of a registered file format",
filename.to_string_lossy()
),
)))
} else {
filename.set_extension("toml");
if filename.is_file() {
return Ok(filename);
}
Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
format!(
"configuration file \"{}\" not found",
self.name.to_string_lossy()
),
)))
}
}
}
impl FileSource for FileSourceFile {
fn resolve(&self) -> Result<(Option<String>, String), Box<dyn Error + Send + Sync>> {
// Find file.
let filename = self.find_file()?;
// Attempt to use a relative path for the URI.
let base = env::current_dir()?;
let uri = match path_relative_from(&filename, &base) {
Some(value) => value,
None => filename.clone(),
};
// Read contents from file.
let mut file = fs::File::open(filename)?;
let mut text = String::new();
file.read_to_string(&mut text)?;
Ok((Some(uri.to_string_lossy().into_owned()), text))
}
}
// TODO: This should probably be a crate.
// https://github.com/rust-lang/rust/blob/master/src/librustc_trans/back/rpath.rs#L128
fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
use std::path::Component;
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Some(PathBuf::from(path))
} else {
None
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(Component::CurDir)) => comps.push(a),
(Some(_), Some(Component::ParentDir)) => return None,
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
}

View file

@ -0,0 +1,51 @@
use crate::config::value::{Value, ValueKind};
use toml;
use std::collections::HashMap;
use std::error::Error;
pub fn parse(
uri: Option<&String>,
text: &str,
) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> {
// Parse a TOML value from the provided text.
// TODO: Have a proper error fire if the root of a file is ever not a Table
let value = from_toml_value(uri, &toml::from_str(text)?);
match value.kind {
ValueKind::Table(map) => Ok(map),
_ => Ok(HashMap::new()),
}
}
fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value {
match *value {
toml::Value::String(ref value) => Value::new(uri, value.to_string()),
toml::Value::Float(value) => Value::new(uri, value),
toml::Value::Integer(value) => Value::new(uri, value),
toml::Value::Boolean(value) => Value::new(uri, value),
toml::Value::Table(ref table) => {
let mut m = HashMap::new();
for (key, value) in table {
m.insert(key.clone(), from_toml_value(uri, value));
}
Value::new(uri, m)
}
toml::Value::Array(ref array) => {
let mut l = Vec::new();
for value in array {
l.push(from_toml_value(uri, value));
}
Value::new(uri, l)
}
toml::Value::Datetime(ref datetime) => Value::new(uri, datetime.to_string()),
}
}

167
pagetop/src/config/path.rs Normal file
View file

@ -0,0 +1,167 @@
use crate::config::error::*;
use crate::config::value::{Value, ValueKind};
use std::collections::HashMap;
use std::str::FromStr;
mod parser;
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Expression {
Identifier(String),
Child(Box<Expression>, String),
Subscript(Box<Expression>, isize),
}
impl FromStr for Expression {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Expression> {
parser::from_str(s).map_err(ConfigError::PathParse)
}
}
fn sindex_to_uindex(index: isize, len: usize) -> usize {
if index >= 0 {
index as usize
} else {
len - (index.unsigned_abs())
}
}
impl Expression {
pub fn get_mut_forcibly<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> {
match *self {
Expression::Identifier(ref id) => match root.kind {
ValueKind::Table(ref mut map) => Some(
map.entry(id.clone())
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
),
_ => None,
},
Expression::Child(ref expr, ref key) => match expr.get_mut_forcibly(root) {
Some(value) => match value.kind {
ValueKind::Table(ref mut map) => Some(
map.entry(key.clone())
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
),
_ => {
*value = HashMap::<String, Value>::new().into();
if let ValueKind::Table(ref mut map) = value.kind {
Some(
map.entry(key.clone())
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
)
} else {
unreachable!();
}
}
},
_ => None,
},
Expression::Subscript(ref expr, index) => match expr.get_mut_forcibly(root) {
Some(value) => {
match value.kind {
ValueKind::Array(_) => (),
_ => *value = Vec::<Value>::new().into(),
}
match value.kind {
ValueKind::Array(ref mut array) => {
let index = sindex_to_uindex(index, array.len());
if index >= array.len() {
array.resize(index + 1, Value::new(None, ValueKind::Nil));
}
Some(&mut array[index])
}
_ => None,
}
}
_ => None,
},
}
}
pub fn set(&self, root: &mut Value, value: Value) {
match *self {
Expression::Identifier(ref id) => {
// Ensure that root is a table.
match root.kind {
ValueKind::Table(_) => {}
_ => {
*root = HashMap::<String, Value>::new().into();
}
}
match value.kind {
ValueKind::Table(ref incoming_map) => {
// Pull out another table.
let target = if let ValueKind::Table(ref mut map) = root.kind {
map.entry(id.clone())
.or_insert_with(|| HashMap::<String, Value>::new().into())
} else {
unreachable!();
};
// Continue the deep merge.
for (key, val) in incoming_map {
Expression::Identifier(key.clone()).set(target, val.clone());
}
}
_ => {
if let ValueKind::Table(ref mut map) = root.kind {
// Just do a simple set.
map.insert(id.clone(), value);
}
}
}
}
Expression::Child(ref expr, ref key) => {
if let Some(parent) = expr.get_mut_forcibly(root) {
match parent.kind {
ValueKind::Table(_) => {
Expression::Identifier(key.clone()).set(parent, value);
}
_ => {
// Didn't find a table. Oh well. Make a table and do this anyway.
*parent = HashMap::<String, Value>::new().into();
Expression::Identifier(key.clone()).set(parent, value);
}
}
}
}
Expression::Subscript(ref expr, index) => {
if let Some(parent) = expr.get_mut_forcibly(root) {
match parent.kind {
ValueKind::Array(_) => (),
_ => *parent = Vec::<Value>::new().into(),
}
if let ValueKind::Array(ref mut array) = parent.kind {
let uindex = sindex_to_uindex(index, array.len());
if uindex >= array.len() {
array.resize(uindex + 1, Value::new(None, ValueKind::Nil));
}
array[uindex] = value;
}
}
}
}
}
}

View file

@ -0,0 +1,131 @@
use super::Expression;
use nom::{
branch::alt,
bytes::complete::{is_a, tag},
character::complete::{char, digit1, space0},
combinator::{map, map_res, opt, recognize},
error::ErrorKind,
sequence::{delimited, pair, preceded},
Err, IResult,
};
use std::str::FromStr;
fn raw_ident(i: &str) -> IResult<&str, String> {
map(
is_a(
"abcdefghijklmnopqrstuvwxyz \
ABCDEFGHIJKLMNOPQRSTUVWXYZ \
0123456789 \
_-",
),
|s: &str| s.to_string(),
)(i)
}
fn integer(i: &str) -> IResult<&str, isize> {
map_res(
delimited(space0, recognize(pair(opt(tag("-")), digit1)), space0),
FromStr::from_str,
)(i)
}
fn ident(i: &str) -> IResult<&str, Expression> {
map(raw_ident, Expression::Identifier)(i)
}
fn postfix<'a>(expr: Expression) -> impl FnMut(&'a str) -> IResult<&'a str, Expression> {
let e2 = expr.clone();
let child = map(preceded(tag("."), raw_ident), move |id| {
Expression::Child(Box::new(expr.clone()), id)
});
let subscript = map(delimited(char('['), integer, char(']')), move |num| {
Expression::Subscript(Box::new(e2.clone()), num)
});
alt((child, subscript))
}
pub fn from_str(input: &str) -> Result<Expression, ErrorKind> {
match ident(input) {
Ok((mut rem, mut expr)) => {
while !rem.is_empty() {
match postfix(expr)(rem) {
Ok((rem_, expr_)) => {
rem = rem_;
expr = expr_;
}
// Forward Incomplete and Error
result => {
return result.map(|(_, o)| o).map_err(to_error_kind);
}
}
}
Ok(expr)
}
// Forward Incomplete and Error
result => result.map(|(_, o)| o).map_err(to_error_kind),
}
}
pub fn to_error_kind(e: Err<nom::error::Error<&str>>) -> ErrorKind {
match e {
Err::Incomplete(_) => ErrorKind::Complete,
Err::Failure(e) | Err::Error(e) => e.code,
}
}
#[cfg(test)]
mod test {
use super::Expression::*;
use super::*;
#[test]
fn test_id() {
let parsed: Expression = from_str("abcd").unwrap();
assert_eq!(parsed, Identifier("abcd".into()));
}
#[test]
fn test_id_dash() {
let parsed: Expression = from_str("abcd-efgh").unwrap();
assert_eq!(parsed, Identifier("abcd-efgh".into()));
}
#[test]
fn test_child() {
let parsed: Expression = from_str("abcd.efgh").unwrap();
let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into());
assert_eq!(parsed, expected);
let parsed: Expression = from_str("abcd.efgh.ijkl").unwrap();
let expected = Child(
Box::new(Child(Box::new(Identifier("abcd".into())), "efgh".into())),
"ijkl".into(),
);
assert_eq!(parsed, expected);
}
#[test]
fn test_subscript() {
let parsed: Expression = from_str("abcd[12]").unwrap();
let expected = Subscript(Box::new(Identifier("abcd".into())), 12);
assert_eq!(parsed, expected);
}
#[test]
fn test_subscript_neg() {
let parsed: Expression = from_str("abcd[-1]").unwrap();
let expected = Subscript(Box::new(Identifier("abcd".into())), -1);
assert_eq!(parsed, expected);
}
}

View file

@ -0,0 +1,87 @@
use crate::config::error::*;
use crate::config::path;
use crate::config::value::{Value, ValueKind};
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
/// Describes a generic _source_ of configuration properties.
pub trait Source: Debug {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync>;
/// Collect all configuration properties available from this source and return a HashMap.
fn collect(&self) -> Result<HashMap<String, Value>>;
fn collect_to(&self, cache: &mut Value) -> Result<()> {
let props = match self.collect() {
Ok(props) => props,
Err(error) => {
return Err(error);
}
};
for (key, val) in &props {
match path::Expression::from_str(key) {
// Set using the path.
Ok(expr) => expr.set(cache, val.clone()),
// Set diretly anyway.
_ => path::Expression::Identifier(key.clone()).set(cache, val.clone()),
}
}
Ok(())
}
}
impl Clone for Box<dyn Source + Send + Sync> {
fn clone(&self) -> Box<dyn Source + Send + Sync> {
self.clone_into_box()
}
}
impl Source for Vec<Box<dyn Source + Send + Sync>> {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
let mut cache: Value = HashMap::<String, Value>::new().into();
for source in self {
source.collect_to(&mut cache)?;
}
if let ValueKind::Table(table) = cache.kind {
Ok(table)
} else {
unreachable!();
}
}
}
impl<T> Source for Vec<T>
where
T: Source + Sync + Send,
T: Clone,
T: 'static,
{
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
let mut cache: Value = HashMap::<String, Value>::new().into();
for source in self {
source.collect_to(&mut cache)?;
}
if let ValueKind::Table(table) = cache.kind {
Ok(table)
} else {
unreachable!();
}
}
}

545
pagetop/src/config/value.rs Normal file
View file

@ -0,0 +1,545 @@
use crate::config::error::*;
use serde::de::{Deserialize, Deserializer, Visitor};
use std::collections::HashMap;
use std::fmt;
use std::fmt::Display;
/// Underlying kind of the configuration value.
#[derive(Clone, Debug, Default, PartialEq)]
pub enum ValueKind {
#[default]
Nil,
Boolean(bool),
Integer(i64),
Float(f64),
String(String),
Table(Table),
Array(Array),
}
pub type Array = Vec<Value>;
pub type Table = HashMap<String, Value>;
impl<T> From<Option<T>> for ValueKind
where
T: Into<ValueKind>,
{
fn from(value: Option<T>) -> Self {
match value {
Some(value) => value.into(),
None => ValueKind::Nil,
}
}
}
impl From<String> for ValueKind {
fn from(value: String) -> Self {
ValueKind::String(value)
}
}
impl<'a> From<&'a str> for ValueKind {
fn from(value: &'a str) -> Self {
ValueKind::String(value.into())
}
}
impl From<i64> for ValueKind {
fn from(value: i64) -> Self {
ValueKind::Integer(value)
}
}
impl From<f64> for ValueKind {
fn from(value: f64) -> Self {
ValueKind::Float(value)
}
}
impl From<bool> for ValueKind {
fn from(value: bool) -> Self {
ValueKind::Boolean(value)
}
}
impl<T> From<HashMap<String, T>> for ValueKind
where
T: Into<Value>,
{
fn from(values: HashMap<String, T>) -> Self {
let mut r = HashMap::new();
for (k, v) in values {
r.insert(k.clone(), v.into());
}
ValueKind::Table(r)
}
}
impl<T> From<Vec<T>> for ValueKind
where
T: Into<Value>,
{
fn from(values: Vec<T>) -> Self {
let mut l = Vec::new();
for v in values {
l.push(v.into());
}
ValueKind::Array(l)
}
}
impl Display for ValueKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ValueKind::String(ref value) => write!(f, "{}", value),
ValueKind::Boolean(value) => write!(f, "{}", value),
ValueKind::Integer(value) => write!(f, "{}", value),
ValueKind::Float(value) => write!(f, "{}", value),
ValueKind::Nil => write!(f, "nil"),
// TODO: Figure out a nice Display for these
ValueKind::Table(ref table) => write!(f, "{:?}", table),
ValueKind::Array(ref array) => write!(f, "{:?}", array),
}
}
}
/// A configuration value.
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Value {
/// A description of the original location of the value.
///
/// A Value originating from a File might contain:
/// ```text
/// Settings.toml
/// ```
///
/// A Value originating from the environment would contain:
/// ```text
/// the envrionment
/// ```
///
/// A Value originating from a remote source might contain:
/// ```text
/// etcd+http://127.0.0.1:2379
/// ```
origin: Option<String>,
/// Underlying kind of the configuration value.
pub kind: ValueKind,
}
impl Value {
/// Create a new value instance that will remember its source uri.
pub fn new<V>(origin: Option<&String>, kind: V) -> Self
where
V: Into<ValueKind>,
{
Value {
origin: origin.cloned(),
kind: kind.into(),
}
}
/// Attempt to deserialize this value into the requested type.
pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result<T> {
T::deserialize(self)
}
/// Returns `self` as a bool, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_bool(self) -> Result<bool> {
match self.kind {
ValueKind::Boolean(value) => Ok(value),
ValueKind::Integer(value) => Ok(value != 0),
ValueKind::Float(value) => Ok(value != 0.0),
ValueKind::String(ref value) => {
match value.to_lowercase().as_ref() {
"1" | "true" | "on" | "yes" => Ok(true),
"0" | "false" | "off" | "no" => Ok(false),
// Unexpected string value
s => Err(ConfigError::invalid_type(
self.origin.clone(),
Unexpected::Str(s.into()),
"a boolean",
)),
}
}
// Unexpected type
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a boolean",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"a boolean",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a boolean",
)),
}
}
/// Returns `self` into an i64, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_int(self) -> Result<i64> {
match self.kind {
ValueKind::Integer(value) => Ok(value),
ValueKind::String(ref s) => {
match s.to_lowercase().as_ref() {
"true" | "on" | "yes" => Ok(1),
"false" | "off" | "no" => Ok(0),
_ => {
s.parse().map_err(|_| {
// Unexpected string
ConfigError::invalid_type(
self.origin.clone(),
Unexpected::Str(s.clone()),
"an integer",
)
})
}
}
}
#[allow(clippy::bool_to_int_with_if)]
ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }),
ValueKind::Float(value) => Ok(value.round() as i64),
// Unexpected type
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"an integer",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"an integer",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"an integer",
)),
}
}
/// Returns `self` into a f64, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_float(self) -> Result<f64> {
match self.kind {
ValueKind::Float(value) => Ok(value),
ValueKind::String(ref s) => {
match s.to_lowercase().as_ref() {
"true" | "on" | "yes" => Ok(1.0),
"false" | "off" | "no" => Ok(0.0),
_ => {
s.parse().map_err(|_| {
// Unexpected string
ConfigError::invalid_type(
self.origin.clone(),
Unexpected::Str(s.clone()),
"a floating point",
)
})
}
}
}
ValueKind::Integer(value) => Ok(value as f64),
ValueKind::Boolean(value) => Ok(if value { 1.0 } else { 0.0 }),
// Unexpected type.
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a floating point",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"a floating point",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a floating point",
)),
}
}
/// Returns `self` into a str, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_str(self) -> Result<String> {
match self.kind {
ValueKind::String(value) => Ok(value),
ValueKind::Boolean(value) => Ok(value.to_string()),
ValueKind::Integer(value) => Ok(value.to_string()),
ValueKind::Float(value) => Ok(value.to_string()),
// Cannot convert.
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a string",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"a string",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a string",
)),
}
}
/// Returns `self` into an array, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_array(self) -> Result<Vec<Value>> {
match self.kind {
ValueKind::Array(value) => Ok(value),
// Cannot convert.
ValueKind::Float(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Float(value),
"an array",
)),
ValueKind::String(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Str(value),
"an array",
)),
ValueKind::Integer(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Integer(value),
"an array",
)),
ValueKind::Boolean(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Bool(value),
"an array",
)),
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"an array",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"an array",
)),
}
}
/// If the `Value` is a Table, returns the associated Map.
// FIXME: Should this not be `try_into_*` ?
pub fn into_table(self) -> Result<HashMap<String, Value>> {
match self.kind {
ValueKind::Table(value) => Ok(value),
// Cannot convert.
ValueKind::Float(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Float(value),
"a map",
)),
ValueKind::String(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Str(value),
"a map",
)),
ValueKind::Integer(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Integer(value),
"a map",
)),
ValueKind::Boolean(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Bool(value),
"a map",
)),
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a map",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a map",
)),
}
}
}
impl<'de> Deserialize<'de> for Value {
#[inline]
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Value, D::Error>
where
D: Deserializer<'de>,
{
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("any valid configuration value")
}
#[inline]
fn visit_bool<E>(self, value: bool) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_i8<E>(self, value: i8) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_i16<E>(self, value: i16) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_i32<E>(self, value: i32) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_i64<E>(self, value: i64) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_u8<E>(self, value: u8) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_u16<E>(self, value: u16) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_u32<E>(self, value: u32) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Value, E> {
// FIXME: This is bad
Ok((value as i64).into())
}
#[inline]
fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Value, E>
where
E: ::serde::de::Error,
{
self.visit_string(String::from(value))
}
#[inline]
fn visit_string<E>(self, value: String) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_none<E>(self) -> ::std::result::Result<Value, E> {
Ok(Value::new(None, ValueKind::Nil))
}
#[inline]
fn visit_some<D>(self, deserializer: D) -> ::std::result::Result<Value, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer)
}
#[inline]
fn visit_unit<E>(self) -> ::std::result::Result<Value, E> {
Ok(Value::new(None, ValueKind::Nil))
}
#[inline]
fn visit_seq<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
where
V: ::serde::de::SeqAccess<'de>,
{
let mut vec = Array::new();
while let Some(elem) = visitor.next_element()? {
vec.push(elem);
}
Ok(vec.into())
}
fn visit_map<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
where
V: ::serde::de::MapAccess<'de>,
{
let mut values = Table::new();
while let Some((key, value)) = visitor.next_entry()? {
values.insert(key, value);
}
Ok(values.into())
}
}
deserializer.deserialize_any(ValueVisitor)
}
}
impl<T> From<T> for Value
where
T: Into<ValueKind>,
{
fn from(value: T) -> Self {
Value {
origin: None,
kind: value.into(),
}
}
}
impl Display for Value {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.kind)
}
}

87
pagetop/src/core.rs Normal file
View file

@ -0,0 +1,87 @@
//! Key types and functions for creating actions, components, packages, and themes.
use crate::util::TypeInfo;
use std::any::Any;
/// A base trait that extends `Any` to provide metadata and dynamic type casting capabilities.
pub trait AnyBase: Any {
/// Returns the full name of the type.
fn type_name(&self) -> &'static str;
/// Returns a short name for the type.
fn short_name(&self) -> &'static str;
/// Returns a reference to `dyn Any` for dynamic type casting.
fn as_any_ref(&self) -> &dyn Any;
/// Returns a mutable reference to `dyn Any` for dynamic type casting.
fn as_any_mut(&mut self) -> &mut dyn Any;
}
#[allow(clippy::inline_always)]
impl<T: Any> AnyBase for T {
#[inline(always)]
fn type_name(&self) -> &'static str {
TypeInfo::FullName.of::<T>()
}
#[inline(always)]
fn short_name(&self) -> &'static str {
TypeInfo::ShortName.of::<T>()
}
#[inline(always)]
fn as_any_ref(&self) -> &dyn Any {
self
}
#[inline(always)]
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// A trait for advanced dynamic type manipulation and downcasting.
pub trait AnyTo: AnyBase {
/// Checks if the type is of the specified type `T`.
#[inline]
fn is<T>(&self) -> bool
where
T: AnyBase,
{
self.as_any_ref().is::<T>()
}
/// Attempts to downcast a reference to the specified type `T`.
#[inline]
fn downcast_ref<T>(&self) -> Option<&T>
where
T: AnyBase,
{
self.as_any_ref().downcast_ref()
}
/// Attempts to downcast a mutable reference to the specified type `T`.
#[inline]
fn downcast_mut<T>(&mut self) -> Option<&mut T>
where
T: AnyBase,
{
self.as_any_mut().downcast_mut()
}
}
impl<T: ?Sized + AnyBase> AnyTo for T {}
// API to define functions that alter the predefined behavior of the code.
pub mod action;
// API to build new components.
pub mod component;
// API to add new features with packages.
pub mod package;
// API to add new layouts with themes.
pub mod theme;

View file

@ -0,0 +1,19 @@
mod definition;
pub use definition::{ActionBase, ActionBox, ActionKey, ActionTrait};
mod list;
use list::ActionsList;
mod all;
pub(crate) use all::add_action;
pub use all::dispatch_actions;
#[macro_export]
macro_rules! actions {
() => {
Vec::<ActionBox>::new()
};
( $($action:expr),+ $(,)? ) => {{
vec![$(Box::new($action),)+]
}};
}

View file

@ -0,0 +1,30 @@
use crate::core::action::{ActionBox, ActionKey, ActionTrait, ActionsList};
use std::collections::HashMap;
use std::sync::{LazyLock, RwLock};
// Registered actions.
static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
pub fn add_action(action: ActionBox) {
let key = action.key();
let mut actions = ACTIONS.write().unwrap();
if let Some(list) = actions.get_mut(&key) {
list.add(action);
} else {
let mut list = ActionsList::new();
list.add(action);
actions.insert(key, list);
}
}
pub fn dispatch_actions<A, B, F>(key: &ActionKey, f: F)
where
A: ActionTrait,
F: FnMut(&A) -> B,
{
if let Some(list) = ACTIONS.read().unwrap().get(key) {
list.iter_map(f);
}
}

View file

@ -0,0 +1,61 @@
use crate::core::AnyBase;
use crate::{TypeId, Weight};
pub type ActionBox = Box<dyn ActionTrait>;
#[derive(Eq, PartialEq, Hash)]
pub struct ActionKey {
action_type_id: TypeId,
theme_type_id: Option<TypeId>,
referer_type_id: Option<TypeId>,
referer_id: Option<String>,
}
impl ActionKey {
pub fn new(
action_type_id: TypeId,
theme_type_id: Option<TypeId>,
referer_type_id: Option<TypeId>,
referer_id: Option<String>,
) -> Self {
ActionKey {
action_type_id,
theme_type_id,
referer_type_id,
referer_id,
}
}
}
pub trait ActionBase {
fn key(&self) -> ActionKey;
}
pub trait ActionTrait: ActionBase + AnyBase + Send + Sync {
fn theme_type_id(&self) -> Option<TypeId> {
None
}
fn referer_type_id(&self) -> Option<TypeId> {
None
}
fn referer_id(&self) -> Option<String> {
None
}
fn weight(&self) -> Weight {
0
}
}
impl<A: ActionTrait> ActionBase for A {
fn key(&self) -> ActionKey {
ActionKey {
action_type_id: self.type_id(),
theme_type_id: self.theme_type_id(),
referer_type_id: self.referer_type_id(),
referer_id: self.referer_id(),
}
}
}

View file

@ -0,0 +1,43 @@
use crate::core::action::{ActionBox, ActionTrait};
use crate::core::AnyTo;
use crate::trace;
use crate::AutoDefault;
use std::sync::RwLock;
#[derive(AutoDefault)]
pub struct ActionsList(RwLock<Vec<ActionBox>>);
impl ActionsList {
pub fn new() -> Self {
ActionsList::default()
}
pub fn add(&mut self, action: ActionBox) {
let mut list = self.0.write().unwrap();
list.push(action);
list.sort_by_key(|a| a.weight());
}
pub fn iter_map<A, B, F>(&self, mut f: F)
where
Self: Sized,
A: ActionTrait,
F: FnMut(&A) -> B,
{
let _: Vec<_> = self
.0
.read()
.unwrap()
.iter()
.rev()
.map(|a| {
if let Some(action) = (**a).downcast_ref::<A>() {
f(action);
} else {
trace::error!("Failed to downcast action of type {}", (**a).type_name());
}
})
.collect();
}
}

View file

@ -0,0 +1,14 @@
mod context;
pub use context::{AssetsOp, Context, ErrorParam};
pub type FnContextualPath = fn(cx: &Context) -> &str;
mod definition;
pub use definition::{ComponentBase, ComponentTrait};
mod classes;
pub use classes::{ComponentClasses, ComponentClassesOp};
mod children;
pub use children::Children;
pub use children::{ChildComponent, ChildOp};
pub use children::{TypedComponent, TypedOp};

View file

@ -0,0 +1,213 @@
use crate::core::component::{ComponentTrait, Context};
use crate::html::{html, Markup};
use crate::{fn_builder, TypeId};
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct ChildComponent(Arc<RwLock<dyn ComponentTrait>>);
impl ChildComponent {
pub fn with(component: impl ComponentTrait) -> Self {
ChildComponent(Arc::new(RwLock::new(component)))
}
// ChildComponent RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
self.0.write().unwrap().render(cx)
}
// ChildComponent HELPERS.
fn type_id(&self) -> TypeId {
self.0.read().unwrap().type_id()
}
fn id(&self) -> String {
self.0.read().unwrap().id().unwrap_or_default()
}
}
// *************************************************************************************************
pub struct TypedComponent<C: ComponentTrait>(Arc<RwLock<C>>);
impl<C: ComponentTrait> Clone for TypedComponent<C> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<C: ComponentTrait> TypedComponent<C> {
pub fn with(component: C) -> Self {
TypedComponent(Arc::new(RwLock::new(component)))
}
// TypedComponent RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
self.0.write().unwrap().render(cx)
}
// TypedComponent HELPERS.
fn to_child(&self) -> ChildComponent {
ChildComponent(self.0.clone())
}
}
// *************************************************************************************************
pub enum ChildOp {
Add(ChildComponent),
InsertAfterId(&'static str, ChildComponent),
InsertBeforeId(&'static str, ChildComponent),
Prepend(ChildComponent),
RemoveById(&'static str),
ReplaceById(&'static str, ChildComponent),
Reset,
}
pub enum TypedOp<C: ComponentTrait> {
Add(TypedComponent<C>),
InsertAfterId(&'static str, TypedComponent<C>),
InsertBeforeId(&'static str, TypedComponent<C>),
Prepend(TypedComponent<C>),
RemoveById(&'static str),
ReplaceById(&'static str, TypedComponent<C>),
Reset,
}
#[derive(Clone, Default)]
pub struct Children(Vec<ChildComponent>);
impl Children {
pub fn new() -> Self {
Children::default()
}
pub fn with(child: ChildComponent) -> Self {
Children::default().with_value(ChildOp::Add(child))
}
pub(crate) fn merge(mixes: &[Option<&Children>]) -> Self {
let mut opt = Children::default();
for m in mixes.iter().flatten() {
opt.0.append(&mut m.0.clone());
}
opt
}
// Children BUILDER.
#[fn_builder]
pub fn set_value(&mut self, op: ChildOp) -> &mut Self {
match op {
ChildOp::Add(any) => self.add(any),
ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any),
ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any),
ChildOp::Prepend(any) => self.prepend(any),
ChildOp::RemoveById(id) => self.remove_by_id(id),
ChildOp::ReplaceById(id, any) => self.replace_by_id(id, any),
ChildOp::Reset => self.reset(),
};
self
}
#[fn_builder]
pub fn set_typed<C: ComponentTrait + Default>(&mut self, op: TypedOp<C>) -> &mut Self {
match op {
TypedOp::Add(typed) => self.add(typed.to_child()),
TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.to_child()),
TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.to_child()),
TypedOp::Prepend(typed) => self.prepend(typed.to_child()),
TypedOp::RemoveById(id) => self.remove_by_id(id),
TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.to_child()),
TypedOp::Reset => self.reset(),
};
self
}
#[inline]
fn add(&mut self, child: ChildComponent) {
self.0.push(child);
}
#[inline]
fn insert_after_id(&mut self, id: &str, child: ChildComponent) {
match self.0.iter().position(|c| c.id() == id) {
Some(index) => self.0.insert(index + 1, child),
_ => self.0.push(child),
};
}
#[inline]
fn insert_before_id(&mut self, id: &str, child: ChildComponent) {
match self.0.iter().position(|c| c.id() == id) {
Some(index) => self.0.insert(index, child),
_ => self.0.insert(0, child),
};
}
#[inline]
fn prepend(&mut self, child: ChildComponent) {
self.0.insert(0, child);
}
#[inline]
fn remove_by_id(&mut self, id: &str) {
if let Some(index) = self.0.iter().position(|c| c.id() == id) {
self.0.remove(index);
}
}
#[inline]
fn replace_by_id(&mut self, id: &str, child: ChildComponent) {
for c in &mut self.0 {
if c.id() == id {
*c = child;
break;
}
}
}
#[inline]
fn reset(&mut self) {
self.0.clear();
}
// Children GETTERS.
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get_by_id(&self, id: impl Into<String>) -> Option<&ChildComponent> {
let id = id.into();
self.0.iter().find(|c| c.id() == id)
}
pub fn iter_by_id(&self, id: impl Into<String>) -> impl Iterator<Item = &ChildComponent> {
let id = id.into();
self.0.iter().filter(move |&c| c.id() == id)
}
pub fn iter_by_type_id(&self, type_id: TypeId) -> impl Iterator<Item = &ChildComponent> {
self.0.iter().filter(move |&c| c.type_id() == type_id)
}
// Children RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
html! {
@for c in &self.0 {
(c.render(cx))
}
}
}
}

View file

@ -0,0 +1,19 @@
use crate::core::component::ComponentBase;
use crate::html::{ClassesOp, OptionClasses};
pub trait ComponentClassesOp {
fn with_classes(self, op: ClassesOp, classes: impl Into<String>) -> Self;
}
pub trait ComponentClasses: ComponentBase + ComponentClassesOp {
fn set_classes(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self;
fn classes(&self) -> &OptionClasses;
}
impl<C: ComponentBase + ComponentClasses> ComponentClassesOp for C {
fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.set_classes(op, classes);
self
}
}

View file

@ -0,0 +1,199 @@
use crate::concat_string;
use crate::core::component::ChildOp;
use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME};
use crate::core::theme::{ChildrenInRegions, ThemeRef};
use crate::html::{html, Markup};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
use crate::service::HttpRequest;
use crate::util::TypeInfo;
use std::collections::HashMap;
use std::error::Error;
use std::str::FromStr;
use std::fmt;
pub enum AssetsOp {
LangId(&'static LanguageIdentifier),
Theme(&'static str),
Layout(&'static str),
// Favicon.
SetFavicon(Option<Favicon>),
SetFaviconIfNone(Favicon),
// Stylesheets.
AddStyleSheet(StyleSheet),
RemoveStyleSheet(&'static str),
// JavaScripts.
AddJavaScript(JavaScript),
RemoveJavaScript(&'static str),
}
#[derive(Debug)]
pub enum ErrorParam {
NotFound,
ParseError(String),
}
impl fmt::Display for ErrorParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorParam::NotFound => write!(f, "Parameter not found"),
ErrorParam::ParseError(e) => write!(f, "Parse error: {e}"),
}
}
}
impl Error for ErrorParam {}
#[rustfmt::skip]
pub struct Context {
request : HttpRequest,
langid : &'static LanguageIdentifier,
theme : ThemeRef,
layout : &'static str,
favicon : Option<Favicon>,
stylesheet: Assets<StyleSheet>,
javascript: Assets<JavaScript>,
regions : ChildrenInRegions,
params : HashMap<&'static str, String>,
id_counter: usize,
}
impl Context {
#[rustfmt::skip]
pub(crate) fn new(request: HttpRequest) -> Self {
Context {
request,
langid : &DEFAULT_LANGID,
theme : *DEFAULT_THEME,
layout : "default",
favicon : None,
stylesheet: Assets::<StyleSheet>::new(),
javascript: Assets::<JavaScript>::new(),
regions : ChildrenInRegions::default(),
params : HashMap::<&str, String>::new(),
id_counter: 0,
}
}
pub fn set_assets(&mut self, op: AssetsOp) -> &mut Self {
match op {
AssetsOp::LangId(langid) => {
self.langid = langid;
}
AssetsOp::Theme(theme_name) => {
self.theme = theme_by_short_name(theme_name).unwrap_or(*DEFAULT_THEME);
}
AssetsOp::Layout(layout) => {
self.layout = layout;
}
// Favicon.
AssetsOp::SetFavicon(favicon) => {
self.favicon = favicon;
}
AssetsOp::SetFaviconIfNone(icon) => {
if self.favicon.is_none() {
self.favicon = Some(icon);
}
}
// Stylesheets.
AssetsOp::AddStyleSheet(css) => {
self.stylesheet.add(css);
}
AssetsOp::RemoveStyleSheet(path) => {
self.stylesheet.remove(path);
}
// JavaScripts.
AssetsOp::AddJavaScript(js) => {
self.javascript.add(js);
}
AssetsOp::RemoveJavaScript(path) => {
self.javascript.remove(path);
}
}
self
}
pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self {
self.regions.set_in_region(region, op);
self
}
pub fn set_param<T: FromStr + ToString>(&mut self, key: &'static str, value: &T) -> &mut Self {
self.params.insert(key, value.to_string());
self
}
// Context GETTERS.
pub fn request(&self) -> &HttpRequest {
&self.request
}
pub fn langid(&self) -> &LanguageIdentifier {
self.langid
}
pub fn theme(&self) -> ThemeRef {
self.theme
}
pub fn layout(&self) -> &str {
self.layout
}
pub fn regions(&self) -> &ChildrenInRegions {
&self.regions
}
pub fn get_param<T: FromStr + ToString>(&self, key: &'static str) -> Result<T, ErrorParam> {
self.params
.get(key)
.ok_or(ErrorParam::NotFound)
.and_then(|v| T::from_str(v).map_err(|_| ErrorParam::ParseError(v.clone())))
}
// Context RENDER.
pub fn render_assets(&mut self) -> Markup {
html! {
@if let Some(favicon) = &self.favicon {
(favicon.render())
}
(self.stylesheet.render())
(self.javascript.render())
}
}
pub fn render_region(&mut self, region: impl Into<String>) -> Markup {
self.regions
.all_in_region(self.theme, &region.into())
.render(self)
}
// Context EXTRAS.
pub fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
}
pub fn required_id<T>(&mut self, id: Option<String>) -> String {
if let Some(id) = id {
id
} else {
let prefix = TypeInfo::ShortName
.of::<T>()
.trim()
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
"prefix".to_owned()
} else {
prefix
};
self.id_counter += 1;
concat_string!(prefix, "-", self.id_counter.to_string())
}
}
}

View file

@ -0,0 +1,66 @@
use crate::base::action;
use crate::core::component::Context;
use crate::core::AnyBase;
use crate::html::{html, Markup, PrepareMarkup};
use crate::util::TypeInfo;
pub trait ComponentBase {
fn render(&mut self, cx: &mut Context) -> Markup;
}
pub trait ComponentTrait: AnyBase + ComponentBase + Send + Sync {
fn new() -> Self
where
Self: Sized;
fn name(&self) -> &'static str {
TypeInfo::ShortName.of::<Self>()
}
fn description(&self) -> Option<String> {
None
}
fn id(&self) -> Option<String> {
None
}
#[allow(unused_variables)]
fn setup_before_prepare(&mut self, cx: &mut Context) {}
#[allow(unused_variables)]
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::None
}
}
impl<C: ComponentTrait> ComponentBase for C {
fn render(&mut self, cx: &mut Context) -> Markup {
if action::component::IsRenderable::dispatch(self, cx) {
// Comprueba el componente antes de prepararlo.
self.setup_before_prepare(cx);
// Acciones del tema antes de preparar el componente.
action::theme::BeforePrepare::dispatch(self, cx);
// Acciones de los módulos antes de preparar el componente.
action::component::BeforePrepare::dispatch(self, cx);
// Renderiza el componente.
let markup = match action::theme::RenderComponent::dispatch(self, cx) {
Some(html) => html,
None => self.prepare_component(cx).render(),
};
// Acciones del tema después de preparar el componente.
action::theme::AfterPrepare::dispatch(self, cx);
// Acciones de los módulos después de preparar el componente.
action::component::AfterPrepare::dispatch(self, cx);
markup
} else {
html! {}
}
}
}

View file

@ -0,0 +1,4 @@
mod definition;
pub use definition::{PackageRef, PackageTrait};
pub(crate) mod all;

View file

@ -0,0 +1,134 @@
use crate::core::action::add_action;
use crate::core::package::PackageRef;
use crate::core::theme::all::THEMES;
use crate::{global, include_files, include_files_service, service, trace};
use std::sync::{LazyLock, RwLock};
// PACKAGES ****************************************************************************************
static ENABLED_PACKAGES: LazyLock<RwLock<Vec<PackageRef>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
static DROPPED_PACKAGES: LazyLock<RwLock<Vec<PackageRef>>> =
LazyLock::new(|| RwLock::new(Vec::new()));
// REGISTER PACKAGES *******************************************************************************
pub fn register_packages(root_package: Option<PackageRef>) {
// Initialize a list for packages to be enabled.
let mut enabled_list: Vec<PackageRef> = Vec::new();
// Add default theme to the enabled list.
add_to_enabled(&mut enabled_list, &crate::base::theme::Basic);
// If a root package is provided, add it to the enabled list.
if let Some(package) = root_package {
add_to_enabled(&mut enabled_list, package);
}
// Add default welcome page package to the enabled list.
add_to_enabled(&mut enabled_list, &crate::base::package::Welcome);
// Save the final list of enabled packages.
ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list);
// Initialize a list for packages to be dropped.
let mut dropped_list: Vec<PackageRef> = Vec::new();
// If a root package is provided, analyze its dropped list.
if let Some(package) = root_package {
add_to_dropped(&mut dropped_list, package);
}
// Save the final list of dropped packages.
DROPPED_PACKAGES.write().unwrap().append(&mut dropped_list);
}
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 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();
// Ensure the theme is not already registered to avoid duplicates.
if !registered_themes
.iter()
.any(|t| t.type_id() == theme.type_id())
{
registered_themes.push(theme);
trace::debug!("Enabling \"{}\" theme", theme.short_name());
}
} else {
trace::debug!("Enabling \"{}\" package", package.short_name());
}
}
}
fn add_to_dropped(list: &mut Vec<PackageRef>, package: PackageRef) {
// Iterate through packages recommended to be dropped.
for d in &package.drop_packages() {
// Check if the package is not already in the dropped list.
if !list.iter().any(|p| p.type_id() == d.type_id()) {
// Check if the package is currently enabled. If so, log a warning.
if ENABLED_PACKAGES
.read()
.unwrap()
.iter()
.any(|p| p.type_id() == package.type_id())
{
trace::warn!(
"Trying to drop \"{}\" package which is enabled",
package.short_name()
);
} else {
// If the package is not enabled, add it to the dropped list and log the action.
list.push(*d);
trace::debug!("Package \"{}\" dropped", d.short_name());
// Recursively add the dependencies of the dropped package to the dropped list.
// This ensures that all dependencies are also considered for dropping.
for dependency in &package.dependencies() {
add_to_dropped(list, *dependency);
}
}
}
}
}
// REGISTER ACTIONS ********************************************************************************
pub fn register_actions() {
for m in ENABLED_PACKAGES.read().unwrap().iter() {
for a in m.actions().into_iter() {
add_action(a);
}
}
}
// INIT PACKAGES ***********************************************************************************
pub fn init_packages() {
trace::info!("Calling application bootstrap");
for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.init();
}
}
// 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);
}
include_files_service!(
scfg, assets => "/", [&global::SETTINGS.dev.pagetop_project_dir, "static"]
);
}

View file

@ -0,0 +1,39 @@
use crate::core::action::ActionBox;
use crate::core::theme::ThemeRef;
use crate::core::AnyBase;
use crate::locale::L10n;
use crate::{actions, service};
pub type PackageRef = &'static dyn PackageTrait;
/// Los paquetes deben implementar este *trait*.
pub trait PackageTrait: AnyBase + Send + Sync {
fn name(&self) -> L10n {
L10n::n(self.short_name())
}
fn description(&self) -> L10n {
L10n::default()
}
fn theme(&self) -> Option<ThemeRef> {
None
}
fn dependencies(&self) -> Vec<PackageRef> {
vec![]
}
fn drop_packages(&self) -> Vec<PackageRef> {
vec![]
}
fn actions(&self) -> Vec<ActionBox> {
actions![]
}
fn init(&self) {}
#[allow(unused_variables)]
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
}

View file

@ -0,0 +1,8 @@
mod definition;
pub use definition::{ThemeRef, ThemeTrait};
mod regions;
pub(crate) use regions::ChildrenInRegions;
pub use regions::InRegion;
pub(crate) mod all;

View file

@ -0,0 +1,31 @@
use crate::core::theme::ThemeRef;
use crate::global;
use std::sync::{LazyLock, RwLock};
// THEMES ******************************************************************************************
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
// DEFAULT THEME ***********************************************************************************
pub static DEFAULT_THEME: LazyLock<ThemeRef> =
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
Some(theme) => theme,
None => &crate::base::theme::Basic,
});
// THEME BY NAME ***********************************************************************************
pub fn theme_by_short_name(short_name: &str) -> Option<ThemeRef> {
let short_name = short_name.to_lowercase();
match THEMES
.read()
.unwrap()
.iter()
.find(|t| t.short_name().to_lowercase() == short_name)
{
Some(theme) => Some(*theme),
_ => None,
}
}

View file

@ -0,0 +1,59 @@
use crate::core::package::PackageTrait;
use crate::global;
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::response::page::Page;
pub type ThemeRef = &'static dyn ThemeTrait;
/// Los temas deben implementar este "trait".
pub trait ThemeTrait: PackageTrait + Send + Sync {
fn regions(&self) -> Vec<(&'static str, L10n)> {
vec![("content", L10n::l("content"))]
}
fn render_head(&self, page: &mut Page) -> Markup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
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().render_assets())
}
}
}
#[allow(unused_variables)]
fn before_render_body(&self, page: &mut Page) {}
fn render_body(&self, page: &mut Page) -> Markup {
html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
(page.context().render_region("content"))
}
}
}
#[allow(unused_variables)]
fn after_render_body(&self, page: &mut Page) {}
}

View file

@ -0,0 +1,78 @@
use crate::core::component::{ChildComponent, ChildOp, Children};
use crate::core::theme::ThemeRef;
use crate::{fn_builder, AutoDefault, TypeId};
use std::collections::HashMap;
use std::sync::{LazyLock, RwLock};
static THEME_REGIONS: LazyLock<RwLock<HashMap<TypeId, ChildrenInRegions>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
LazyLock::new(|| RwLock::new(ChildrenInRegions::default()));
#[derive(AutoDefault)]
pub struct ChildrenInRegions(HashMap<&'static str, Children>);
impl ChildrenInRegions {
pub fn new() -> Self {
ChildrenInRegions::default()
}
pub fn with(region: &'static str, child: ChildComponent) -> Self {
ChildrenInRegions::default().with_in_region(region, ChildOp::Add(child))
}
#[fn_builder]
pub fn set_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self {
if let Some(region) = self.0.get_mut(region) {
region.set_value(op);
} else {
self.0.insert(region, Children::new().with_value(op));
}
self
}
pub fn all_in_region(&self, theme: ThemeRef, region: &str) -> Children {
let common = COMMON_REGIONS.read().unwrap();
if let Some(r) = THEME_REGIONS.read().unwrap().get(&theme.type_id()) {
Children::merge(&[common.0.get(region), self.0.get(region), r.0.get(region)])
} else {
Children::merge(&[common.0.get(region), self.0.get(region)])
}
}
}
pub enum InRegion {
Content,
Named(&'static str),
OfTheme(&'static str, ThemeRef),
}
impl InRegion {
pub fn add(&self, child: ChildComponent) -> &Self {
match self {
InRegion::Content => {
COMMON_REGIONS
.write()
.unwrap()
.set_in_region("content", ChildOp::Add(child));
}
InRegion::Named(name) => {
COMMON_REGIONS
.write()
.unwrap()
.set_in_region(name, ChildOp::Add(child));
}
InRegion::OfTheme(region, theme) => {
let mut regions = THEME_REGIONS.write().unwrap();
if let Some(r) = regions.get_mut(&theme.type_id()) {
r.set_in_region(region, ChildOp::Add(child));
} else {
regions.insert(theme.type_id(), ChildrenInRegions::with(region, child));
}
}
}
self
}
}

4
pagetop/src/datetime.rs Normal file
View file

@ -0,0 +1,4 @@
//! [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time handling
//! ([chrono](https://docs.rs/chrono)).
pub use chrono::prelude::*;

121
pagetop/src/global.rs Normal file
View file

@ -0,0 +1,121 @@
//! Global settings.
use crate::include_config;
use serde::Deserialize;
include_config!(SETTINGS: Settings => [
// [app]
"app.name" => "My App",
"app.description" => "Developed with the amazing PageTop framework.",
"app.theme" => "",
"app.language" => "en-US",
"app.text_direction" => "ltr",
"app.startup_banner" => "Slant",
// [dev]
"dev.pagetop_project_dir" => "",
// [log]
"log.tracing" => "Info",
"log.rolling" => "Stdout",
"log.path" => "log",
"log.prefix" => "tracing.log",
"log.format" => "Full",
// [server]
"server.bind_address" => "localhost",
"server.bind_port" => 8088,
"server.session_lifetime" => 604_800,
]);
#[derive(Debug, Deserialize)]
/// Configuration settings for the global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and
/// [`[server]`](Server) sections (see [`SETTINGS`]).
pub struct Settings {
pub app: App,
pub dev: Dev,
pub log: Log,
pub server: Server,
}
#[derive(Debug, Deserialize)]
/// Section `[app]` of the configuration settings.
///
/// See [`Settings`].
pub struct App {
/// The name of the application.
/// Default: *"My App"*.
pub name: String,
/// A brief description of the application.
/// Default: *"Developed with the amazing PageTop framework."*.
pub description: String,
/// Default theme.
/// Default: *""*.
pub theme: String,
/// Default language (localization).
/// Default: *"en-US"*.
pub language: String,
/// Default text direction: *"ltr"* (left-to-right), *"rtl"* (right-to-left), or *"auto"*.
/// Default: *"ltr"*.
pub text_direction: String,
/// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*.
/// Default: *"Slant"*.
pub startup_banner: String,
/// Default: according to the `PAGETOP_RUN_MODE` environment variable, or *"default"* if unset.
pub run_mode: String,
}
#[derive(Debug, Deserialize)]
/// Section `[dev]` of the configuration settings.
///
/// See [`Settings`].
pub struct Dev {
/// Static files required by the application are integrated by default into the executable
/// binary. However, during development, it can be useful to serve these files from their own
/// directory to avoid recompilation every time they are modified. In this case, specify the
/// full path to the project's root directory.
/// Default: *""*.
pub pagetop_project_dir: String,
}
#[derive(Debug, Deserialize)]
/// Section `[log]` of the configuration settings.
///
/// See [`Settings`].
pub struct Log {
/// Filter, or a comma-separated combination of filters, for execution traces: *"Error"*,
/// *"Warn"*, *"Info"*, *"Debug"*, or *"Trace"*.
/// Example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
/// Default: *"Info"*.
pub tracing: String,
/// Displays traces in the terminal (*"Stdout"*) or logs them in files with rotation: *"Daily"*,
/// *"Hourly"*, *"Minutely"*, or *"Endless"*.
/// Default: *"Stdout"*.
pub rolling: String,
/// Directory for trace files (if `rolling` != *"Stdout"*).
/// Default: *"log"*.
pub path: String,
/// Prefix for trace files (if `rolling` != *"Stdout"*).
/// Default: *"tracing.log"*.
pub prefix: String,
/// Trace output format. Options are *"Full"*, *"Compact"*, *"Pretty"*, or *"Json"*.
/// Default: *"Full"*.
pub format: String,
}
#[derive(Debug, Deserialize)]
/// Section `[server]` of the configuration settings.
///
/// See [`Settings`].
pub struct Server {
/// Web server bind address.
/// Default: *"localhost"*.
pub bind_address: String,
/// Web server bind port.
/// Default: *8088*.
pub bind_port: u16,
/// Session cookie duration in seconds (0 means "until the browser is closed").
/// Default: *604800* (7 days).
pub session_lifetime: i64,
}

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

@ -0,0 +1,52 @@
//! 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};
mod opt_component;
pub use opt_component::OptionComponent;
pub mod unit;
use crate::AutoDefault;
#[derive(AutoDefault)]
pub enum PrepareMarkup {
#[default]
None,
Text(&'static str),
Escaped(String),
With(Markup),
}
impl PrepareMarkup {
pub fn render(&self) -> Markup {
match self {
PrepareMarkup::None => html! {},
PrepareMarkup::Text(text) => html! { (text) },
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 render(&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 render(&mut self) -> Markup {
let assets = &mut self.0;
assets.sort_by_key(AssetsTrait::weight);
html! {
@for a in assets {
(a.render())
}
}
}
}

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 render(&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 render(&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 render(&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
}
}

350
pagetop/src/html/maud.rs Normal file
View file

@ -0,0 +1,350 @@
//#![no_std]
//! A macro for writing HTML templates.
//!
//! This documentation only describes the runtime API. For a general
//! guide, check out the [book] instead.
//!
//! [book]: https://maud.lambda.xyz/
//#![doc(html_root_url = "https://docs.rs/maud/0.25.0")]
extern crate alloc;
use alloc::{borrow::Cow, boxed::Box, string::String};
use core::fmt::{self, Arguments, Display, Write};
pub use pagetop_macros::html;
mod escape;
/// An adapter that escapes HTML special characters.
///
/// The following characters are escaped:
///
/// * `&` is escaped as `&amp;`
/// * `<` is escaped as `&lt;`
/// * `>` is escaped as `&gt;`
/// * `"` is escaped as `&quot;`
///
/// All other characters are passed through unchanged.
///
/// **Note:** In versions prior to 0.13, the single quote (`'`) was
/// escaped as well.
///
/// # Example
///
/// ```rust
/// use maud::Escaper;
/// use std::fmt::Write;
/// let mut s = String::new();
/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap();
/// assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
/// ```
pub struct Escaper<'a>(&'a mut String);
impl<'a> Escaper<'a> {
/// Creates an `Escaper` from a `String`.
pub fn new(buffer: &'a mut String) -> Escaper<'a> {
Escaper(buffer)
}
}
impl<'a> fmt::Write for Escaper<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
escape::escape_to_string(s, self.0);
Ok(())
}
}
/// Represents a type that can be rendered as HTML.
///
/// To implement this for your own type, override either the `.render()`
/// or `.render_to()` methods; since each is defined in terms of the
/// other, you only need to implement one of them. See the example below.
///
/// # Minimal implementation
///
/// An implementation of this trait must override at least one of
/// `.render()` or `.render_to()`. Since the default definitions of
/// these methods call each other, not doing this will result in
/// infinite recursion.
///
/// # Example
///
/// ```rust
/// use maud::{html, Markup, Render};
///
/// /// Provides a shorthand for linking to a CSS stylesheet.
/// pub struct Stylesheet(&'static str);
///
/// impl Render for Stylesheet {
/// fn render(&self) -> Markup {
/// html! {
/// link rel="stylesheet" type="text/css" href=(self.0);
/// }
/// }
/// }
/// ```
pub trait Render {
/// Renders `self` as a block of `Markup`.
fn render(&self) -> Markup {
let mut buffer = String::new();
self.render_to(&mut buffer);
PreEscaped(buffer)
}
/// Appends a representation of `self` to the given buffer.
///
/// Its default implementation just calls `.render()`, but you may
/// override it with something more efficient.
///
/// Note that no further escaping is performed on data written to
/// the buffer. If you override this method, you must make sure that
/// any data written is properly escaped, whether by hand or using
/// the [`Escaper`](struct.Escaper.html) wrapper struct.
fn render_to(&self, buffer: &mut String) {
buffer.push_str(&self.render().into_string());
}
}
impl Render for str {
fn render_to(&self, w: &mut String) {
escape::escape_to_string(self, w);
}
}
impl Render for String {
fn render_to(&self, w: &mut String) {
str::render_to(self, w);
}
}
impl<'a> Render for Cow<'a, str> {
fn render_to(&self, w: &mut String) {
str::render_to(self, w);
}
}
impl<'a> Render for Arguments<'a> {
fn render_to(&self, w: &mut String) {
let _ = Escaper::new(w).write_fmt(*self);
}
}
impl<'a, T: Render + ?Sized> Render for &'a T {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}
impl<'a, T: Render + ?Sized> Render for &'a mut T {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}
impl<T: Render + ?Sized> Render for Box<T> {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}
macro_rules! impl_render_with_display {
($($ty:ty)*) => {
$(
impl Render for $ty {
fn render_to(&self, w: &mut String) {
// TODO: remove the explicit arg when Rust 1.58 is released
format_args!("{self}", self = self).render_to(w);
}
}
)*
};
}
impl_render_with_display! {
char f32 f64
}
macro_rules! impl_render_with_itoa {
($($ty:ty)*) => {
$(
impl Render for $ty {
fn render_to(&self, w: &mut String) {
w.push_str(itoa::Buffer::new().format(*self));
}
}
)*
};
}
impl_render_with_itoa! {
i8 i16 i32 i64 i128 isize
u8 u16 u32 u64 u128 usize
}
/// Renders a value using its [`Display`] impl.
///
/// # Example
///
/// ```rust
/// use maud::html;
/// use std::net::Ipv4Addr;
///
/// let ip_address = Ipv4Addr::new(127, 0, 0, 1);
///
/// let markup = html! {
/// "My IP address is: "
/// (maud::display(ip_address))
/// };
///
/// assert_eq!(markup.into_string(), "My IP address is: 127.0.0.1");
/// ```
pub fn display(value: impl Display) -> impl Render {
struct DisplayWrapper<T>(T);
impl<T: Display> Render for DisplayWrapper<T> {
fn render_to(&self, w: &mut String) {
format_args!("{0}", self.0).render_to(w);
}
}
DisplayWrapper(value)
}
/// A wrapper that renders the inner value without escaping.
#[derive(Debug, Clone, Copy)]
pub struct PreEscaped<T: AsRef<str>>(pub T);
impl<T: AsRef<str>> Render for PreEscaped<T> {
fn render_to(&self, w: &mut String) {
w.push_str(self.0.as_ref());
}
}
/// A block of markup is a string that does not need to be escaped.
///
/// The `html!` macro expands to an expression of this type.
pub type Markup = PreEscaped<String>;
impl Markup {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<T: AsRef<str> + Into<String>> PreEscaped<T> {
/// Converts the inner value to a string.
pub fn into_string(self) -> String {
self.0.into()
}
}
impl<T: AsRef<str> + Into<String>> From<PreEscaped<T>> for String {
fn from(value: PreEscaped<T>) -> String {
value.into_string()
}
}
impl<T: AsRef<str> + Default> Default for PreEscaped<T> {
fn default() -> Self {
Self(Default::default())
}
}
/// The literal string `<!DOCTYPE html>`.
///
/// # Example
///
/// A minimal web page:
///
/// ```rust
/// use maud::{DOCTYPE, html};
///
/// let markup = html! {
/// (DOCTYPE)
/// html {
/// head {
/// meta charset="utf-8";
/// title { "Test page" }
/// }
/// body {
/// p { "Hello, world!" }
/// }
/// }
/// };
/// ```
pub const DOCTYPE: PreEscaped<&'static str> = PreEscaped("<!DOCTYPE html>");
mod actix_support {
extern crate alloc;
use crate::html::PreEscaped;
use actix_web::{http::header, HttpRequest, HttpResponse, Responder};
use alloc::string::String;
impl Responder for PreEscaped<String> {
type Body = String;
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::Ok()
.content_type(header::ContentType::html())
.message_body(self.0)
.unwrap()
}
}
}
#[doc(hidden)]
pub mod html_private {
extern crate alloc;
use super::{display, Render};
use alloc::string::String;
use core::fmt::Display;
#[doc(hidden)]
#[macro_export]
macro_rules! render_to {
($x:expr, $buffer:expr) => {{
use $crate::html::html_private::*;
match ChooseRenderOrDisplay($x) {
x => (&&x).implements_render_or_display().render_to(x.0, $buffer),
}
}};
}
pub use render_to;
pub struct ChooseRenderOrDisplay<T>(pub T);
pub struct ViaRenderTag;
pub struct ViaDisplayTag;
pub trait ViaRender {
fn implements_render_or_display(&self) -> ViaRenderTag {
ViaRenderTag
}
}
pub trait ViaDisplay {
fn implements_render_or_display(&self) -> ViaDisplayTag {
ViaDisplayTag
}
}
impl<T: Render> ViaRender for &ChooseRenderOrDisplay<T> {}
impl<T: Display> ViaDisplay for ChooseRenderOrDisplay<T> {}
impl ViaRenderTag {
pub fn render_to<T: Render + ?Sized>(self, value: &T, buffer: &mut String) {
value.render_to(buffer);
}
}
impl ViaDisplayTag {
pub fn render_to<T: Display + ?Sized>(self, value: &T, buffer: &mut String) {
display(value).render_to(buffer);
}
}
}

View file

@ -0,0 +1,34 @@
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!! PLEASE KEEP THIS IN SYNC WITH `maud_macros/src/escape.rs` !!!!!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
extern crate alloc;
use alloc::string::String;
pub fn escape_to_string(input: &str, output: &mut String) {
for b in input.bytes() {
match b {
b'&' => output.push_str("&amp;"),
b'<' => output.push_str("&lt;"),
b'>' => output.push_str("&gt;"),
b'"' => output.push_str("&quot;"),
_ => unsafe { output.as_mut_vec().push(b) },
}
}
}
#[cfg(test)]
mod test {
extern crate alloc;
use super::escape_to_string;
use alloc::string::String;
#[test]
fn it_works() {
let mut s = String::new();
escape_to_string("<script>launchMissiles()</script>", &mut s);
assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
}
}

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,46 @@
use crate::core::component::{ComponentTrait, Context, TypedComponent};
use crate::fn_builder;
use crate::html::{html, Markup};
pub struct OptionComponent<C: ComponentTrait>(Option<TypedComponent<C>>);
impl<C: ComponentTrait> Default for OptionComponent<C> {
fn default() -> Self {
OptionComponent(None)
}
}
impl<C: ComponentTrait> OptionComponent<C> {
pub fn new(component: C) -> Self {
OptionComponent::default().with_value(Some(component))
}
// OptionComponent BUILDER.
#[fn_builder]
pub fn set_value(&mut self, component: Option<C>) -> &mut Self {
if let Some(component) = component {
self.0 = Some(TypedComponent::with(component));
} else {
self.0 = None;
}
self
}
// OptionComponent GETTERS.
pub fn get(&self) -> Option<TypedComponent<C>> {
if let Some(value) = &self.0 {
return Some(value.clone());
}
None
}
pub fn render(&self, cx: &mut Context) -> Markup {
if let Some(component) = &self.0 {
component.render(cx)
} else {
html! {}
}
}
}

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"),
}
}
}

119
pagetop/src/lib.rs Normal file
View file

@ -0,0 +1,119 @@
//! <div align="center">
//!
//! <img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
//!
//! <h1>PageTop</h1>
//!
//! <p>An opinionated web framework to build modular <em>Server-Side Rendering</em> web solutions.</p>
//!
//! [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](https://github.com/manuelcillero/pagetop#-license)
//! [![API Docs](https://img.shields.io/docsrs/pagetop?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop)
//! [![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop)
//! [![Downloads](https://img.shields.io/crates/d/pagetop.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
//!
//! <br>
//! </div>
//!
//! The `PageTop` core API provides a comprehensive toolkit for extending its functionalities to
//! specific requirements and application scenarios through actions, components, packages, and
//! themes:
//!
//! * **Actions** serve as a mechanism to customize `PageTop`'s internal behavior by intercepting
//! its execution flow.
//! * **Components** encapsulate HTML, CSS, and JavaScript into functional, configurable, and
//! well-defined units.
//! * **Packages** extend or customize existing functionality by interacting with `PageTop` APIs
//! or third-party package APIs.
//! * **Themes** enable developers to alter the appearance of pages and components without
//! affecting their functionality.
//!
//! # ⚡️ Quick start
//!
//! ```rust
//! use pagetop::prelude::*;
//!
//! struct HelloWorld;
//!
//! impl PackageTrait for HelloWorld {
//! fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
//! scfg.route("/", service::web::get().to(hello_world));
//! }
//! }
//!
//! async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
//! Page::new(request)
//! .with_component(Html::with(html! { h1 { "Hello World!" } }))
//! .render()
//! }
//!
//! #[pagetop::main]
//! async fn main() -> std::io::Result<()> {
//! Application::prepare(&HelloWorld).run()?.await
//! }
//! ```
//! 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](`global::Server`)). You can find this code in the `PageTop`
//! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples).
//!
//! # 🧩 Dependency Management
//!
//! Projects leveraging `PageTop` will use `cargo` to resolve dependencies, similar to any other
//! Rust project.
//!
//! Nevertheless, its crucial that each package explicitly declares its
//! [dependencies](core::package::PackageTrait#method.dependencies), if any, to assist `PageTop` in
//! structuring and initializing the application in a modular fashion.
//!
//! # 🚧 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**.
#![cfg_attr(docsrs, feature(doc_cfg))]
// RE-EXPORTED *************************************************************************************
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::{fn_builder, html, main, test, AutoDefault, ComponentClasses};
pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;
pub use std::any::TypeId;
pub type Weight = i8;
// API *********************************************************************************************
// Useful functions and macros.
pub mod util;
// Load configuration settings.
pub mod config;
// Global settings.
pub mod global;
// Application tracing and event logging.
pub mod trace;
// HTML in code.
pub mod html;
// Localization.
pub mod locale;
// Date and time handling.
pub mod datetime;
// Essential web framework.
pub mod service;
// Key types and functions for creating actions, components, packages, and themes.
pub mod core;
// Web request response variants.
pub mod response;
// Base actions, components, packages, and themes.
pub mod base;
// Prepare and run the application.
pub mod app;
// The PageTop Prelude *****************************************************************************
pub mod prelude;

281
pagetop/src/locale.rs Normal file
View file

@ -0,0 +1,281 @@
//! Localization (L10n).
//!
//! PageTop uses the [Fluent](https://www.projectfluent.org/) specifications for application
//! localization, leveraging the [fluent-templates](https://docs.rs/fluent-templates/) crate to
//! integrate translation resources directly into the application binary.
//!
//! # Fluent Syntax (FTL)
//!
//! The format used to describe the translation resources used by Fluent is called
//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be both readable and
//! expressive, enabling complex natural language constructs like gender, plurals, and conjugations.
//!
//! # Fluent Resources
//!
//! Localization resources are organized in the *src/locale* directory, with subdirectories for
//! each valid [Unicode Language Identifier](https://docs.rs/unic-langid/):
//!
//! ```text
//! src/locale/
//! ├── common.ftl
//! ├── en-US/
//! │ ├── default.ftl
//! │ └── main.ftl
//! ├── es-ES/
//! │ ├── default.ftl
//! │ └── main.ftl
//! ├── es-MX/
//! │ ├── default.ftl
//! │ └── main.ftl
//! └── fr/
//! ├── default.ftl
//! └── main.ftl
//! ```
//!
//! Example of a file *src/locale/en-US/main.ftl*:
//!
//! ```text
//! hello-world = Hello world!
//! hello-user = Hello, {$userName}!
//! shared-photos =
//! {$userName} {$photoCount ->
//! [one] added a new photo
//! *[other] added {$photoCount} new photos
//! } of {$userGender ->
//! [male] him and his family
//! [female] her and her family
//! *[other] the family
//! }.
//! ```
//!
//! Example of the equivalent file *src/locale/es-ES/main.ftl*:
//!
//! ```text
//! hello-world = Hola mundo!
//! hello-user = ¡Hola, {$userName}!
//! shared-photos =
//! {$userName} {$photoCount ->
//! [one] ha añadido una nueva foto
//! *[other] ha añadido {$photoCount} nuevas fotos
//! } de {$userGender ->
//! [male] él y su familia
//! [female] ella y su familia
//! *[other] la familia
//! }.
//! ```
//!
//! # How to apply localization in your code
//!
//! Once you have created your FTL resource directory, use the
//! [`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::*;
//!
//! include_locales!(LOCALES_SAMPLE);
//! ```
//!
//! But if they are in another directory, then you can use:
//!
//! ```
//! use pagetop::prelude::*;
//!
//! include_locales!(LOCALES_SAMPLE from "path/to/locale");
//! ```
use crate::html::{Markup, PreEscaped};
use crate::{global, kv, AutoDefault};
pub use fluent_templates;
pub use unic_langid::{CharacterDirection, LanguageIdentifier};
use fluent_templates::Loader;
use fluent_templates::StaticLoader as Locales;
use unic_langid::langid;
use std::collections::HashMap;
use std::sync::LazyLock;
use std::fmt;
/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
/// 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" ),
]
});
pub static FALLBACK_LANGID: 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 DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&FALLBACK_LANGID));
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 Err(LangError::EmptyLang);
}
// 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! include_locales {
( $LOCALES:ident $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
static $LOCALES = {
locales: "src/locale",
$( core_locales: $core_locales, )?
fallback_language: "en-US",
// Removes unicode isolating marks around arguments.
customise: |bundle| bundle.set_use_isolating(false),
};
}
};
( $LOCALES:ident from $dir_locales:literal $(, $core_locales:literal)? ) => {
$crate::locale::fluent_templates::static_loader! {
static $LOCALES = {
locales: $dir_locales,
$( core_locales: $core_locales, )?
fallback_language: "en-US",
// Removes unicode isolating marks around arguments.
customise: |bundle| bundle.set_use_isolating(false),
};
}
};
}
include_locales!(LOCALES_PAGETOP);
#[derive(AutoDefault)]
enum L10nOp {
#[default]
None,
Text(String),
Translate(String),
}
#[derive(AutoDefault)]
pub struct L10n {
op: L10nOp,
#[default(&LOCALES_PAGETOP)]
locales: &'static Locales,
args: HashMap<String, String>,
}
impl L10n {
pub fn n(text: impl Into<String>) -> Self {
L10n {
op: L10nOp::Text(text.into()),
..Default::default()
}
}
pub fn l(key: impl Into<String>) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
..Default::default()
}
}
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
locales,
..Default::default()
}
}
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
self.args.insert(arg.into(), value.into());
self
}
pub fn with_args(mut self, args: HashMap<String, String>) -> Self {
for (k, v) in args {
self.args.insert(k, 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) => {
if self.args.is_empty() {
self.locales.try_lookup(langid, key)
} else {
self.locales.try_lookup_with_args(
langid,
key,
&self.args.iter().fold(HashMap::new(), |mut args, (k, v)| {
args.insert(k.to_string(), v.to_owned().into());
args
}),
)
}
}
}
}
/// 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 {
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 @@
content = Content

View file

@ -0,0 +1,23 @@
welcome_package_name = Default homepage
welcome_package_description = Displays a landing page when none is configured.
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 @@
content = Contenido

View file

@ -0,0 +1,23 @@
welcome_package_name = Página de inicio predeterminada
welcome_package_description = Muestra una página de inicio predeterminada cuando no hay ninguna configurada.
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

52
pagetop/src/prelude.rs Normal file
View file

@ -0,0 +1,52 @@
//! The `PageTop` Prelude.
// RE-EXPORTED.
pub use crate::{concat_string, fn_builder, html, main, paste, test};
pub use crate::{AutoDefault, ComponentClasses, StaticResources, TypeId, Weight};
// MACROS.
// crate::util
pub use crate::kv;
// crate::config
pub use crate::include_config;
// crate::locale
pub use crate::include_locales;
// crate::service
pub use crate::{include_files, include_files_service};
// crate::core::action
pub use crate::actions;
// API.
pub use crate::util;
pub use crate::global;
pub use crate::trace;
pub use crate::html::*;
pub use crate::locale::*;
pub use crate::datetime::*;
pub use crate::service;
pub use crate::service::{HttpMessage, HttpRequest};
pub use crate::core::{AnyBase, AnyTo};
pub use crate::core::action::*;
pub use crate::core::component::*;
pub use crate::core::package::*;
pub use crate::core::theme::*;
pub use crate::response::{json::*, page::*, redirect::*, ResponseError};
pub use crate::base::action;
pub use crate::base::component::*;
pub use crate::base::theme;
pub use crate::app::Application;

9
pagetop/src/response.rs Normal file
View file

@ -0,0 +1,9 @@
//! Web request response variants.
pub use actix_web::ResponseError;
pub mod page;
pub mod json;
pub mod redirect;

View file

@ -0,0 +1 @@
pub use actix_web::web::Json;

View file

@ -0,0 +1,192 @@
mod error;
pub use error::ErrorPage;
pub use actix_web::Result as ResultPage;
use crate::base::action;
use crate::core::component::{AssetsOp, Context};
use crate::core::component::{ChildComponent, ChildOp, ComponentTrait};
use crate::fn_builder;
use crate::html::{html, Markup, DOCTYPE};
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
use crate::locale::L10n;
use crate::service::HttpRequest;
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 : Context,
body_id : OptionId,
body_classes: OptionClasses,
body_skip_to: OptionId,
}
impl Page {
#[rustfmt::skip]
pub fn new(request: HttpRequest) -> Self {
Page {
title : OptionTranslated::default(),
description : OptionTranslated::default(),
metadata : Vec::default(),
properties : Vec::default(),
context : Context::new(request),
body_id : OptionId::default(),
body_classes: OptionClasses::default(),
body_skip_to: OptionId::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_skip_to(&mut self, id: impl Into<String>) -> &mut Self {
self.body_skip_to.set_value(id);
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_in_region(&mut self, region: &'static str, op: ChildOp) -> &mut Self {
self.context.set_in_region(region, op);
self
}
pub fn with_component(mut self, component: impl ComponentTrait) -> Self {
self.context
.set_in_region("content", ChildOp::Add(ChildComponent::with(component)));
self
}
pub fn with_component_in(
mut self,
region: &'static str,
component: impl ComponentTrait,
) -> Self {
self.context
.set_in_region(region, ChildOp::Add(ChildComponent::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 Context {
&mut self.context
}
pub fn body_id(&self) -> &OptionId {
&self.body_id
}
pub fn body_classes(&self) -> &OptionClasses {
&self.body_classes
}
pub fn body_skip_to(&self) -> &OptionId {
&self.body_skip_to
}
// Page RENDER.
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
// Theme-specific operations before rendering the page body.
self.context.theme().before_render_body(self);
// Execute package actions before rendering the page body.
action::page::BeforeRenderBody::dispatch(self);
// Render the page body.
let body = self.context.theme().render_body(self);
// Theme-specific operations after rendering the page body.
self.context.theme().after_render_body(self);
// Execute package actions after rendering the page body.
action::page::AfterRenderBody::dispatch(self);
// Render the page head.
let head = self.context.theme().render_head(self);
// Render the full page with language and direction attributes.
let lang = &self.context.langid().language;
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)
(body)
}
})
}
}

View file

@ -0,0 +1,86 @@
use crate::base::component::{Error403, Error404};
use crate::locale::L10n;
use crate::response::ResponseError;
use crate::service::http::{header::ContentType, StatusCode};
use crate::service::{HttpRequest, HttpResponse};
use super::Page;
use std::fmt;
#[derive(Debug)]
pub enum ErrorPage {
NotModified(HttpRequest),
BadRequest(HttpRequest),
AccessDenied(HttpRequest),
NotFound(HttpRequest),
PreconditionFailed(HttpRequest),
InternalError(HttpRequest),
Timeout(HttpRequest),
}
impl fmt::Display for ErrorPage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Error 304.
ErrorPage::NotModified(_) => write!(f, "Not Modified"),
// Error 400.
ErrorPage::BadRequest(_) => write!(f, "Bad Client Data"),
// Error 403.
ErrorPage::AccessDenied(request) => {
let error_page = Page::new(request.clone());
if let Ok(page) = error_page
.with_title(L10n::n("Error FORBIDDEN"))
.with_layout("error")
.with_component(Error403)
.render()
{
write!(f, "{}", page.into_string())
} else {
write!(f, "Access Denied")
}
}
// Error 404.
ErrorPage::NotFound(request) => {
let error_page = Page::new(request.clone());
if let Ok(page) = error_page
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
.with_layout("error")
.with_component(Error404)
.render()
{
write!(f, "{}", page.into_string())
} else {
write!(f, "Not Found")
}
}
// Error 412.
ErrorPage::PreconditionFailed(_) => write!(f, "Precondition Failed"),
// Error 500.
ErrorPage::InternalError(_) => write!(f, "Internal Error"),
// Error 504.
ErrorPage::Timeout(_) => write!(f, "Timeout"),
}
}
}
impl ResponseError for ErrorPage {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
#[rustfmt::skip]
fn status_code(&self) -> StatusCode {
match self {
ErrorPage::NotModified(_) => StatusCode::NOT_MODIFIED,
ErrorPage::BadRequest(_) => StatusCode::BAD_REQUEST,
ErrorPage::AccessDenied(_) => StatusCode::FORBIDDEN,
ErrorPage::NotFound(_) => StatusCode::NOT_FOUND,
ErrorPage::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
ErrorPage::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
}
}
}

View file

@ -0,0 +1,76 @@
//! Perform redirections in HTTP.
//!
//! **URL redirection**, also known as *URL forwarding*, is a technique to give more than one URL
//! address to a web resource. HTTP has a response called ***HTTP redirect*** for this operation
//! (see [Redirections in HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections)).
//!
//! There are several types of redirects, sorted into three categories:
//!
//! * **Permanent redirections**. These redirections are meant to last forever. They imply that
//! the original URL should no longer be used, and replaced with the new one. Search engine
//! robots, RSS readers, and other crawlers will update the original URL for the resource.
//!
//! * **Temporary redirections**. Sometimes the requested resource can't be accessed from its
//! canonical location, but it can be accessed from another place. In this case, a temporary
//! redirect can be used. Search engine robots and other crawlers don't memorize the new,
//! temporary URL. Temporary redirections are also used when creating, updating, or deleting
//! resources, to show temporary progress pages.
//!
//! * **Special redirections**.
use crate::service::HttpResponse;
pub struct Redirect;
impl Redirect {
/// Permanent redirection. Status Code **301**. GET methods unchanged. Others may or may not be
/// changed to GET. Typical for reorganization of a website.
pub fn moved(redirect_to_url: &str) -> HttpResponse {
HttpResponse::MovedPermanently()
.append_header(("Location", redirect_to_url))
.finish()
}
/// Permanent redirection. Status Code **308**. Method and body not changed. Typical for
/// reorganization of a website, with non-GET links/operations.
pub fn permanent(redirect_to_url: &str) -> HttpResponse {
HttpResponse::PermanentRedirect()
.append_header(("Location", redirect_to_url))
.finish()
}
/// Temporary redirection. Status Code **302**. GET methods unchanged. Others may or may not be
/// changed to GET. Used when the web page is temporarily unavailable for unforeseen reasons.
pub fn found(redirect_to_url: &str) -> HttpResponse {
HttpResponse::Found()
.append_header(("Location", redirect_to_url))
.finish()
}
/// Temporary redirection. Status Code **303**. GET methods unchanged. Others changed to GET
/// (body lost). Used to redirect after a PUT or a POST, so that refreshing the result page
/// doesn't re-trigger the operation.
pub fn see_other(redirect_to_url: &str) -> HttpResponse {
HttpResponse::SeeOther()
.append_header(("Location", redirect_to_url))
.finish()
}
/// Temporary redirection. Status Code **307**. Method and body not changed. The web page is
/// temporarily unavailable for unforeseen reasons. Better than [`found()`](Self::found) when
/// non-GET operations are available on the site.
pub fn temporary(redirect_to_url: &str) -> HttpResponse {
HttpResponse::TemporaryRedirect()
.append_header(("Location", redirect_to_url))
.finish()
}
/// Special redirection. Status Code **304**. Redirects a page to the locally cached copy (that
/// was stale). Sent for revalidated conditional requests. Indicates that the cached response is
/// still fresh and can be used.
pub fn not_modified(redirect_to_url: &str) -> HttpResponse {
HttpResponse::NotModified()
.append_header(("Location", redirect_to_url))
.finish()
}
}

64
pagetop/src/service.rs Normal file
View file

@ -0,0 +1,64 @@
//! Essential web framework ([actix-web](https://docs.rs/actix-web)).
pub use actix_session::Session;
pub use actix_web::body::BoxBody;
pub use actix_web::dev::Server;
pub use actix_web::dev::ServiceFactory as Factory;
pub use actix_web::dev::ServiceRequest as Request;
pub use actix_web::dev::ServiceResponse as Response;
pub use actix_web::{cookie, get, http, rt, test, web};
pub use actix_web::{App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder};
pub use actix_web_files::Files as ActixFiles;
pub use actix_web_static_files::ResourceFiles;
#[macro_export]
macro_rules! include_files {
( $bundle:ident ) => {
$crate::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
}
};
( $STATIC:ident => $bundle:ident ) => {
$crate::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
pub static $STATIC: std::sync::LazyLock<StaticResources> = std::sync::LazyLock::new(
[<static_files_ $bundle>]::$bundle
);
}
};
}
#[macro_export]
macro_rules! include_files_service {
( $scfg:ident, $bundle:ident => $path:expr $(, [$root:expr, $relative:expr])? ) => {{
$crate::paste! {
let span = $crate::trace::debug_span!("Configuring static files ", path = $path);
let _ = span.in_scope(|| {
#[allow(unused_mut)]
let mut serve_embedded:bool = true;
$(
if !$root.is_empty() && !$relative.is_empty() {
if let Ok(absolute) = $crate::util::absolute_dir($root, $relative) {
$scfg.service($crate::service::ActixFiles::new(
$path,
absolute,
).show_files_listing());
serve_embedded = false
}
}
)?
if serve_embedded {
$scfg.service($crate::service::ResourceFiles::new(
$path,
[<static_files_ $bundle>]::$bundle(),
));
}
});
}
}};
}

84
pagetop/src/trace.rs Normal file
View file

@ -0,0 +1,84 @@
//! Application tracing and event logging.
//!
//! `PageTop` collects application diagnostic information in a structured and event-based manner.
//!
//! In asynchronous systems, interpreting traditional log messages often becomes complicated.
//! Individual tasks are multiplexed to the same thread, and associated events and log messages get
//! intermingled, making it difficult to follow the logical sequence.
//!
//! `PageTop` uses [`tracing`](https://docs.rs/tracing) to allow **applications** and **modules** to
//! log structured events with added information about *temporality* and *causality*. Unlike a log
//! message, a span has a start and end time, can enter and exit the execution flow, and can exist
//! within a nested tree of similar spans. Additionally, these spans are *structured*, with the
//! ability to record data types and text messages.
use crate::global;
pub use tracing::{debug, error, info, trace, warn};
pub use tracing::{debug_span, error_span, info_span, trace_span, warn_span};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
use std::sync::LazyLock;
/// Application tracing and event logging.
///
/// To increase performance, a dedicated thread uses a non-blocking writer system that acts
/// periodically instead of sending each trace or event instantly. If the program terminates
/// abruptly (e.g., due to a panic! or a `std::process::exit`), some traces or events might not be
/// sent.
///
/// Since traces or events logged shortly before an application crash are often important for
/// diagnosing the cause of the failure, `Lazy<WorkerGuard>` ensures that all stored logs are sent
/// before terminating execution.
#[rustfmt::skip]
pub(crate) static TRACING: LazyLock<WorkerGuard> = LazyLock::new(|| {
let env_filter = EnvFilter::try_new(&global::SETTINGS.log.tracing)
.unwrap_or_else(|_| EnvFilter::new("Info"));
let rolling = global::SETTINGS.log.rolling.to_lowercase();
let (non_blocking, guard) = match rolling.as_str() {
"stdout" => tracing_appender::non_blocking(std::io::stdout()),
_ => tracing_appender::non_blocking({
let path = &global::SETTINGS.log.path;
let prefix = &global::SETTINGS.log.prefix;
match rolling.as_str() {
"daily" => tracing_appender::rolling::daily(path, prefix),
"hourly" => tracing_appender::rolling::hourly(path, prefix),
"minutely" => tracing_appender::rolling::minutely(path, prefix),
"endless" => tracing_appender::rolling::never(path, prefix),
_ => {
println!(
"Rolling value \"{}\" not valid. Using \"daily\". Check the settings file.",
global::SETTINGS.log.rolling,
);
tracing_appender::rolling::daily(path, prefix)
}
}
}),
};
let subscriber = tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(non_blocking)
.with_ansi(rolling.as_str() == "stdout");
match global::SETTINGS.log.format.to_lowercase().as_str() {
"json" => subscriber.json().init(),
"full" => subscriber.init(),
"compact" => subscriber.compact().init(),
"pretty" => subscriber.pretty().init(),
_ => {
println!(
"Tracing format \"{}\" not valid. Using \"Full\". Check the settings file.",
global::SETTINGS.log.format,
);
subscriber.init();
}
}
guard
});

161
pagetop/src/util.rs Normal file
View file

@ -0,0 +1,161 @@
//! Useful functions and macros.
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
}};
}

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