♻️ 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-session = { version = "0.10", features = ["cookie-session"] }
|
||||
fluent-templates = "0.11"
|
||||
fluent-bundle = "0.15"
|
||||
nom = "7.1"
|
||||
substring = "1.4"
|
||||
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]
|
||||
#theme = "Basic"
|
||||
#theme = "Chassis"
|
||||
theme = "Inception"
|
||||
#theme = "Bootsier"
|
||||
#theme = "Bulmix"
|
||||
language = "es-ES"
|
||||
|
||||
[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;
|
||||
|
||||
// Common definitions for core types.
|
||||
/// A base trait that extends `Any` to provide metadata and dynamic type casting
|
||||
/// capabilities.
|
||||
pub trait AnyBase: Any {
|
||||
/// Returns the full name of the type.
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
/// Returns a short name for the type.
|
||||
fn short_name(&self) -> &'static str;
|
||||
|
||||
/// Returns a reference to `dyn Any` for dynamic type casting.
|
||||
fn as_any_ref(&self) -> &dyn Any;
|
||||
|
||||
/// Returns a mutable reference to `dyn Any` for dynamic type casting.
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +43,9 @@ impl<T: Any> AnyBase for T {
|
|||
}
|
||||
}
|
||||
|
||||
/// A trait for advanced dynamic type manipulation and downcasting.
|
||||
pub trait AnyTo: AnyBase {
|
||||
/// Checks if the type is of the specified type `T`.
|
||||
#[inline]
|
||||
fn is<T>(&self) -> bool
|
||||
where
|
||||
|
|
@ -47,6 +54,7 @@ pub trait AnyTo: AnyBase {
|
|||
self.as_any_ref().is::<T>()
|
||||
}
|
||||
|
||||
/// Attempts to downcast a reference to the specified type `T`.
|
||||
#[inline]
|
||||
fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
|
|
@ -55,6 +63,7 @@ pub trait AnyTo: AnyBase {
|
|||
self.as_any_ref().downcast_ref()
|
||||
}
|
||||
|
||||
/// Attempts to downcast a mutable reference to the specified type `T`.
|
||||
#[inline]
|
||||
fn downcast_mut<T>(&mut self) -> Option<&mut T>
|
||||
where
|
||||
|
|
@ -66,8 +75,11 @@ pub trait AnyTo: AnyBase {
|
|||
|
||||
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;
|
||||
|
||||
// API to add new features with packages.
|
||||
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::package::PackageRef;
|
||||
use crate::core::theme::all::THEMES;
|
||||
use crate::{service, trace};
|
||||
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
|
@ -60,7 +61,7 @@ fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
|
|||
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() {
|
||||
let mut registered_themes = THEMES.write().unwrap();
|
||||
// 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 {
|
||||
trace::debug!("Enabling \"{}\" package", package.short_name());
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::core::action::ActionBox;
|
||||
use crate::core::theme::ThemeRef;
|
||||
use crate::core::AnyBase;
|
||||
use crate::locale::L10n;
|
||||
use crate::{actions, service};
|
||||
|
|
@ -15,6 +16,10 @@ pub trait PackageTrait: AnyBase + Send + Sync {
|
|||
L10n::none()
|
||||
}
|
||||
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
None
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<PackageRef> {
|
||||
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 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 => [
|
||||
static_config!(SETTINGS: Settings => [
|
||||
// [app]
|
||||
"app.name" => "My App",
|
||||
"app.description" => "Developed with the amazing PageTop framework.",
|
||||
"app.theme" => "Default",
|
||||
"app.theme" => "",
|
||||
"app.language" => "en-US",
|
||||
"app.direction" => "ltr",
|
||||
"app.text_direction" => "ltr",
|
||||
"app.startup_banner" => "Slant",
|
||||
|
||||
// [dev]
|
||||
|
|
@ -128,161 +29,93 @@ config_defaults!(SETTINGS: Settings => [
|
|||
"server.session_lifetime" => 604_800,
|
||||
]);
|
||||
|
||||
// *************************************************************************************************
|
||||
// FUNCTIONS HELPERS.
|
||||
// *************************************************************************************************
|
||||
|
||||
pub enum TypeInfo {
|
||||
FullName,
|
||||
ShortName,
|
||||
NameFrom(isize),
|
||||
NameTo(isize),
|
||||
PartialName(isize, isize),
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Configuration settings for the global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and
|
||||
/// [`[server]`](Server) sections (see [`SETTINGS`]).
|
||||
pub struct Settings {
|
||||
pub app: App,
|
||||
pub dev: Dev,
|
||||
pub log: Log,
|
||||
pub server: Server,
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Section `[app]` of the configuration settings.
|
||||
///
|
||||
/// See [`Settings`].
|
||||
pub struct App {
|
||||
/// The name of the application.
|
||||
/// Default: *"My App"*.
|
||||
pub name: String,
|
||||
/// A brief description of the application.
|
||||
/// Default: *"Developed with the amazing PageTop framework."*.
|
||||
pub description: String,
|
||||
/// Default theme.
|
||||
/// Default: *""*.
|
||||
pub theme: String,
|
||||
/// Default language (localization).
|
||||
/// Default: *"en-US"*.
|
||||
pub language: String,
|
||||
/// Default text direction: *"ltr"* (left-to-right), *"rtl"* (right-to-left), or *"auto"*.
|
||||
/// Default: *"ltr"*.
|
||||
pub text_direction: String,
|
||||
/// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*.
|
||||
/// Default: *"Slant"*.
|
||||
pub startup_banner: String,
|
||||
/// Default: according to the `PAGETOP_RUN_MODE` environment variable, or *"default"* if unset.
|
||||
pub run_mode: String,
|
||||
}
|
||||
|
||||
/// Calculates the absolute directory given a root path and a relative path.
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Section `[dev]` of the configuration settings.
|
||||
///
|
||||
/// # 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)
|
||||
/// See [`Settings`].
|
||||
pub struct Dev {
|
||||
/// Static files required by the application are integrated by default into the executable
|
||||
/// binary. However, during development, it can be useful to serve these files from their own
|
||||
/// directory to avoid recompilation every time they are modified. In this case, specify the
|
||||
/// full path to the project's root directory.
|
||||
/// Default: *""*.
|
||||
pub pagetop_project_dir: String,
|
||||
}
|
||||
|
||||
// *************************************************************************************************
|
||||
// MACRO HELPERS.
|
||||
// *************************************************************************************************
|
||||
|
||||
#[macro_export]
|
||||
/// Macro para construir grupos de pares clave-valor.
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Section `[log]` of the configuration settings.
|
||||
///
|
||||
/// ```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
|
||||
}};
|
||||
/// See [`Settings`].
|
||||
pub struct Log {
|
||||
/// Filter, or a comma-separated combination of filters, for execution traces: *"Error"*,
|
||||
/// *"Warn"*, *"Info"*, *"Debug"*, or *"Trace"*.
|
||||
/// Example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
||||
/// Default: *"Info"*.
|
||||
pub tracing: String,
|
||||
/// Displays traces in the terminal (*"Stdout"*) or logs them in files with rotation: *"Daily"*,
|
||||
/// *"Hourly"*, *"Minutely"*, or *"Endless"*.
|
||||
/// Default: *"Stdout"*.
|
||||
pub rolling: String,
|
||||
/// Directory for trace files (if `rolling` != *"Stdout"*).
|
||||
/// Default: *"log"*.
|
||||
pub path: String,
|
||||
/// Prefix for trace files (if `rolling` != *"Stdout"*).
|
||||
/// Default: *"tracing.log"*.
|
||||
pub prefix: String,
|
||||
/// Trace output format. Options are *"Full"*, *"Compact"*, *"Pretty"*, or *"Json"*.
|
||||
/// Default: *"Full"*.
|
||||
pub format: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Section `[server]` of the configuration settings.
|
||||
///
|
||||
/// See [`Settings`].
|
||||
pub struct Server {
|
||||
/// Web server bind address.
|
||||
/// Default: *"localhost"*.
|
||||
pub bind_address: String,
|
||||
/// Web server bind port.
|
||||
/// Default: *8088*.
|
||||
pub bind_port: u16,
|
||||
/// Session cookie duration in seconds (0 means "until the browser is closed").
|
||||
/// Default: *604800* (7 days).
|
||||
pub session_lifetime: i64,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
// *************************************************************************************************
|
||||
// RE-EXPORTED MACROS AND DERIVES.
|
||||
// RE-EXPORTED.
|
||||
// *************************************************************************************************
|
||||
|
||||
pub use concat_string::concat_string as join;
|
||||
|
|
@ -83,43 +83,30 @@ 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 type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;
|
||||
|
||||
pub use std::any::TypeId;
|
||||
|
||||
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.
|
||||
pub mod config;
|
||||
// Useful functions and macros.
|
||||
pub mod util;
|
||||
// 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;
|
||||
|
||||
// Global settings.
|
||||
pub mod global;
|
||||
// Prepare and run the application.
|
||||
pub mod app;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
//! Localization (L10n).
|
||||
//!
|
||||
//! PageTop uses the [Fluent](https://www.projectfluent.org/) set of specifications for application
|
||||
//! localization.
|
||||
//! PageTop uses the [Fluent](https://www.projectfluent.org/) specifications for application
|
||||
//! localization, leveraging the [fluent-templates](https://docs.rs/fluent-templates/) crate to
|
||||
//! integrate translation resources directly into the application binary.
|
||||
//!
|
||||
//! # Fluent Syntax (FTL)
|
||||
//!
|
||||
//! The format used to describe the translation resources used by Fluent is called
|
||||
//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be easy to read while
|
||||
//! simultaneously allowing the representation of complex natural language concepts to address
|
||||
//! gender, plurals, conjugations, and others.
|
||||
//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be both readable and
|
||||
//! expressive, enabling complex natural language constructs like gender, plurals, and conjugations.
|
||||
//!
|
||||
//! # Fluent Resources
|
||||
//!
|
||||
//! 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:
|
||||
//! Localization resources are organized in the *src/locale* directory, with subdirectories for
|
||||
//! each valid [Unicode Language Identifier](https://docs.rs/unic-langid/):
|
||||
//!
|
||||
//! ```text
|
||||
//! src/locale/
|
||||
|
|
@ -86,8 +84,9 @@
|
|||
//! 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 unic_langid::LanguageIdentifier;
|
||||
|
||||
|
|
@ -103,6 +102,8 @@ use std::fmt;
|
|||
|
||||
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
|
||||
|
||||
/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
|
||||
/// and human-readable names.
|
||||
static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
|
||||
kv
|
||||
/// 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 static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> =
|
||||
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).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}\".",
|
||||
))
|
||||
}
|
||||
}
|
||||
if language.is_empty() {
|
||||
return Ok(&LANGID_FALLBACK);
|
||||
}
|
||||
LANGUAGES
|
||||
.get(&language)
|
||||
.map(|(langid, _)| langid)
|
||||
.ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\"."))
|
||||
}
|
||||
|
||||
#[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 {
|
||||
( $LOCALES:ident $(, $core_locales:literal)? ) => {
|
||||
$crate::locale::fluent_templates::static_loader! {
|
||||
|
|
@ -165,6 +161,8 @@ macro_rules! static_locales {
|
|||
};
|
||||
}
|
||||
|
||||
static_locales!(LOCALES_PAGETOP);
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
enum L10nOp {
|
||||
#[default]
|
||||
|
|
@ -177,7 +175,7 @@ enum L10nOp {
|
|||
pub struct L10n {
|
||||
op: L10nOp,
|
||||
locales: Option<&'static Locales>,
|
||||
args: HashMap<String, String>,
|
||||
args: HashMap<String, FluentValue<'static>>,
|
||||
}
|
||||
|
||||
impl L10n {
|
||||
|
|
@ -209,7 +207,8 @@ impl L10n {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -222,17 +221,7 @@ impl L10n {
|
|||
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
|
||||
}),
|
||||
)
|
||||
locales.try_lookup_with_args(langid, key, &self.args)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
|
|
@ -266,13 +255,7 @@ impl fmt::Display for L10n {
|
|||
_ => &LANGID_DEFAULT,
|
||||
},
|
||||
key,
|
||||
&self
|
||||
.args
|
||||
.iter()
|
||||
.fold(HashMap::new(), |mut args, (key, value)| {
|
||||
args.insert(key.to_string(), value.to_owned().into());
|
||||
args
|
||||
}),
|
||||
&self.args,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
//! The `PageTop` Prelude.
|
||||
|
||||
// RE-EXPORTED.
|
||||
pub use crate::{join, main, paste, test, AutoDefault};
|
||||
|
||||
// GLOBAL.
|
||||
pub use crate::{global, HashMapResources, TypeId, Weight};
|
||||
pub use crate::{join, main, paste, test};
|
||||
|
||||
pub use crate::{AutoDefault, StaticResources, TypeId, Weight};
|
||||
|
||||
// MACROS.
|
||||
|
||||
// crate::global
|
||||
pub use crate::kv;
|
||||
// crate::config
|
||||
pub use crate::config_defaults;
|
||||
// crate::util
|
||||
pub use crate::{kv, static_config};
|
||||
// crate::locale
|
||||
pub use crate::static_locales;
|
||||
// crate::service
|
||||
|
|
@ -21,6 +19,8 @@ pub use crate::actions;
|
|||
|
||||
// API.
|
||||
|
||||
pub use crate::util;
|
||||
|
||||
pub use crate::trace;
|
||||
|
||||
pub use crate::locale::*;
|
||||
|
|
@ -35,4 +35,6 @@ pub use crate::core::package::*;
|
|||
|
||||
pub use crate::response::{json::*, redirect::*, ResponseError};
|
||||
|
||||
pub use crate::global;
|
||||
|
||||
pub use crate::app::Application;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ macro_rules! static_files {
|
|||
mod [<static_files_ $bundle>] {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ macro_rules! static_files_service {
|
|||
let mut serve_embedded:bool = true;
|
||||
$(
|
||||
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(
|
||||
$path,
|
||||
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::config::path;
|
||||
use crate::config::source::Source;
|
||||
use crate::config::value::Value;
|
||||
use crate::util::error::*;
|
||||
use crate::util::path;
|
||||
use crate::util::source::Source;
|
||||
use crate::util::value::Value;
|
||||
|
||||
use serde::de::Deserialize;
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::data::ConfigData;
|
||||
use crate::config::error::*;
|
||||
use crate::config::value::{Table, Value, ValueKind};
|
||||
use crate::util::data::ConfigData;
|
||||
use crate::util::error::*;
|
||||
use crate::util::value::{Table, Value, ValueKind};
|
||||
|
||||
use serde::de;
|
||||
use serde::forward_to_deserialize_any;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
mod source;
|
||||
mod toml;
|
||||
|
||||
use crate::config::error::*;
|
||||
use crate::config::source::Source;
|
||||
use crate::config::value::Value;
|
||||
use crate::util::error::*;
|
||||
use crate::util::source::Source;
|
||||
use crate::util::value::Value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::config::value::{Value, ValueKind};
|
||||
use crate::util::value::{Value, ValueKind};
|
||||
|
||||
use toml;
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::config::error::*;
|
||||
use crate::config::value::{Value, ValueKind};
|
||||
use crate::util::error::*;
|
||||
use crate::util::value::{Value, ValueKind};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::error::*;
|
||||
use crate::config::path;
|
||||
use crate::config::value::{Value, ValueKind};
|
||||
use crate::util::error::*;
|
||||
use crate::util::path;
|
||||
use crate::util::value::{Value, ValueKind};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::config::error::*;
|
||||
use crate::util::error::*;
|
||||
|
||||
use serde::de::{Deserialize, Deserializer, Visitor};
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue