♻️ Refactoring config parsing & theme integration
This commit is contained in:
parent
c26432d58c
commit
cafa1d53a2
28 changed files with 716 additions and 589 deletions
|
|
@ -42,6 +42,7 @@ actix-web-files = { package = "actix-files", version = "0.6" }
|
||||||
actix-web-static-files = "4.0"
|
actix-web-static-files = "4.0"
|
||||||
actix-session = { version = "0.10", features = ["cookie-session"] }
|
actix-session = { version = "0.10", features = ["cookie-session"] }
|
||||||
fluent-templates = "0.11"
|
fluent-templates = "0.11"
|
||||||
|
fluent-bundle = "0.15"
|
||||||
nom = "7.1"
|
nom = "7.1"
|
||||||
substring = "1.4"
|
substring = "1.4"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
|
||||||
37
config/predefined-settings.toml
Normal file
37
config/predefined-settings.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
[app]
|
||||||
|
name = "My App"
|
||||||
|
description = "Developed with the amazing PageTop framework."
|
||||||
|
# Default theme.
|
||||||
|
theme = ""
|
||||||
|
# Default language (localization).
|
||||||
|
language = "en-US"
|
||||||
|
# Default text direction: "ltr", "rtl", or "auto".
|
||||||
|
text_direction = "ltr"
|
||||||
|
# Banner displayed at startup: "Off", "Slant", "Small", "Speed", or "Starwars".
|
||||||
|
startup_banner = "Slant"
|
||||||
|
|
||||||
|
[dev]
|
||||||
|
# During development, serve static files from the project's root directory to
|
||||||
|
# avoid recompilation.
|
||||||
|
pagetop_project_dir = ""
|
||||||
|
|
||||||
|
[log]
|
||||||
|
# Execution trace level: "Error", "Warn", "Info", "Debug", or "Trace".
|
||||||
|
# Example: tracing = "Error,actix_server::builder=Info,tracing_actix_web=Debug"
|
||||||
|
tracing = "Info"
|
||||||
|
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
|
||||||
|
rolling = "Stdout"
|
||||||
|
# Directory for trace files (if rolling != "Stdout").
|
||||||
|
path = "log"
|
||||||
|
# Prefix for trace files (if rolling != "Stdout").
|
||||||
|
prefix = "tracing.log"
|
||||||
|
# Traces format: "Full", "Compact", "Pretty", or "Json".
|
||||||
|
format = "Full"
|
||||||
|
|
||||||
|
[server]
|
||||||
|
# Web server config.
|
||||||
|
bind_address = "localhost"
|
||||||
|
bind_port = 8088
|
||||||
|
# If cookies are used, specify the session cookie duration (in seconds). A value
|
||||||
|
# of 0 means "until the browser is closed". Default: one week.
|
||||||
|
session_lifetime = 604800
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
[app]
|
|
||||||
name = "My App"
|
|
||||||
description = "Developed with the amazing PageTop framework."
|
|
||||||
# Default theme.
|
|
||||||
theme = "Default"
|
|
||||||
# Default language (localization).
|
|
||||||
language = "en-US"
|
|
||||||
# Default text direction: "ltr", "rtl", or "auto".
|
|
||||||
direction = "ltr"
|
|
||||||
# Startup banner: "Off", "Slant", "Small", "Speed", or "Starwars".
|
|
||||||
startup_banner = "Slant"
|
|
||||||
|
|
||||||
[dev]
|
|
||||||
# Static files required by the app are integrated by default into the executable
|
|
||||||
# binary. However, during development, it can be useful to serve these files
|
|
||||||
# from their own directory to avoid recompiling every time they are modified. In
|
|
||||||
# this case, just indicate the full path to the project's root directory.
|
|
||||||
pagetop_project_dir = ""
|
|
||||||
|
|
||||||
[log]
|
|
||||||
# Execution trace: "Error", "Warn", "Info", "Debug", or "Trace".
|
|
||||||
# For example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
|
||||||
tracing = "Info"
|
|
||||||
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
|
|
||||||
rolling = "Stdout"
|
|
||||||
# Directory for trace files (if rolling != "Stdout").
|
|
||||||
path = "log"
|
|
||||||
# Prefix for trace files (if rolling != "Stdout").
|
|
||||||
prefix = "tracing.log"
|
|
||||||
# Traces format: "Full", "Compact", "Pretty", or "Json".
|
|
||||||
format = "Full"
|
|
||||||
|
|
||||||
[server]
|
|
||||||
# Web server config.
|
|
||||||
bind_address = "localhost"
|
|
||||||
bind_port = 8088
|
|
||||||
# Session cookie duration (in seconds), i.e., the time from when the session is
|
|
||||||
# created until the cookie expires. A value of 0 indicates "until the browser is
|
|
||||||
# closed". By default, it is one week.
|
|
||||||
session_lifetime = 604800
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
[app]
|
[app]
|
||||||
#theme = "Basic"
|
|
||||||
#theme = "Chassis"
|
|
||||||
theme = "Inception"
|
theme = "Inception"
|
||||||
#theme = "Bootsier"
|
#theme = "Bootsier"
|
||||||
#theme = "Bulmix"
|
|
||||||
language = "es-ES"
|
language = "es-ES"
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
|
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
//! 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),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
//! Key types and functions for creating actions, components, packages, and themes.
|
//! Key types and functions for creating actions, packages, and themes.
|
||||||
|
|
||||||
use crate::global::TypeInfo;
|
use crate::util::TypeInfo;
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
// Common definitions for core types.
|
/// A base trait that extends `Any` to provide metadata and dynamic type casting
|
||||||
|
/// capabilities.
|
||||||
pub trait AnyBase: Any {
|
pub trait AnyBase: Any {
|
||||||
|
/// Returns the full name of the type.
|
||||||
fn type_name(&self) -> &'static str;
|
fn type_name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Returns a short name for the type.
|
||||||
fn short_name(&self) -> &'static str;
|
fn short_name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Returns a reference to `dyn Any` for dynamic type casting.
|
||||||
fn as_any_ref(&self) -> &dyn Any;
|
fn as_any_ref(&self) -> &dyn Any;
|
||||||
|
|
||||||
|
/// Returns a mutable reference to `dyn Any` for dynamic type casting.
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +43,9 @@ impl<T: Any> AnyBase for T {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A trait for advanced dynamic type manipulation and downcasting.
|
||||||
pub trait AnyTo: AnyBase {
|
pub trait AnyTo: AnyBase {
|
||||||
|
/// Checks if the type is of the specified type `T`.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn is<T>(&self) -> bool
|
fn is<T>(&self) -> bool
|
||||||
where
|
where
|
||||||
|
|
@ -47,6 +54,7 @@ pub trait AnyTo: AnyBase {
|
||||||
self.as_any_ref().is::<T>()
|
self.as_any_ref().is::<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to downcast a reference to the specified type `T`.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn downcast_ref<T>(&self) -> Option<&T>
|
fn downcast_ref<T>(&self) -> Option<&T>
|
||||||
where
|
where
|
||||||
|
|
@ -55,6 +63,7 @@ pub trait AnyTo: AnyBase {
|
||||||
self.as_any_ref().downcast_ref()
|
self.as_any_ref().downcast_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to downcast a mutable reference to the specified type `T`.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn downcast_mut<T>(&mut self) -> Option<&mut T>
|
fn downcast_mut<T>(&mut self) -> Option<&mut T>
|
||||||
where
|
where
|
||||||
|
|
@ -66,8 +75,11 @@ pub trait AnyTo: AnyBase {
|
||||||
|
|
||||||
impl<T: ?Sized + AnyBase> AnyTo for T {}
|
impl<T: ?Sized + AnyBase> AnyTo for T {}
|
||||||
|
|
||||||
// API to define functions that alter the behavior of PageTop core.
|
// API to define functions that modify the predefined behavior of the code.
|
||||||
pub mod action;
|
pub mod action;
|
||||||
|
|
||||||
// API to add new features with packages.
|
// API to add new features with packages.
|
||||||
pub mod package;
|
pub mod package;
|
||||||
|
|
||||||
|
// API to add new layouts with themes.
|
||||||
|
pub mod theme;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::core::action::add_action;
|
use crate::core::action::add_action;
|
||||||
use crate::core::package::PackageRef;
|
use crate::core::package::PackageRef;
|
||||||
|
use crate::core::theme::all::THEMES;
|
||||||
use crate::{service, trace};
|
use crate::{service, trace};
|
||||||
|
|
||||||
use std::sync::{LazyLock, RwLock};
|
use std::sync::{LazyLock, RwLock};
|
||||||
|
|
@ -60,7 +61,7 @@ fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
|
||||||
add_to_enabled(list, *d);
|
add_to_enabled(list, *d);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check if the package has an associated theme to register.
|
// Check if the package has an associated theme to register.
|
||||||
if let Some(theme) = package.theme() {
|
if let Some(theme) = package.theme() {
|
||||||
let mut registered_themes = THEMES.write().unwrap();
|
let mut registered_themes = THEMES.write().unwrap();
|
||||||
// Ensure the theme is not already registered to avoid duplicates.
|
// Ensure the theme is not already registered to avoid duplicates.
|
||||||
|
|
@ -73,7 +74,7 @@ fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trace::debug!("Enabling \"{}\" package", package.short_name());
|
trace::debug!("Enabling \"{}\" package", package.short_name());
|
||||||
} */
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::core::action::ActionBox;
|
use crate::core::action::ActionBox;
|
||||||
|
use crate::core::theme::ThemeRef;
|
||||||
use crate::core::AnyBase;
|
use crate::core::AnyBase;
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::{actions, service};
|
use crate::{actions, service};
|
||||||
|
|
@ -15,6 +16,10 @@ pub trait PackageTrait: AnyBase + Send + Sync {
|
||||||
L10n::none()
|
L10n::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn dependencies(&self) -> Vec<PackageRef> {
|
fn dependencies(&self) -> Vec<PackageRef> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
packages/pagetop/src/core/theme.rs
Normal file
8
packages/pagetop/src/core/theme.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
mod definition;
|
||||||
|
pub use definition::{ThemeRef, ThemeTrait};
|
||||||
|
/*
|
||||||
|
mod regions;
|
||||||
|
pub(crate) use regions::ComponentsInRegions;
|
||||||
|
pub use regions::InRegion;
|
||||||
|
*/
|
||||||
|
pub(crate) mod all;
|
||||||
32
packages/pagetop/src/core/theme/all.rs
Normal file
32
packages/pagetop/src/core/theme/all.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
use crate::core::theme::ThemeRef;
|
||||||
|
//use crate::global;
|
||||||
|
|
||||||
|
use std::sync::{LazyLock, RwLock};
|
||||||
|
|
||||||
|
// THEMES ******************************************************************************************
|
||||||
|
|
||||||
|
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
|
||||||
|
|
||||||
|
// DEFAULT THEME ***********************************************************************************
|
||||||
|
/*
|
||||||
|
pub static THEME_DEFAULT: LazyLock<ThemeRef> =
|
||||||
|
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
|
||||||
|
Some(theme) => theme,
|
||||||
|
None => &crate::base::theme::Inception,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
// THEME BY NAME ***********************************************************************************
|
||||||
|
/*
|
||||||
|
pub fn theme_by_short_name(short_name: &str) -> Option<ThemeRef> {
|
||||||
|
let short_name = short_name.to_lowercase();
|
||||||
|
match THEMES
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.short_name().to_lowercase() == short_name)
|
||||||
|
{
|
||||||
|
Some(theme) => Some(*theme),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
104
packages/pagetop/src/core/theme/definition.rs
Normal file
104
packages/pagetop/src/core/theme/definition.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
use crate::core::package::PackageTrait;
|
||||||
|
|
||||||
|
pub type ThemeRef = &'static dyn ThemeTrait;
|
||||||
|
|
||||||
|
/// Los temas deben implementar este "trait".
|
||||||
|
pub trait ThemeTrait: PackageTrait + Send + Sync {
|
||||||
|
/*
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn regions(&self) -> Vec<(&'static str, L10n)> {
|
||||||
|
vec![
|
||||||
|
("header", L10n::l("header")),
|
||||||
|
("pagetop", L10n::l("pagetop")),
|
||||||
|
("sidebar_left", L10n::l("sidebar_left")),
|
||||||
|
("content", L10n::l("content")),
|
||||||
|
("sidebar_right", L10n::l("sidebar_right")),
|
||||||
|
("footer", L10n::l("footer")),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn before_prepare_body(&self, page: &mut Page) {}
|
||||||
|
|
||||||
|
fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
|
||||||
|
let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
body id=[page.body_id().get()] class=[page.body_classes().get()] {
|
||||||
|
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
|
||||||
|
div class="skip__to_content" {
|
||||||
|
a href=(concat_string!("#", skip_to_id)) { (skip) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(flex::Container::new()
|
||||||
|
.with_id("body__wrapper")
|
||||||
|
.with_direction(flex::Direction::Column(BreakPoint::None))
|
||||||
|
.with_align(flex::Align::Center)
|
||||||
|
.add_item(flex::Item::region().with_id("header"))
|
||||||
|
.add_item(flex::Item::region().with_id("pagetop"))
|
||||||
|
.add_item(
|
||||||
|
flex::Item::with(
|
||||||
|
flex::Container::new()
|
||||||
|
.with_direction(flex::Direction::Row(BreakPoint::None))
|
||||||
|
.add_item(
|
||||||
|
flex::Item::region()
|
||||||
|
.with_id("sidebar_left")
|
||||||
|
.with_grow(flex::Grow::Is1),
|
||||||
|
)
|
||||||
|
.add_item(
|
||||||
|
flex::Item::region()
|
||||||
|
.with_id("content")
|
||||||
|
.with_grow(flex::Grow::Is3),
|
||||||
|
)
|
||||||
|
.add_item(
|
||||||
|
flex::Item::region()
|
||||||
|
.with_id("sidebar_right")
|
||||||
|
.with_grow(flex::Grow::Is1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_id("flex__wrapper"),
|
||||||
|
)
|
||||||
|
.add_item(flex::Item::region().with_id("footer"))
|
||||||
|
.render(page.context()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_prepare_body(&self, page: &mut Page) {
|
||||||
|
page.set_assets(AssetsOp::SetFaviconIfNone(
|
||||||
|
Favicon::new().with_icon("/base/favicon.ico"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_head(&self, page: &mut Page) -> PrepareMarkup {
|
||||||
|
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
|
||||||
|
@if let Some(title) = page.title() {
|
||||||
|
title { (global::SETTINGS.app.name) (" - ") (title) }
|
||||||
|
} @else {
|
||||||
|
title { (global::SETTINGS.app.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@if let Some(description) = page.description() {
|
||||||
|
meta name="description" content=(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
meta name="viewport" content=(viewport);
|
||||||
|
@for (name, content) in page.metadata() {
|
||||||
|
meta name=(name) content=(content) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta http-equiv="X-UA-Compatible" content="IE=edge";
|
||||||
|
@for (property, content) in page.properties() {
|
||||||
|
meta property=(property) content=(content) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
(page.context().prepare_assets())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
@ -1,115 +1,16 @@
|
||||||
//! Global settings, functions and macro helpers.
|
//! Global settings.
|
||||||
|
|
||||||
use crate::{config_defaults, trace};
|
use crate::static_config;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use std::io;
|
static_config!(SETTINGS: Settings => [
|
||||||
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]
|
||||||
"app.name" => "My App",
|
"app.name" => "My App",
|
||||||
"app.description" => "Developed with the amazing PageTop framework.",
|
"app.description" => "Developed with the amazing PageTop framework.",
|
||||||
"app.theme" => "Default",
|
"app.theme" => "",
|
||||||
"app.language" => "en-US",
|
"app.language" => "en-US",
|
||||||
"app.direction" => "ltr",
|
"app.text_direction" => "ltr",
|
||||||
"app.startup_banner" => "Slant",
|
"app.startup_banner" => "Slant",
|
||||||
|
|
||||||
// [dev]
|
// [dev]
|
||||||
|
|
@ -128,161 +29,93 @@ config_defaults!(SETTINGS: Settings => [
|
||||||
"server.session_lifetime" => 604_800,
|
"server.session_lifetime" => 604_800,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// *************************************************************************************************
|
#[derive(Debug, Deserialize)]
|
||||||
// FUNCTIONS HELPERS.
|
/// Configuration settings for the global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and
|
||||||
// *************************************************************************************************
|
/// [`[server]`](Server) sections (see [`SETTINGS`]).
|
||||||
|
pub struct Settings {
|
||||||
pub enum TypeInfo {
|
pub app: App,
|
||||||
FullName,
|
pub dev: Dev,
|
||||||
ShortName,
|
pub log: Log,
|
||||||
NameFrom(isize),
|
pub server: Server,
|
||||||
NameTo(isize),
|
|
||||||
PartialName(isize, isize),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeInfo {
|
#[derive(Debug, Deserialize)]
|
||||||
pub fn of<T: ?Sized>(&self) -> &'static str {
|
/// Section `[app]` of the configuration settings.
|
||||||
let type_name = std::any::type_name::<T>();
|
///
|
||||||
match self {
|
/// See [`Settings`].
|
||||||
TypeInfo::FullName => type_name,
|
pub struct App {
|
||||||
TypeInfo::ShortName => Self::partial(type_name, -1, None),
|
/// The name of the application.
|
||||||
TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None),
|
/// Default: *"My App"*.
|
||||||
TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)),
|
pub name: String,
|
||||||
TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)),
|
/// A brief description of the application.
|
||||||
}
|
/// Default: *"Developed with the amazing PageTop framework."*.
|
||||||
}
|
pub description: String,
|
||||||
|
/// Default theme.
|
||||||
fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
|
/// Default: *""*.
|
||||||
let maxlen = type_name.len();
|
pub theme: String,
|
||||||
let mut segments = Vec::new();
|
/// Default language (localization).
|
||||||
let mut segment_start = 0; // Start position of the current segment.
|
/// Default: *"en-US"*.
|
||||||
let mut angle_brackets = 0; // Counter for tracking '<' and '>'.
|
pub language: String,
|
||||||
let mut previous_char = '\0'; // Initializes to a null character, no previous character.
|
/// Default text direction: *"ltr"* (left-to-right), *"rtl"* (right-to-left), or *"auto"*.
|
||||||
|
/// Default: *"ltr"*.
|
||||||
for (idx, c) in type_name.char_indices() {
|
pub text_direction: String,
|
||||||
match c {
|
/// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*.
|
||||||
':' if angle_brackets == 0 => {
|
/// Default: *"Slant"*.
|
||||||
if previous_char == ':' {
|
pub startup_banner: String,
|
||||||
if segment_start < idx - 1 {
|
/// Default: according to the `PAGETOP_RUN_MODE` environment variable, or *"default"* if unset.
|
||||||
segments.push((segment_start, idx - 1)); // Do not include last '::'.
|
pub run_mode: String,
|
||||||
}
|
|
||||||
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.
|
#[derive(Debug, Deserialize)]
|
||||||
|
/// Section `[dev]` of the configuration settings.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// See [`Settings`].
|
||||||
///
|
pub struct Dev {
|
||||||
/// * `root_path` - A string slice that holds the root path.
|
/// Static files required by the application are integrated by default into the executable
|
||||||
/// * `relative_path` - A string slice that holds the relative path.
|
/// binary. However, during development, it can be useful to serve these files from their own
|
||||||
///
|
/// directory to avoid recompilation every time they are modified. In this case, specify the
|
||||||
/// # Returns
|
/// full path to the project's root directory.
|
||||||
///
|
/// Default: *""*.
|
||||||
/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`.
|
pub pagetop_project_dir: 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************************************
|
#[derive(Debug, Deserialize)]
|
||||||
// MACRO HELPERS.
|
/// Section `[log]` of the configuration settings.
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
/// Macro para construir grupos de pares clave-valor.
|
|
||||||
///
|
///
|
||||||
/// ```rust#ignore
|
/// See [`Settings`].
|
||||||
/// let args = kv![
|
pub struct Log {
|
||||||
/// "userName" => "Roberto",
|
/// Filter, or a comma-separated combination of filters, for execution traces: *"Error"*,
|
||||||
/// "photoCount" => 3,
|
/// *"Warn"*, *"Info"*, *"Debug"*, or *"Trace"*.
|
||||||
/// "userGender" => "male",
|
/// Example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
||||||
/// ];
|
/// Default: *"Info"*.
|
||||||
/// ```
|
pub tracing: String,
|
||||||
macro_rules! kv {
|
/// Displays traces in the terminal (*"Stdout"*) or logs them in files with rotation: *"Daily"*,
|
||||||
( $($key:expr => $value:expr),* $(,)? ) => {{
|
/// *"Hourly"*, *"Minutely"*, or *"Endless"*.
|
||||||
let mut a = std::collections::HashMap::new();
|
/// Default: *"Stdout"*.
|
||||||
$(
|
pub rolling: String,
|
||||||
a.insert($key.into(), $value.into());
|
/// Directory for trace files (if `rolling` != *"Stdout"*).
|
||||||
)*
|
/// Default: *"log"*.
|
||||||
a
|
pub path: String,
|
||||||
}};
|
/// Prefix for trace files (if `rolling` != *"Stdout"*).
|
||||||
|
/// Default: *"tracing.log"*.
|
||||||
|
pub prefix: String,
|
||||||
|
/// Trace output format. Options are *"Full"*, *"Compact"*, *"Pretty"*, or *"Json"*.
|
||||||
|
/// Default: *"Full"*.
|
||||||
|
pub format: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
/// Section `[server]` of the configuration settings.
|
||||||
|
///
|
||||||
|
/// See [`Settings`].
|
||||||
|
pub struct Server {
|
||||||
|
/// Web server bind address.
|
||||||
|
/// Default: *"localhost"*.
|
||||||
|
pub bind_address: String,
|
||||||
|
/// Web server bind port.
|
||||||
|
/// Default: *8088*.
|
||||||
|
pub bind_port: u16,
|
||||||
|
/// Session cookie duration in seconds (0 means "until the browser is closed").
|
||||||
|
/// Default: *604800* (7 days).
|
||||||
|
pub session_lifetime: i64,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
// RE-EXPORTED MACROS AND DERIVES.
|
// RE-EXPORTED.
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
pub use concat_string::concat_string as join;
|
pub use concat_string::concat_string as join;
|
||||||
|
|
@ -83,43 +83,30 @@ pub use paste::paste;
|
||||||
|
|
||||||
pub use pagetop_macros::{main, test, AutoDefault};
|
pub use pagetop_macros::{main, test, AutoDefault};
|
||||||
|
|
||||||
// *************************************************************************************************
|
pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;
|
||||||
// GLOBAL.
|
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
pub use static_files::Resource as StaticResource;
|
|
||||||
|
|
||||||
pub type HashMapResources = std::collections::HashMap<&'static str, StaticResource>;
|
|
||||||
|
|
||||||
pub use std::any::TypeId;
|
pub use std::any::TypeId;
|
||||||
|
|
||||||
pub type Weight = i8;
|
pub type Weight = i8;
|
||||||
|
|
||||||
// Global settings, functions and macro helpers.
|
|
||||||
pub mod global;
|
|
||||||
|
|
||||||
static_locales!(LOCALES_PAGETOP);
|
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
// PUBLIC API.
|
// API.
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
// Retrieve and apply settings values from configuration files.
|
// Useful functions and macros.
|
||||||
pub mod config;
|
pub mod util;
|
||||||
// Application tracing and event logging.
|
// Application tracing and event logging.
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
// Localization.
|
// Localization.
|
||||||
pub mod locale;
|
pub mod locale;
|
||||||
|
|
||||||
// Essential web framework.
|
// Essential web framework.
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
||||||
// Key types and functions for creating actions, components, packages, and themes.
|
// Key types and functions for creating actions, components, packages, and themes.
|
||||||
pub mod core;
|
pub mod core;
|
||||||
|
|
||||||
// Web request response variants.
|
// Web request response variants.
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
// Global settings.
|
||||||
|
pub mod global;
|
||||||
// Prepare and run the application.
|
// Prepare and run the application.
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
//! Localization (L10n).
|
//! Localization (L10n).
|
||||||
//!
|
//!
|
||||||
//! PageTop uses the [Fluent](https://www.projectfluent.org/) set of specifications for application
|
//! PageTop uses the [Fluent](https://www.projectfluent.org/) specifications for application
|
||||||
//! localization.
|
//! localization, leveraging the [fluent-templates](https://docs.rs/fluent-templates/) crate to
|
||||||
|
//! integrate translation resources directly into the application binary.
|
||||||
//!
|
//!
|
||||||
//! # Fluent Syntax (FTL)
|
//! # Fluent Syntax (FTL)
|
||||||
//!
|
//!
|
||||||
//! The format used to describe the translation resources used by Fluent is called
|
//! 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
|
//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be both readable and
|
||||||
//! simultaneously allowing the representation of complex natural language concepts to address
|
//! expressive, enabling complex natural language constructs like gender, plurals, and conjugations.
|
||||||
//! gender, plurals, conjugations, and others.
|
|
||||||
//!
|
//!
|
||||||
//! # Fluent Resources
|
//! # Fluent Resources
|
||||||
//!
|
//!
|
||||||
//! PageTop utilizes [fluent-templates](https://docs.rs/fluent-templates/) to integrate localization
|
//! Localization resources are organized in the *src/locale* directory, with subdirectories for
|
||||||
//! resources into the application binary. The following example groups files and subfolders from
|
//! each valid [Unicode Language Identifier](https://docs.rs/unic-langid/):
|
||||||
//! *src/locale* that have a valid [Unicode Language Identifier](https://docs.rs/unic-langid/) and
|
|
||||||
//! assigns them to their corresponding identifier:
|
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! src/locale/
|
//! src/locale/
|
||||||
|
|
@ -86,8 +84,9 @@
|
||||||
//! static_locales!(LOCALES_SAMPLE in "path/to/locale");
|
//! static_locales!(LOCALES_SAMPLE in "path/to/locale");
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::{global, kv, AutoDefault, LOCALES_PAGETOP};
|
use crate::{global, kv, AutoDefault};
|
||||||
|
|
||||||
|
pub use fluent_bundle::FluentValue;
|
||||||
pub use fluent_templates;
|
pub use fluent_templates;
|
||||||
pub use unic_langid::LanguageIdentifier;
|
pub use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
|
@ -103,6 +102,8 @@ use std::fmt;
|
||||||
|
|
||||||
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
|
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
|
||||||
|
|
||||||
|
/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
|
||||||
|
/// and human-readable names.
|
||||||
static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
|
static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
|
||||||
kv
|
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
|
||||||
/// through `SETTINGS.app.language`.
|
/// through `SETTINGS.app.language`.
|
||||||
pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = LazyLock::new(|| {
|
pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> =
|
||||||
langid_for(global::SETTINGS.app.language.as_str()).unwrap_or(&LANGID_FALLBACK)
|
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK));
|
||||||
});
|
|
||||||
|
|
||||||
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> {
|
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> {
|
||||||
let language = language.into();
|
let language = language.into();
|
||||||
match LANGUAGES.get(language.as_str()) {
|
if language.is_empty() {
|
||||||
Some((langid, _)) => Ok(langid),
|
return Ok(&LANGID_FALLBACK);
|
||||||
None => {
|
|
||||||
if language.is_empty() {
|
|
||||||
Ok(&LANGID_FALLBACK)
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"No langid for Unicode Language Identifier \"{language}\".",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
LANGUAGES
|
||||||
|
.get(&language)
|
||||||
|
.map(|(langid, _)| langid)
|
||||||
|
.ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\"."))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
/// Defines a set of localization elements and local translation texts.
|
/// Defines a set of localization elements and local translation texts, removing Unicode isolating
|
||||||
|
/// marks around arguments to improve readability and compatibility in certain rendering contexts.
|
||||||
macro_rules! static_locales {
|
macro_rules! static_locales {
|
||||||
( $LOCALES:ident $(, $core_locales:literal)? ) => {
|
( $LOCALES:ident $(, $core_locales:literal)? ) => {
|
||||||
$crate::locale::fluent_templates::static_loader! {
|
$crate::locale::fluent_templates::static_loader! {
|
||||||
|
|
@ -165,6 +161,8 @@ macro_rules! static_locales {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static_locales!(LOCALES_PAGETOP);
|
||||||
|
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
enum L10nOp {
|
enum L10nOp {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -177,7 +175,7 @@ enum L10nOp {
|
||||||
pub struct L10n {
|
pub struct L10n {
|
||||||
op: L10nOp,
|
op: L10nOp,
|
||||||
locales: Option<&'static Locales>,
|
locales: Option<&'static Locales>,
|
||||||
args: HashMap<String, String>,
|
args: HashMap<String, FluentValue<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl L10n {
|
impl L10n {
|
||||||
|
|
@ -209,7 +207,8 @@ impl L10n {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
|
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
|
||||||
self.args.insert(arg.into(), value.into());
|
self.args
|
||||||
|
.insert(arg.into(), FluentValue::from(value.into()));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,17 +221,7 @@ impl L10n {
|
||||||
if self.args.is_empty() {
|
if self.args.is_empty() {
|
||||||
locales.try_lookup(langid, key)
|
locales.try_lookup(langid, key)
|
||||||
} else {
|
} else {
|
||||||
locales.try_lookup_with_args(
|
locales.try_lookup_with_args(langid, key, &self.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,
|
None => None,
|
||||||
|
|
@ -266,13 +255,7 @@ impl fmt::Display for L10n {
|
||||||
_ => &LANGID_DEFAULT,
|
_ => &LANGID_DEFAULT,
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
&self
|
&self.args,
|
||||||
.args
|
|
||||||
.iter()
|
|
||||||
.fold(HashMap::new(), |mut args, (key, value)| {
|
|
||||||
args.insert(key.to_string(), value.to_owned().into());
|
|
||||||
args
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
//! The `PageTop` Prelude.
|
//! The `PageTop` Prelude.
|
||||||
|
|
||||||
// RE-EXPORTED.
|
// RE-EXPORTED.
|
||||||
pub use crate::{join, main, paste, test, AutoDefault};
|
|
||||||
|
|
||||||
// GLOBAL.
|
pub use crate::{join, main, paste, test};
|
||||||
pub use crate::{global, HashMapResources, TypeId, Weight};
|
|
||||||
|
pub use crate::{AutoDefault, StaticResources, TypeId, Weight};
|
||||||
|
|
||||||
// MACROS.
|
// MACROS.
|
||||||
|
|
||||||
// crate::global
|
// crate::util
|
||||||
pub use crate::kv;
|
pub use crate::{kv, static_config};
|
||||||
// crate::config
|
|
||||||
pub use crate::config_defaults;
|
|
||||||
// crate::locale
|
// crate::locale
|
||||||
pub use crate::static_locales;
|
pub use crate::static_locales;
|
||||||
// crate::service
|
// crate::service
|
||||||
|
|
@ -21,6 +19,8 @@ pub use crate::actions;
|
||||||
|
|
||||||
// API.
|
// API.
|
||||||
|
|
||||||
|
pub use crate::util;
|
||||||
|
|
||||||
pub use crate::trace;
|
pub use crate::trace;
|
||||||
|
|
||||||
pub use crate::locale::*;
|
pub use crate::locale::*;
|
||||||
|
|
@ -35,4 +35,6 @@ pub use crate::core::package::*;
|
||||||
|
|
||||||
pub use crate::response::{json::*, redirect::*, ResponseError};
|
pub use crate::response::{json::*, redirect::*, ResponseError};
|
||||||
|
|
||||||
|
pub use crate::global;
|
||||||
|
|
||||||
pub use crate::app::Application;
|
pub use crate::app::Application;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ macro_rules! static_files {
|
||||||
mod [<static_files_ $bundle>] {
|
mod [<static_files_ $bundle>] {
|
||||||
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
|
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
|
||||||
}
|
}
|
||||||
static $STATIC: std::sync::LazyLock<HashMapResources> = std::sync::LazyLock::new(
|
static $STATIC: std::sync::LazyLock<StaticResources> = std::sync::LazyLock::new(
|
||||||
[<static_files_ $bundle>]::$bundle
|
[<static_files_ $bundle>]::$bundle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ macro_rules! static_files_service {
|
||||||
let mut serve_embedded:bool = true;
|
let mut serve_embedded:bool = true;
|
||||||
$(
|
$(
|
||||||
if !$root.is_empty() && !$relative.is_empty() {
|
if !$root.is_empty() && !$relative.is_empty() {
|
||||||
if let Ok(absolute) = $crate::global::absolute_dir($root, $relative) {
|
if let Ok(absolute) = $crate::util::absolute_dir($root, $relative) {
|
||||||
$scfg.service($crate::service::ActixFiles::new(
|
$scfg.service($crate::service::ActixFiles::new(
|
||||||
$path,
|
$path,
|
||||||
absolute,
|
absolute,
|
||||||
|
|
|
||||||
310
packages/pagetop/src/util.rs
Normal file
310
packages/pagetop/src/util.rs
Normal file
|
|
@ -0,0 +1,310 @@
|
||||||
|
//! Useful functions and macros.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
mod data;
|
||||||
|
mod de;
|
||||||
|
mod error;
|
||||||
|
mod file;
|
||||||
|
mod path;
|
||||||
|
mod source;
|
||||||
|
mod value;
|
||||||
|
|
||||||
|
use crate::trace;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
// USEFUL FUNCTIONS.
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
|
pub enum TypeInfo {
|
||||||
|
FullName,
|
||||||
|
ShortName,
|
||||||
|
NameFrom(isize),
|
||||||
|
NameTo(isize),
|
||||||
|
PartialName(isize, isize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeInfo {
|
||||||
|
pub fn of<T: ?Sized>(&self) -> &'static str {
|
||||||
|
let type_name = std::any::type_name::<T>();
|
||||||
|
match self {
|
||||||
|
TypeInfo::FullName => type_name,
|
||||||
|
TypeInfo::ShortName => Self::partial(type_name, -1, None),
|
||||||
|
TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None),
|
||||||
|
TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)),
|
||||||
|
TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
|
||||||
|
let maxlen = type_name.len();
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
let mut segment_start = 0; // Start position of the current segment.
|
||||||
|
let mut angle_brackets = 0; // Counter for tracking '<' and '>'.
|
||||||
|
let mut previous_char = '\0'; // Initializes to a null character, no previous character.
|
||||||
|
|
||||||
|
for (idx, c) in type_name.char_indices() {
|
||||||
|
match c {
|
||||||
|
':' if angle_brackets == 0 => {
|
||||||
|
if previous_char == ':' {
|
||||||
|
if segment_start < idx - 1 {
|
||||||
|
segments.push((segment_start, idx - 1)); // Do not include last '::'.
|
||||||
|
}
|
||||||
|
segment_start = idx + 1; // Next segment starts after '::'.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'<' => angle_brackets += 1,
|
||||||
|
'>' => angle_brackets -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
previous_char = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the last segment if there's any.
|
||||||
|
if segment_start < maxlen {
|
||||||
|
segments.push((segment_start, maxlen));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the start position.
|
||||||
|
let start_pos = segments
|
||||||
|
.get(if start >= 0 {
|
||||||
|
start as usize
|
||||||
|
} else {
|
||||||
|
segments.len() - start.unsigned_abs()
|
||||||
|
})
|
||||||
|
.map_or(0, |&(s, _)| s);
|
||||||
|
|
||||||
|
// Calculates the end position.
|
||||||
|
let end_pos = segments
|
||||||
|
.get(if let Some(end) = end {
|
||||||
|
if end >= 0 {
|
||||||
|
end as usize
|
||||||
|
} else {
|
||||||
|
segments.len() - end.unsigned_abs()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
segments.len() - 1
|
||||||
|
})
|
||||||
|
.map_or(maxlen, |&(_, e)| e);
|
||||||
|
|
||||||
|
// Returns the partial string based on the calculated positions.
|
||||||
|
&type_name[start_pos..end_pos]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the absolute directory given a root path and a relative path.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `root_path` - A string slice that holds the root path.
|
||||||
|
/// * `relative_path` - A string slice that holds the relative path.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`.
|
||||||
|
/// * `Err` - If an I/O error occurs, returns an `io::Error`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function will return an error if:
|
||||||
|
/// - The root path or relative path are invalid.
|
||||||
|
/// - There is an issue with file system operations, such as reading the directory.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let root = "/home/user";
|
||||||
|
/// let relative = "documents";
|
||||||
|
/// let abs_dir = absolute_dir(root, relative).unwrap();
|
||||||
|
/// println!("{}", abs_dir);
|
||||||
|
/// ```
|
||||||
|
pub fn absolute_dir(
|
||||||
|
root_path: impl Into<String>,
|
||||||
|
relative_path: impl Into<String>,
|
||||||
|
) -> Result<String, io::Error> {
|
||||||
|
let root_path = PathBuf::from(root_path.into());
|
||||||
|
let full_path = root_path.join(relative_path.into());
|
||||||
|
let absolute_dir = full_path.to_string_lossy().into();
|
||||||
|
|
||||||
|
if !full_path.is_absolute() {
|
||||||
|
let message = format!("Path \"{absolute_dir}\" is not absolute");
|
||||||
|
trace::warn!(message);
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !full_path.exists() {
|
||||||
|
let message = format!("Path \"{absolute_dir}\" does not exist");
|
||||||
|
trace::warn!(message);
|
||||||
|
return Err(io::Error::new(io::ErrorKind::NotFound, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !full_path.is_dir() {
|
||||||
|
let message = format!("Path \"{absolute_dir}\" is not a directory");
|
||||||
|
trace::warn!(message);
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(absolute_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************************************
|
||||||
|
// USEFUL MACROS.
|
||||||
|
// *************************************************************************************************
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
/// Macro para construir grupos de pares clave-valor.
|
||||||
|
///
|
||||||
|
/// ```rust#ignore
|
||||||
|
/// let args = kv![
|
||||||
|
/// "userName" => "Roberto",
|
||||||
|
/// "photoCount" => 3,
|
||||||
|
/// "userGender" => "male",
|
||||||
|
/// ];
|
||||||
|
/// ```
|
||||||
|
macro_rules! kv {
|
||||||
|
( $($key:expr => $value:expr),* $(,)? ) => {{
|
||||||
|
let mut a = std::collections::HashMap::new();
|
||||||
|
$(
|
||||||
|
a.insert($key.into(), $value.into());
|
||||||
|
)*
|
||||||
|
a
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
/// Define un conjunto de ajustes de configuración usando tipos seguros y valores predefinidos.
|
||||||
|
///
|
||||||
|
/// Detiene la aplicación con un panic! si no pueden asignarse los ajustes de configuración.
|
||||||
|
///
|
||||||
|
/// Carga la configuración de la aplicación en forma de pares `clave = valor` recogidos en archivos
|
||||||
|
/// [TOML](https://toml.io).
|
||||||
|
///
|
||||||
|
/// La metodología [The Twelve-Factor App](https://12factor.net/es/) define **la configuración de
|
||||||
|
/// una aplicación como todo lo que puede variar entre despliegues**, diferenciando entre entornos
|
||||||
|
/// de desarrollo, pre-producción, producción, etc.
|
||||||
|
///
|
||||||
|
/// A veces las aplicaciones guardan configuraciones como constantes en el código, lo que implica
|
||||||
|
/// una violación de esta metodología. `PageTop` recomienda una **estricta separación entre código y
|
||||||
|
/// configuración**. La configuración variará en cada tipo de despliegue, y el código no.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// # Cómo cargar los ajustes de configuración
|
||||||
|
///
|
||||||
|
/// Si tu aplicación requiere archivos de configuración debes crear un directorio *config* al mismo
|
||||||
|
/// nivel del archivo *Cargo.toml* de tu proyecto (o del ejecutable binario de la aplicación).
|
||||||
|
///
|
||||||
|
/// `PageTop` se encargará de cargar todos los ajustes de configuración de tu aplicación leyendo los
|
||||||
|
/// siguientes archivos TOML en este orden (todos los archivos son opcionales):
|
||||||
|
///
|
||||||
|
/// 1. **config/common.toml**, útil para los ajustes comunes a cualquier entorno. Estos valores
|
||||||
|
/// podrán ser sobrescritos al fusionar los archivos de configuración restantes.
|
||||||
|
///
|
||||||
|
/// 2. **config/{file}.toml**, donde *{file}* se define con la variable de entorno
|
||||||
|
/// `PAGETOP_RUN_MODE`:
|
||||||
|
///
|
||||||
|
/// * Si no está definida se asumirá *default* por defecto y `PageTop` intentará cargar el
|
||||||
|
/// archivo *config/default.toml* si existe.
|
||||||
|
///
|
||||||
|
/// * De esta manera podrás tener diferentes ajustes de configuración para diferentes entornos
|
||||||
|
/// de ejecución. Por ejemplo, para *devel.toml*, *staging.toml* o *production.toml*. O
|
||||||
|
/// también para *server1.toml* o *server2.toml*. Sólo uno será cargado.
|
||||||
|
///
|
||||||
|
/// * Normalmente estos archivos suelen ser idóneos para incluir contraseñas o configuración
|
||||||
|
/// sensible asociada al entorno correspondiente. Estos archivos no deberían ser publicados en
|
||||||
|
/// el repositorio Git por razones de seguridad.
|
||||||
|
///
|
||||||
|
/// 3. **config/local.toml**, para añadir o sobrescribir ajustes de los archivos anteriores.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// # Cómo añadir ajustes de configuración
|
||||||
|
///
|
||||||
|
/// Para proporcionar a tu **módulo** sus propios ajustes de configuración, añade
|
||||||
|
/// [*serde*](https://docs.rs/serde) en las dependencias de tu archivo *Cargo.toml* habilitando la
|
||||||
|
/// característica `derive`:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// [dependencies]
|
||||||
|
/// serde = { version = "1.0", features = ["derive"] }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Y luego inicializa con la macro [`static_config!`](crate::static_config) tus ajustes, usando
|
||||||
|
/// tipos seguros y asignando los valores predefinidos para la estructura asociada:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
/// use serde::Deserialize;
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Deserialize)]
|
||||||
|
/// pub struct Settings {
|
||||||
|
/// pub myapp: MyApp,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Deserialize)]
|
||||||
|
/// pub struct MyApp {
|
||||||
|
/// pub name: String,
|
||||||
|
/// pub description: Option<String>,
|
||||||
|
/// pub width: u16,
|
||||||
|
/// pub height: u16,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// static_config!(SETTINGS: Settings => [
|
||||||
|
/// // [myapp]
|
||||||
|
/// "myapp.name" => "Value Name",
|
||||||
|
/// "myapp.width" => 900,
|
||||||
|
/// "myapp.height" => 320,
|
||||||
|
/// ]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// De hecho, así se declaran los ajustes globales de la configuración (ver [`SETTINGS`]).
|
||||||
|
///
|
||||||
|
/// Puedes usar la [sintaxis TOML](https://toml.io/en/v1.0.0#table) para añadir tu nueva sección
|
||||||
|
/// `[myapp]` en los archivos de configuración, del mismo modo que se añaden `[log]` o `[server]` en
|
||||||
|
/// los ajustes globales (ver [`Settings`]).
|
||||||
|
///
|
||||||
|
/// Se recomienda inicializar todos los ajustes con valores predefinidos, o utilizar la notación
|
||||||
|
/// `Option<T>` si van a ser tratados en el código como opcionales.
|
||||||
|
///
|
||||||
|
/// Si no pueden inicializarse correctamente los ajustes de configuración, entonces la aplicación
|
||||||
|
/// ejecutará un panic! y detendrá la ejecución.
|
||||||
|
///
|
||||||
|
/// Los ajustes de configuración siempre serán de sólo lectura.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// # Cómo usar tus nuevos ajustes de configuración
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
/// use crate::config;
|
||||||
|
///
|
||||||
|
/// fn global_settings() {
|
||||||
|
/// println!("App name: {}", &global::SETTINGS.app.name);
|
||||||
|
/// println!("App description: {}", &global::SETTINGS.app.description);
|
||||||
|
/// println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn package_settings() {
|
||||||
|
/// println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
|
||||||
|
/// println!("{}", &config::SETTINGS.myapp.width);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
macro_rules! static_config {
|
||||||
|
( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => {
|
||||||
|
#[doc = concat!(
|
||||||
|
"Assigned or predefined values for configuration settings associated to the ",
|
||||||
|
"[`", stringify!($Settings), "`] type."
|
||||||
|
)]
|
||||||
|
pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| {
|
||||||
|
let mut settings = $crate::util::config::CONFIG_DATA.clone();
|
||||||
|
$(
|
||||||
|
settings.set_default($key, $value).unwrap();
|
||||||
|
)*
|
||||||
|
match settings.try_into() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => panic!("Error parsing settings: {}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
51
packages/pagetop/src/util/config.rs
Normal file
51
packages/pagetop/src/util/config.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
//! Retrieve settings values from configuration files.
|
||||||
|
|
||||||
|
use crate::join;
|
||||||
|
use crate::util::data::ConfigData;
|
||||||
|
use crate::util::file::File;
|
||||||
|
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Original configuration values in `key = value` pairs gathered from configuration files.
|
||||||
|
pub static CONFIG_DATA: LazyLock<ConfigData> = LazyLock::new(|| {
|
||||||
|
// Identify the configuration directory.
|
||||||
|
let config_dir = env::var("CARGO_MANIFEST_DIR")
|
||||||
|
.map(|manifest_dir| {
|
||||||
|
let manifest_config = Path::new(&manifest_dir).join("config");
|
||||||
|
if manifest_config.exists() {
|
||||||
|
manifest_config.to_string_lossy().to_string()
|
||||||
|
} else {
|
||||||
|
"config".to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|_| "config".to_string());
|
||||||
|
|
||||||
|
// Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'.
|
||||||
|
let 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
|
||||||
|
});
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::config::error::*;
|
use crate::util::error::*;
|
||||||
use crate::config::path;
|
use crate::util::path;
|
||||||
use crate::config::source::Source;
|
use crate::util::source::Source;
|
||||||
use crate::config::value::Value;
|
use crate::util::value::Value;
|
||||||
|
|
||||||
use serde::de::Deserialize;
|
use serde::de::Deserialize;
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::config::data::ConfigData;
|
use crate::util::data::ConfigData;
|
||||||
use crate::config::error::*;
|
use crate::util::error::*;
|
||||||
use crate::config::value::{Table, Value, ValueKind};
|
use crate::util::value::{Table, Value, ValueKind};
|
||||||
|
|
||||||
use serde::de;
|
use serde::de;
|
||||||
use serde::forward_to_deserialize_any;
|
use serde::forward_to_deserialize_any;
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
mod source;
|
mod source;
|
||||||
mod toml;
|
mod toml;
|
||||||
|
|
||||||
use crate::config::error::*;
|
use crate::util::error::*;
|
||||||
use crate::config::source::Source;
|
use crate::util::source::Source;
|
||||||
use crate::config::value::Value;
|
use crate::util::value::Value;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::value::{Value, ValueKind};
|
use crate::util::value::{Value, ValueKind};
|
||||||
|
|
||||||
use toml;
|
use toml;
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::config::error::*;
|
use crate::util::error::*;
|
||||||
use crate::config::value::{Value, ValueKind};
|
use crate::util::value::{Value, ValueKind};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::config::error::*;
|
use crate::util::error::*;
|
||||||
use crate::config::path;
|
use crate::util::path;
|
||||||
use crate::config::value::{Value, ValueKind};
|
use crate::util::value::{Value, ValueKind};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::error::*;
|
use crate::util::error::*;
|
||||||
|
|
||||||
use serde::de::{Deserialize, Deserializer, Visitor};
|
use serde::de::{Deserialize, Deserializer, Visitor};
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue