🎉 Start refactoring PageTop for Sycamore

This commit is contained in:
Manuel Cillero 2024-11-12 23:42:42 +01:00
parent 9f62955acb
commit 38ca8f1d1c
155 changed files with 2805 additions and 10960 deletions

39
packages/drust/Cargo.toml Normal file
View file

@ -0,0 +1,39 @@
[package]
name = "drust"
version = "0.0.3"
edition = "2021"
description = "A modern web Content Management System to share your world."
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
[dependencies]
pagetop.workspace = true
# Packages.
#pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
#pagetop-user = { version = "0.0", path = "../pagetop-user" }
#pagetop-node = { version = "0.0", path = "../pagetop-node" }
# Themes.
#pagetop-bootsier = { version = "0.0", path = "../pagetop-bootsier" }
#pagetop-bulmix = { version = "0.0", path = "../pagetop-bulmix" }
#[features]
#default = [ "mysql" ]
#mysql = [
# "pagetop-user/mysql",
# "pagetop-node/mysql",
#]
#postgres = [
# "pagetop-user/postgres",
# "pagetop-node/postgres",
#]
#sqlite = [
# "pagetop-user/sqlite",
# "pagetop-node/sqlite",
#]

View file

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

View file

@ -0,0 +1,10 @@
[app]
#theme = "Basic"
#theme = "Chassis"
theme = "Inception"
#theme = "Bootsier"
#theme = "Bulmix"
language = "es-ES"
[log]
tracing = "Info,pagetop=Debug,sqlx::query=Warn"

View file

@ -0,0 +1,7 @@
[database]
db_name = "drust"
db_user = "drust"
db_pass = "demo"
[dev]
pagetop_project_dir = "/home/manuelcillero/Proyectos/pagetop"

View file

@ -0,0 +1,22 @@
use pagetop::prelude::*;
struct Drust;
impl PackageTrait for Drust {
fn dependencies(&self) -> Vec<PackageRef> {
vec![
// Packages.
//&pagetop_admin::Admin,
//&pagetop_user::User,
//&pagetop_node::Node,
// Themes.
//&pagetop_bootsier::Bootsier,
//&pagetop_bulmix::Bulmix,
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&Drust).run()?.await
}

View file

@ -0,0 +1,47 @@
[package]
name = "pagetop"
version = "0.0.56"
edition = "2021"
description = "An opinionated web framework to build modular Server-Side Rendering web solutions."
categories = ["web-programming", "gui", "development-tools", "asynchronous"]
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
readme = "../../README.md"
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
authors = { workspace = true }
[lib]
name = "pagetop"
[dependencies]
concat-string = "1.0.1"
figlet-rs = "0.1.5"
paste = "1.0.15"
terminal_size = "0.4.0"
toml = "0.8.19"
serde.workspace = true
static-files.workspace = true
pagetop-macros.workspace = true
actix-web.workspace = true
actix-web-files.workspace = true
actix-web-static-files.workspace = true
actix-session.workspace = true
fluent-templates.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-appender.workspace = true
tracing-subscriber.workspace = true
tracing-actix-web.workspace = true
substring.workspace = true
unic-langid.workspace = true

155
packages/pagetop/src/app.rs Normal file
View file

@ -0,0 +1,155 @@
//! Prepare and run an application created with **Pagetop**.
mod figfont;
use crate::core::{package, package::PackageRef};
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::LANGID_DEFAULT);
// 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 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_name = global::SETTINGS.app.name.to_string();
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_name = ff.to_string();
}
}
}
println!("\n{app_name}");
// Application description.
if !global::SETTINGS.app.description.is_empty() {
println!("{}\n", global::SETTINGS.app.description);
};
// PageTop version.
println!("Powered by PageTop {}\n", env!("CARGO_PKG_VERSION"));
}
}
/// Starts the web server.
pub fn run(self) -> Result<service::Server, Error> {
// Generate the cookie key.
let secret_key = service::cookie::Key::generate();
// Prepares the web server.
Ok(service::HttpServer::new(move || {
Self::service_app()
.wrap(tracing_actix_web::TracingLogger::default())
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
.session_lifecycle(match global::SETTINGS.server.session_lifetime {
0 => SessionLifecycle::BrowserSession(BrowserSession::default()),
_ => SessionLifecycle::PersistentSession(
PersistentSession::default().session_ttl(
service::cookie::time::Duration::seconds(
global::SETTINGS.server.session_lifetime,
),
),
),
})
.build(),
)
})
.bind(format!(
"{}:{}",
&global::SETTINGS.server.bind_address,
&global::SETTINGS.server.bind_port
))?
.run())
}
/// Method for testing, returns a service application instance.
pub fn test(
self,
) -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
Error = service::Error,
InitError = (),
>,
> {
Self::service_app()
}
// Configures the service application.
fn service_app() -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
Error = service::Error,
InitError = (),
>,
> {
service::App::new().configure(package::all::configure_services)
// .default_service(service::web::route().to(service_not_found))
}
}
/*
async fn service_not_found(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Err(ErrorPage::NotFound(request))
}
*/

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,196 @@
//! Retrieve and apply settings values from configuration files.
//!
//! Carga la configuración de la aplicación en forma de pares `clave = valor` recogidos en archivos
//! [TOML](https://toml.io).
//!
//! La metodología [The Twelve-Factor App](https://12factor.net/es/) define **la configuración de
//! una aplicación como todo lo que puede variar entre despliegues**, diferenciando entre entornos
//! de desarrollo, pre-producción, producción, etc.
//!
//! A veces las aplicaciones guardan configuraciones como constantes en el código, lo que implica
//! una violación de esta metodología. `PageTop` recomienda una **estricta separación entre código y
//! configuración**. La configuración variará en cada tipo de despliegue, y el código no.
//!
//!
//! # Cómo cargar los ajustes de configuración
//!
//! Si tu aplicación requiere archivos de configuración debes crear un directorio *config* al mismo
//! nivel del archivo *Cargo.toml* de tu proyecto (o del ejecutable binario de la aplicación).
//!
//! `PageTop` se encargará de cargar todos los ajustes de configuración de tu aplicación leyendo los
//! siguientes archivos TOML en este orden (todos los archivos son opcionales):
//!
//! 1. **config/common.toml**, útil para los ajustes comunes a cualquier entorno. Estos valores
//! podrán ser sobrescritos al fusionar los archivos de configuración restantes.
//!
//! 2. **config/{file}.toml**, donde *{file}* se define con la variable de entorno
//! `PAGETOP_RUN_MODE`:
//!
//! * Si no está definida se asumirá *default* por defecto y `PageTop` intentará cargar el
//! archivo *config/default.toml* si existe.
//!
//! * De esta manera podrás tener diferentes ajustes de configuración para diferentes entornos
//! de ejecución. Por ejemplo, para *devel.toml*, *staging.toml* o *production.toml*. O
//! también para *server1.toml* o *server2.toml*. Sólo uno será cargado.
//!
//! * Normalmente estos archivos suelen ser idóneos para incluir contraseñas o configuración
//! sensible asociada al entorno correspondiente. Estos archivos no deberían ser publicados en
//! el repositorio Git por razones de seguridad.
//!
//! 3. **config/local.toml**, para añadir o sobrescribir ajustes de los archivos anteriores.
//!
//!
//! # Cómo añadir ajustes de configuración
//!
//! Para proporcionar a tu **módulo** sus propios ajustes de configuración, añade
//! [*serde*](https://docs.rs/serde) en las dependencias de tu archivo *Cargo.toml* habilitando la
//! característica `derive`:
//!
//! ```toml
//! [dependencies]
//! serde = { version = "1.0", features = ["derive"] }
//! ```
//!
//! Y luego inicializa con la macro [`config_defaults!`](crate::config_defaults) tus ajustes, usando
//! tipos seguros y asignando los valores predefinidos para la estructura asociada:
//!
//! ```
//! use pagetop::prelude::*;
//! use serde::Deserialize;
//!
//! #[derive(Debug, Deserialize)]
//! pub struct Settings {
//! pub myapp: MyApp,
//! }
//!
//! #[derive(Debug, Deserialize)]
//! pub struct MyApp {
//! pub name: String,
//! pub description: Option<String>,
//! pub width: u16,
//! pub height: u16,
//! }
//!
//! config_defaults!(SETTINGS: Settings => [
//! // [myapp]
//! "myapp.name" => "Value Name",
//! "myapp.width" => 900,
//! "myapp.height" => 320,
//! ]);
//! ```
//!
//! De hecho, así se declaran los ajustes globales de la configuración (ver [`SETTINGS`]).
//!
//! Puedes usar la [sintaxis TOML](https://toml.io/en/v1.0.0#table) para añadir tu nueva sección
//! `[myapp]` en los archivos de configuración, del mismo modo que se añaden `[log]` o `[server]` en
//! los ajustes globales (ver [`Settings`]).
//!
//! Se recomienda inicializar todos los ajustes con valores predefinidos, o utilizar la notación
//! `Option<T>` si van a ser tratados en el código como opcionales.
//!
//! Si no pueden inicializarse correctamente los ajustes de configuración, entonces la aplicación
//! ejecutará un panic! y detendrá la ejecución.
//!
//! Los ajustes de configuración siempre serán de sólo lectura.
//!
//!
//! # Cómo usar tus nuevos ajustes de configuración
//!
//! ```
//! use pagetop::prelude::*;
//! use crate::config;
//!
//! fn global_settings() {
//! println!("App name: {}", &global::SETTINGS.app.name);
//! println!("App description: {}", &global::SETTINGS.app.description);
//! println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode);
//! }
//!
//! fn package_settings() {
//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
//! println!("{}", &config::SETTINGS.myapp.width);
//! }
//! ```
mod data;
mod de;
mod error;
mod file;
mod path;
mod source;
mod value;
use crate::config::data::ConfigData;
use crate::config::file::File;
use crate::join;
use std::sync::LazyLock;
use std::env;
use std::path::Path;
/// Original configuration values in `key = value` pairs gathered from configuration files.
pub static CONFIG_DATA: LazyLock<ConfigData> = LazyLock::new(|| {
// Identify the configuration directory.
let config_dir = env::var("CARGO_MANIFEST_DIR")
.map(|manifest_dir| {
let manifest_config = Path::new(&manifest_dir).join("config");
if manifest_config.exists() {
manifest_config.to_string_lossy().to_string()
} else {
"config".to_string()
}
})
.unwrap_or_else(|_| "config".to_string());
// Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'.
let run_mode = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
// Initialize settings.
let mut settings = ConfigData::default();
// Merge (optional) configuration files and set the execution mode.
settings
// First, add the common configuration for all environments. Defaults to 'common.toml'.
.merge(File::with_name(&join!(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(&join!(config_dir, "/", run_mode, ".toml")).required(false))
.expect(&format!("Failed to merge {run_mode}.toml configuration"))
// Add reserved local configuration for the environment. Defaults to 'local.default.toml'.
.merge(File::with_name(&join!(config_dir, "/local.", run_mode, ".toml")).required(false))
.expect("Failed to merge reserved local environment configuration")
// Add the general reserved local configuration. Defaults to 'local.toml'.
.merge(File::with_name(&join!(config_dir, "/local.toml")).required(false))
.expect("Failed to merge general reserved local configuration")
// Save the execution mode.
.set("app.run_mode", run_mode)
.expect("Failed to set application run mode");
settings
});
#[macro_export]
/// Define un conjunto de ajustes de configuración usando tipos seguros y valores predefinidos.
///
/// Detiene la aplicación con un panic! si no pueden asignarse los ajustes de configuración.
///
/// Ver [`Cómo añadir ajustes de configuración`](config/index.html#cómo-añadir-ajustes-de-configuración).
macro_rules! config_defaults {
( $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_DATA.clone();
$(
settings.set_default($key, $value).unwrap();
)*
match settings.try_into() {
Ok(s) => s,
Err(e) => panic!("Error parsing settings: {}", e),
}
});
};
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,73 @@
//! Key types and functions for creating actions, components, packages, and themes.
use crate::global::TypeInfo;
use std::any::Any;
// Common definitions for core types.
pub trait AnyBase: Any {
fn type_name(&self) -> &'static str;
fn short_name(&self) -> &'static str;
fn as_any_ref(&self) -> &dyn Any;
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
}
}
pub trait AnyTo: AnyBase {
#[inline]
fn is<T>(&self) -> bool
where
T: AnyBase,
{
self.as_any_ref().is::<T>()
}
#[inline]
fn downcast_ref<T>(&self) -> Option<&T>
where
T: AnyBase,
{
self.as_any_ref().downcast_ref()
}
#[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 behavior of PageTop core.
pub mod action;
// API to add new features with packages.
pub mod package;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,142 @@
use crate::core::action::add_action;
use crate::core::package::PackageRef;
use crate::{service, trace};
use std::sync::{LazyLock, RwLock};
//static_files!(base);
// 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 welcome page package to the enabled list.
// add_to_enabled(&mut enabled_list, &crate::base::package::Welcome);
// Add default theme packages to the enabled list.
// add_to_enabled(&mut enabled_list, &crate::base::theme::Basic);
// add_to_enabled(&mut enabled_list, &crate::base::theme::Chassis);
// add_to_enabled(&mut enabled_list, &crate::base::theme::Inception);
// 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);
}
// Reverse the order to ensure packages are sorted from none to most dependencies.
enabled_list.reverse();
// Save the final list of enabled packages.
ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list);
// 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 to the enabled list.
list.push(package);
// Reverse dependencies to add them in correct order (dependencies first).
let mut dependencies = package.dependencies();
dependencies.reverse();
for d in &dependencies {
add_to_enabled(list, *d);
}
/* 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 ******************************************************************************
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
/*
static_files_service!(
scfg,
base => "/base",
[&global::SETTINGS.dev.pagetop_project_dir, "static/base"]
);
*/
for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.configure_service(scfg);
}
}

View file

@ -0,0 +1,34 @@
use crate::core::action::ActionBox;
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::none()
}
fn dependencies(&self) -> Vec<PackageRef> {
vec![]
}
fn drop_packages(&self) -> Vec<PackageRef> {
vec![]
}
fn actions(&self) -> Vec<ActionBox> {
actions![]
}
fn init(&self) {}
#[allow(unused_variables)]
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
}

View file

@ -0,0 +1,288 @@
//! Global settings, functions and macro helpers.
use crate::{config_defaults, trace};
use serde::Deserialize;
use std::io;
use std::path::PathBuf;
// *************************************************************************************************
// SETTINGS.
// *************************************************************************************************
#[derive(Debug, Deserialize)]
/// Configuration settings for 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 {
/// El nombre de la aplicación.
/// Por defecto: *"My App"*.
pub name: String,
/// Una descripción breve de la aplicación.
/// Por defecto: *"Developed with the amazing PageTop framework."*.
pub description: String,
/// Tema predeterminado.
/// Por defecto: *"Default"*.
pub theme: String,
/// Idioma (localización) predeterminado.
/// Por defecto: *"en-US"*.
pub language: String,
/// Dirección predeterminada para el texto: *"ltr"* (de izquierda a derecha), *"rtl"* (de
/// derecha a izquierda) o *"auto"*.
/// Por defecto: *"ltr"*.
pub direction: String,
/// Rótulo de texto ASCII al arrancar: *"Off"*, *"Slant"*, *"Small"*, *"Speed"* o *"Starwars"*.
/// Por defecto: *"Slant"*.
pub startup_banner: String,
/// Por defecto: según variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no lo está.
pub run_mode: String,
}
#[derive(Debug, Deserialize)]
/// Section `[dev]` of the configuration settings.
///
/// See [`Settings`].
pub struct Dev {
/// Los archivos estáticos requeridos por la aplicación se integran de manera predeterminada en
/// el binario ejecutable. Sin embargo, durante el desarrollo puede resultar útil servir estos
/// archivos desde su propio directorio para evitar recompilar cada vez que se modifican. En
/// este caso bastaría con indicar la ruta completa al directorio raíz del proyecto.
/// Por defecto: *""*.
pub pagetop_project_dir: String,
}
#[derive(Debug, Deserialize)]
/// Section `[log]` of the configuration settings.
///
/// See [`Settings`].
pub struct Log {
/// Filtro, o combinación de filtros separados por coma, para la traza de ejecución: *"Error"*,
/// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*.
/// Por ejemplo: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
/// Por defecto: *"Info"*.
pub tracing: String,
/// Muestra la traza en el terminal (*"Stdout"*) o queda registrada en archivos con rotación
/// *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*.
/// Por defecto: *"Stdout"*.
pub rolling: String,
/// Directorio para los archivos de traza (si `rolling` != *"Stdout"*).
/// Por defecto: *"log"*.
pub path: String,
/// Prefijo para los archivos de traza (si `rolling` != *"Stdout"*).
/// Por defecto: *"tracing.log"*.
pub prefix: String,
/// Presentación de las trazas. Puede ser *"Full"*, *"Compact"*, *"Pretty"* o *"Json"*.
/// Por defecto: *"Full"*.
pub format: String,
}
#[derive(Debug, Deserialize)]
/// Section `[server]` of the configuration settings.
///
/// See [`Settings`].
pub struct Server {
/// Dirección del servidor web.
/// Por defecto: *"localhost"*.
pub bind_address: String,
/// Puerto del servidor web.
/// Por defecto: *8088*.
pub bind_port: u16,
/// Duración en segundos para la sesión (0 indica "hasta que se cierre el navegador").
/// Por defecto: *604800* (7 días).
pub session_lifetime: i64,
}
config_defaults!(SETTINGS: Settings => [
// [app]
"app.name" => "My App",
"app.description" => "Developed with the amazing PageTop framework.",
"app.theme" => "Default",
"app.language" => "en-US",
"app.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,
]);
// *************************************************************************************************
// FUNCTIONS HELPERS.
// *************************************************************************************************
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)
}
// *************************************************************************************************
// MACRO HELPERS.
// *************************************************************************************************
#[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
}};
}

130
packages/pagetop/src/lib.rs Normal file
View file

@ -0,0 +1,130 @@
//! <div align="center">
//!
//! <img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
//!
//! <h1>PageTop</h1>
//!
//! <p>An opinionated web framework to build modular <em>Server-Side Rendering</em> web solutions.</p>
//!
//! [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](https://github.com/manuelcillero/pagetop#-license)
//! [![API Docs](https://img.shields.io/docsrs/pagetop?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop)
//! [![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop)
//! [![Downloads](https://img.shields.io/crates/d/pagetop.svg?style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
//!
//! <br>
//! </div>
//!
//! The `PageTop` core API provides a comprehensive toolkit for extending its functionalities to
//! specific requirements and application scenarios through actions, components, packages, and
//! themes:
//!
//! * **Actions** serve as a mechanism to customize `PageTop`'s internal behavior by intercepting
//! its execution flow.
//! * **Components** encapsulate HTML, CSS, and JavaScript into functional, configurable, and
//! well-defined units.
//! * **Packages** extend or customize existing functionality by interacting with `PageTop` APIs
//! or third-party package APIs.
//! * **Themes** enable developers to alter the appearance of pages and components without
//! affecting their functionality.
//!
//! # ⚡️ Quick start
//!
//! ```rust
//! use pagetop::prelude::*;
//!
//! struct HelloWorld;
//!
//! impl PackageTrait for HelloWorld {
//! fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
//! scfg.route("/", service::web::get().to(hello_world));
//! }
//! }
//!
//! async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
//! Page::new(request)
//! .with_component(Html::with(html! { h1 { "Hello World!" } }))
//! .render()
//! }
//!
//! #[pagetop::main]
//! async fn main() -> std::io::Result<()> {
//! Application::prepare(&HelloWorld).run()?.await
//! }
//! ```
//! This program implements a package named `HelloWorld` with one service that returns a web page
//! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using
//! the [default configuration settings](`config::Server`)). You can find this code in the `PageTop`
//! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples).
//!
//! # 🧩 Dependency Management
//!
//! Projects leveraging `PageTop` will use `cargo` to resolve dependencies, similar to any other
//! Rust project.
//!
//! Nevertheless, its crucial that each package explicitly declares its
//! [dependencies](core::package::PackageTrait#method.dependencies), if any, to assist `PageTop` in
//! structuring and initializing the application in a modular fashion.
//!
//! # 🚧 Warning
//!
//! **`PageTop`** framework is currently in active development. The API is unstable and subject to
//! frequent changes. Production use is not recommended until version **0.1.0**.
#![cfg_attr(docsrs, feature(doc_cfg))]
// *************************************************************************************************
// RE-EXPORTED MACROS AND DERIVES.
// *************************************************************************************************
pub use concat_string::concat_string as join;
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
pub use paste::paste;
pub use pagetop_macros::{main, test, AutoDefault};
// *************************************************************************************************
// GLOBAL.
// *************************************************************************************************
pub use static_files::Resource as StaticResource;
pub type HashMapResources = std::collections::HashMap<&'static str, StaticResource>;
pub use std::any::TypeId;
pub type Weight = i8;
// Global settings, functions and macro helpers.
pub mod global;
static_locales!(LOCALES_PAGETOP);
// *************************************************************************************************
// PUBLIC API.
// *************************************************************************************************
// Retrieve and apply settings values from configuration files.
pub mod config;
// Application tracing and event logging.
pub mod trace;
// Localization.
pub mod locale;
// 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;
// Prepare and run the application.
pub mod app;
// *************************************************************************************************
// The PageTop Prelude.
// *************************************************************************************************
pub mod prelude;

View file

@ -0,0 +1,285 @@
//! Localization (L10n).
//!
//! PageTop uses the [Fluent](https://www.projectfluent.org/) set of specifications for application
//! localization.
//!
//! # 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 easy to read while
//! simultaneously allowing the representation of complex natural language concepts to address
//! gender, plurals, conjugations, and others.
//!
//! # Fluent Resources
//!
//! PageTop utilizes [fluent-templates](https://docs.rs/fluent-templates/) to integrate localization
//! resources into the application binary. The following example groups files and subfolders from
//! *src/locale* that have a valid [Unicode Language Identifier](https://docs.rs/unic-langid/) and
//! assigns them to their corresponding identifier:
//!
//! ```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
//! [`static_locales!`](crate::static_locales) macro to integrate them into your module or
//! application. If your resources are located in the `"src/locale"` directory, simply declare:
//!
//! ```
//! use pagetop::prelude::*;
//!
//! static_locales!(LOCALES_SAMPLE);
//! ```
//!
//! But if they are in another directory, then you can use:
//!
//! ```
//! use pagetop::prelude::*;
//!
//! static_locales!(LOCALES_SAMPLE in "path/to/locale");
//! ```
use crate::{global, kv, AutoDefault, LOCALES_PAGETOP};
pub use fluent_templates;
pub use unic_langid::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;
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
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 LANGID_FALLBACK: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
/// Sets the application's default
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
/// through `SETTINGS.app.language`.
pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = LazyLock::new(|| {
langid_for(global::SETTINGS.app.language.as_str()).unwrap_or(&LANGID_FALLBACK)
});
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> {
let language = language.into();
match LANGUAGES.get(language.as_str()) {
Some((langid, _)) => Ok(langid),
None => {
if language.is_empty() {
Ok(&LANGID_FALLBACK)
} else {
Err(format!(
"No langid for Unicode Language Identifier \"{language}\".",
))
}
}
}
}
#[macro_export]
/// Defines a set of localization elements and local translation texts.
macro_rules! static_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 in $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),
};
}
};
}
#[derive(AutoDefault)]
enum L10nOp {
#[default]
None,
Text(String),
Translate(String),
}
#[derive(AutoDefault)]
pub struct L10n {
op: L10nOp,
locales: Option<&'static Locales>,
args: HashMap<String, String>,
}
impl L10n {
pub fn none() -> Self {
L10n::default()
}
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()),
locales: Some(&LOCALES_PAGETOP),
..Default::default()
}
}
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
L10n {
op: L10nOp::Translate(key.into()),
locales: Some(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 using(&self, langid: &LanguageIdentifier) -> Option<String> {
match &self.op {
L10nOp::None => None,
L10nOp::Text(text) => Some(text.to_owned()),
L10nOp::Translate(key) => match self.locales {
Some(locales) => {
if self.args.is_empty() {
locales.try_lookup(langid, key)
} else {
locales.try_lookup_with_args(
langid,
key,
&self
.args
.iter()
.fold(HashMap::new(), |mut args, (key, value)| {
args.insert(key.to_string(), value.to_owned().into());
args
}),
)
}
}
None => None,
},
}
}
}
impl fmt::Display for L10n {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.op {
L10nOp::None => write!(f, ""),
L10nOp::Text(text) => write!(f, "{text}"),
L10nOp::Translate(key) => {
if let Some(locales) = self.locales {
write!(
f,
"{}",
if self.args.is_empty() {
locales.lookup(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &LANGID_DEFAULT,
},
key,
)
} else {
locales.lookup_with_args(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &LANGID_DEFAULT,
},
key,
&self
.args
.iter()
.fold(HashMap::new(), |mut args, (key, value)| {
args.insert(key.to_string(), value.to_owned().into());
args
}),
)
}
)
} else {
write!(f, "Unknown localization {key}")
}
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,38 @@
//! The `PageTop` Prelude.
// RE-EXPORTED.
pub use crate::{join, main, paste, test, AutoDefault};
// GLOBAL.
pub use crate::{global, HashMapResources, TypeId, Weight};
// MACROS.
// crate::global
pub use crate::kv;
// crate::config
pub use crate::config_defaults;
// crate::locale
pub use crate::static_locales;
// crate::service
pub use crate::{static_files, static_files_service};
// crate::core::action
pub use crate::actions;
// API.
pub use crate::trace;
pub use crate::locale::*;
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::package::*;
pub use crate::response::{json::*, redirect::*, ResponseError};
pub use crate::app::Application;

View file

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

View file

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

View file

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

View file

@ -0,0 +1,63 @@
//! 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! static_files {
( $bundle:ident ) => {
$crate::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
}
};
( $bundle:ident => $STATIC:ident ) => {
$crate::paste! {
mod [<static_files_ $bundle>] {
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
}
static $STATIC: std::sync::LazyLock<HashMapResources> = std::sync::LazyLock::new(
[<static_files_ $bundle>]::$bundle
);
}
};
}
#[macro_export]
macro_rules! static_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(|| {
let mut serve_embedded:bool = true;
$(
if !$root.is_empty() && !$relative.is_empty() {
if let Ok(absolute) = $crate::global::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(),
));
}
});
}
}};
}

View file

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