✨ [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:
parent
87aac362d3
commit
514a8d89bc
102 changed files with 13606 additions and 18 deletions
2307
Cargo.lock
generated
2307
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
115
README.md
|
|
@ -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>
|
||||
|
||||
[](#-license)
|
||||
[](https://docs.rs/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](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*.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
[](#-license)
|
||||
[](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
48
pagetop/Cargo.toml
Normal 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
7
pagetop/build.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("../static", None)
|
||||
.with_name("assets")
|
||||
.build()
|
||||
}
|
||||
6
pagetop/config/common.toml
Normal file
6
pagetop/config/common.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[app]
|
||||
name = "Samples"
|
||||
#language = "es-ES"
|
||||
|
||||
[log]
|
||||
tracing = "Debug"
|
||||
40
pagetop/config/predefined-settings.toml
Normal file
40
pagetop/config/predefined-settings.toml
Normal 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
|
||||
6
pagetop/examples/app-basic.rs
Normal file
6
pagetop/examples/app-basic.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::new().run()?.await
|
||||
}
|
||||
25
pagetop/examples/hello-name.rs
Normal file
25
pagetop/examples/hello-name.rs
Normal 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
|
||||
}
|
||||
20
pagetop/examples/hello-world.rs
Normal file
20
pagetop/examples/hello-world.rs
Normal 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
168
pagetop/src/app.rs
Normal 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))
|
||||
}
|
||||
30
pagetop/src/app/figfont.rs
Normal file
30
pagetop/src/app/figfont.rs
Normal 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
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
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
1301
pagetop/src/app/speed.flf
Normal file
File diff suppressed because it is too large
Load diff
719
pagetop/src/app/starwars.flf
Normal file
719
pagetop/src/app/starwars.flf
Normal 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
9
pagetop/src/base.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//! Base actions, components, packages, and themes.
|
||||
|
||||
pub mod action;
|
||||
|
||||
pub mod component;
|
||||
|
||||
pub mod package;
|
||||
|
||||
pub mod theme;
|
||||
9
pagetop/src/base/action.rs
Normal file
9
pagetop/src/base/action.rs
Normal 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;
|
||||
8
pagetop/src/base/action/component.rs
Normal file
8
pagetop/src/base/action/component.rs
Normal 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::*;
|
||||
65
pagetop/src/base/action/component/after_prepare_component.rs
Normal file
65
pagetop/src/base/action/component/after_prepare_component.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
pagetop/src/base/action/component/is_renderable.rs
Normal file
77
pagetop/src/base/action/component/is_renderable.rs
Normal 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
|
||||
}
|
||||
}
|
||||
5
pagetop/src/base/action/page.rs
Normal file
5
pagetop/src/base/action/page.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod before_render_body;
|
||||
pub use before_render_body::*;
|
||||
|
||||
mod after_render_body;
|
||||
pub use after_render_body::*;
|
||||
34
pagetop/src/base/action/page/after_render_body.rs
Normal file
34
pagetop/src/base/action/page/after_render_body.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
pagetop/src/base/action/page/before_render_body.rs
Normal file
34
pagetop/src/base/action/page/before_render_body.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
8
pagetop/src/base/action/theme.rs
Normal file
8
pagetop/src/base/action/theme.rs
Normal 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::*;
|
||||
43
pagetop/src/base/action/theme/after_prepare_component.rs
Normal file
43
pagetop/src/base/action/theme/after_prepare_component.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
pagetop/src/base/action/theme/before_prepare_component.rs
Normal file
43
pagetop/src/base/action/theme/before_prepare_component.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
pagetop/src/base/action/theme/render_component.rs
Normal file
49
pagetop/src/base/action/theme/render_component.rs
Normal 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
|
||||
}
|
||||
}
|
||||
11
pagetop/src/base/component.rs
Normal file
11
pagetop/src/base/component.rs
Normal 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;
|
||||
17
pagetop/src/base/component/error403.rs
Normal file
17
pagetop/src/base/component/error403.rs
Normal 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") }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
17
pagetop/src/base/component/error404.rs
Normal file
17
pagetop/src/base/component/error404.rs
Normal 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") }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
25
pagetop/src/base/component/fluent.rs
Normal file
25
pagetop/src/base/component/fluent.rs
Normal 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
|
||||
}
|
||||
}
|
||||
25
pagetop/src/base/component/html.rs
Normal file
25
pagetop/src/base/component/html.rs
Normal 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
133
pagetop/src/base/package.rs
Normal 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
11
pagetop/src/base/theme.rs
Normal 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
196
pagetop/src/config.rs
Normal 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
136
pagetop/src/config/data.rs
Normal 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
462
pagetop/src/config/de.rs
Normal 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
222
pagetop/src/config/error.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
85
pagetop/src/config/file.rs
Normal file
85
pagetop/src/config/file.rs
Normal 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 })
|
||||
}
|
||||
}
|
||||
126
pagetop/src/config/file/source.rs
Normal file
126
pagetop/src/config/file/source.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
51
pagetop/src/config/file/toml.rs
Normal file
51
pagetop/src/config/file/toml.rs
Normal 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
167
pagetop/src/config/path.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
pagetop/src/config/path/parser.rs
Normal file
131
pagetop/src/config/path/parser.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
87
pagetop/src/config/source.rs
Normal file
87
pagetop/src/config/source.rs
Normal 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
545
pagetop/src/config/value.rs
Normal 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
87
pagetop/src/core.rs
Normal 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;
|
||||
19
pagetop/src/core/action.rs
Normal file
19
pagetop/src/core/action.rs
Normal 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),)+]
|
||||
}};
|
||||
}
|
||||
30
pagetop/src/core/action/all.rs
Normal file
30
pagetop/src/core/action/all.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
61
pagetop/src/core/action/definition.rs
Normal file
61
pagetop/src/core/action/definition.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
43
pagetop/src/core/action/list.rs
Normal file
43
pagetop/src/core/action/list.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
14
pagetop/src/core/component.rs
Normal file
14
pagetop/src/core/component.rs
Normal 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};
|
||||
213
pagetop/src/core/component/children.rs
Normal file
213
pagetop/src/core/component/children.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
pagetop/src/core/component/classes.rs
Normal file
19
pagetop/src/core/component/classes.rs
Normal 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
|
||||
}
|
||||
}
|
||||
199
pagetop/src/core/component/context.rs
Normal file
199
pagetop/src/core/component/context.rs
Normal 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, ®ion.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
66
pagetop/src/core/component/definition.rs
Normal file
66
pagetop/src/core/component/definition.rs
Normal 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! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
pagetop/src/core/package.rs
Normal file
4
pagetop/src/core/package.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod definition;
|
||||
pub use definition::{PackageRef, PackageTrait};
|
||||
|
||||
pub(crate) mod all;
|
||||
134
pagetop/src/core/package/all.rs
Normal file
134
pagetop/src/core/package/all.rs
Normal 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"]
|
||||
);
|
||||
}
|
||||
39
pagetop/src/core/package/definition.rs
Normal file
39
pagetop/src/core/package/definition.rs
Normal 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) {}
|
||||
}
|
||||
8
pagetop/src/core/theme.rs
Normal file
8
pagetop/src/core/theme.rs
Normal 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;
|
||||
31
pagetop/src/core/theme/all.rs
Normal file
31
pagetop/src/core/theme/all.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
59
pagetop/src/core/theme/definition.rs
Normal file
59
pagetop/src/core/theme/definition.rs
Normal 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) {}
|
||||
}
|
||||
78
pagetop/src/core/theme/regions.rs
Normal file
78
pagetop/src/core/theme/regions.rs
Normal 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
4
pagetop/src/datetime.rs
Normal 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
121
pagetop/src/global.rs
Normal 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
52
pagetop/src/html.rs
Normal 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) },
|
||||
}
|
||||
}
|
||||
}
|
||||
53
pagetop/src/html/assets.rs
Normal file
53
pagetop/src/html/assets.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
pagetop/src/html/assets/favicon.rs
Normal file
93
pagetop/src/html/assets/favicon.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
pagetop/src/html/assets/javascript.rs
Normal file
111
pagetop/src/html/assets/javascript.rs
Normal 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
|
||||
}
|
||||
}
|
||||
95
pagetop/src/html/assets/stylesheet.rs
Normal file
95
pagetop/src/html/assets/stylesheet.rs
Normal 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
350
pagetop/src/html/maud.rs
Normal 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 `&`
|
||||
/// * `<` is escaped as `<`
|
||||
/// * `>` is escaped as `>`
|
||||
/// * `"` is escaped as `"`
|
||||
///
|
||||
/// 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, "<script>launchMissiles()</script>");
|
||||
/// ```
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
pagetop/src/html/maud/escape.rs
Normal file
34
pagetop/src/html/maud/escape.rs
Normal 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("&"),
|
||||
b'<' => output.push_str("<"),
|
||||
b'>' => output.push_str(">"),
|
||||
b'"' => output.push_str("""),
|
||||
_ => 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, "<script>launchMissiles()</script>");
|
||||
}
|
||||
}
|
||||
111
pagetop/src/html/opt_classes.rs
Normal file
111
pagetop/src/html/opt_classes.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
46
pagetop/src/html/opt_component.rs
Normal file
46
pagetop/src/html/opt_component.rs
Normal 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! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
pagetop/src/html/opt_id.rs
Normal file
29
pagetop/src/html/opt_id.rs
Normal 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
|
||||
}
|
||||
}
|
||||
29
pagetop/src/html/opt_name.rs
Normal file
29
pagetop/src/html/opt_name.rs
Normal 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
|
||||
}
|
||||
}
|
||||
29
pagetop/src/html/opt_string.rs
Normal file
29
pagetop/src/html/opt_string.rs
Normal 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
|
||||
}
|
||||
}
|
||||
30
pagetop/src/html/opt_translated.rs
Normal file
30
pagetop/src/html/opt_translated.rs
Normal 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
56
pagetop/src/html/unit.rs
Normal 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
119
pagetop/src/lib.rs
Normal 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>
|
||||
//!
|
||||
//! [](https://github.com/manuelcillero/pagetop#-license)
|
||||
//! [](https://docs.rs/pagetop)
|
||||
//! [](https://crates.io/crates/pagetop)
|
||||
//! [](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, it’s 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
281
pagetop/src/locale.rs
Normal 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}")
|
||||
}
|
||||
}
|
||||
5
pagetop/src/locale/en-US/languages.ftl
Normal file
5
pagetop/src/locale/en-US/languages.ftl
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
english = English
|
||||
english_british = English (British)
|
||||
english_united_states = English (United States)
|
||||
spanish = Spanish
|
||||
spanish_spain = Spanish (Spain)
|
||||
1
pagetop/src/locale/en-US/theme.ftl
Normal file
1
pagetop/src/locale/en-US/theme.ftl
Normal file
|
|
@ -0,0 +1 @@
|
|||
content = Content
|
||||
23
pagetop/src/locale/en-US/welcome.ftl
Normal file
23
pagetop/src/locale/en-US/welcome.ftl
Normal 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
|
||||
5
pagetop/src/locale/es-ES/languages.ftl
Normal file
5
pagetop/src/locale/es-ES/languages.ftl
Normal 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)
|
||||
1
pagetop/src/locale/es-ES/theme.ftl
Normal file
1
pagetop/src/locale/es-ES/theme.ftl
Normal file
|
|
@ -0,0 +1 @@
|
|||
content = Contenido
|
||||
23
pagetop/src/locale/es-ES/welcome.ftl
Normal file
23
pagetop/src/locale/es-ES/welcome.ftl
Normal 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
52
pagetop/src/prelude.rs
Normal 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
9
pagetop/src/response.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//! Web request response variants.
|
||||
|
||||
pub use actix_web::ResponseError;
|
||||
|
||||
pub mod page;
|
||||
|
||||
pub mod json;
|
||||
|
||||
pub mod redirect;
|
||||
1
pagetop/src/response/json.rs
Normal file
1
pagetop/src/response/json.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub use actix_web::web::Json;
|
||||
192
pagetop/src/response/page.rs
Normal file
192
pagetop/src/response/page.rs
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
86
pagetop/src/response/page/error.rs
Normal file
86
pagetop/src/response/page/error.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
76
pagetop/src/response/redirect.rs
Normal file
76
pagetop/src/response/redirect.rs
Normal 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
64
pagetop/src/service.rs
Normal 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
84
pagetop/src/trace.rs
Normal 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
161
pagetop/src/util.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue