diff --git a/Cargo.toml b/Cargo.toml index 91e175ec..eb584dcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/config/predefined-settings.toml b/config/predefined-settings.toml new file mode 100644 index 00000000..f6e8e8a0 --- /dev/null +++ b/config/predefined-settings.toml @@ -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 diff --git a/docs/predefined-settings.toml b/docs/predefined-settings.toml deleted file mode 100644 index 08571a92..00000000 --- a/docs/predefined-settings.toml +++ /dev/null @@ -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 diff --git a/packages/drust/config/default.toml b/packages/drust/config/default.toml index 6f119629..a7a99871 100644 --- a/packages/drust/config/default.toml +++ b/packages/drust/config/default.toml @@ -1,9 +1,6 @@ [app] -#theme = "Basic" -#theme = "Chassis" theme = "Inception" #theme = "Bootsier" -#theme = "Bulmix" language = "es-ES" [log] diff --git a/packages/pagetop/src/config.rs b/packages/pagetop/src/config.rs deleted file mode 100644 index 372b6e91..00000000 --- a/packages/pagetop/src/config.rs +++ /dev/null @@ -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, -//! 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` 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 = 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), - } - }); - }; -} diff --git a/packages/pagetop/src/core.rs b/packages/pagetop/src/core.rs index dc24270e..2e307887 100644 --- a/packages/pagetop/src/core.rs +++ b/packages/pagetop/src/core.rs @@ -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 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(&self) -> bool where @@ -47,6 +54,7 @@ pub trait AnyTo: AnyBase { self.as_any_ref().is::() } + /// Attempts to downcast a reference to the specified type `T`. #[inline] fn downcast_ref(&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(&mut self) -> Option<&mut T> where @@ -66,8 +75,11 @@ pub trait AnyTo: AnyBase { impl 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; diff --git a/packages/pagetop/src/core/package/all.rs b/packages/pagetop/src/core/package/all.rs index 4be416a6..43da0065 100644 --- a/packages/pagetop/src/core/package/all.rs +++ b/packages/pagetop/src/core/package/all.rs @@ -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, 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, package: PackageRef) { } } else { trace::debug!("Enabling \"{}\" package", package.short_name()); - } */ + } } } diff --git a/packages/pagetop/src/core/package/definition.rs b/packages/pagetop/src/core/package/definition.rs index 18880a1f..3506a929 100644 --- a/packages/pagetop/src/core/package/definition.rs +++ b/packages/pagetop/src/core/package/definition.rs @@ -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 { + None + } + fn dependencies(&self) -> Vec { vec![] } diff --git a/packages/pagetop/src/core/theme.rs b/packages/pagetop/src/core/theme.rs new file mode 100644 index 00000000..a12fad0b --- /dev/null +++ b/packages/pagetop/src/core/theme.rs @@ -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; diff --git a/packages/pagetop/src/core/theme/all.rs b/packages/pagetop/src/core/theme/all.rs new file mode 100644 index 00000000..ce1b8528 --- /dev/null +++ b/packages/pagetop/src/core/theme/all.rs @@ -0,0 +1,32 @@ +use crate::core::theme::ThemeRef; +//use crate::global; + +use std::sync::{LazyLock, RwLock}; + +// THEMES ****************************************************************************************** + +pub static THEMES: LazyLock>> = LazyLock::new(|| RwLock::new(Vec::new())); + +// DEFAULT THEME *********************************************************************************** +/* +pub static THEME_DEFAULT: LazyLock = + 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 { + 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, + } +} +*/ diff --git a/packages/pagetop/src/core/theme/definition.rs b/packages/pagetop/src/core/theme/definition.rs new file mode 100644 index 00000000..70c178f2 --- /dev/null +++ b/packages/pagetop/src/core/theme/definition.rs @@ -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()) + } + }) + } + */ +} diff --git a/packages/pagetop/src/global.rs b/packages/pagetop/src/global.rs index 9a6fd2bf..49c0cdba 100644 --- a/packages/pagetop/src/global.rs +++ b/packages/pagetop/src/global.rs @@ -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(&self) -> &'static str { - let type_name = std::any::type_name::(); - 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) -> &'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, - relative_path: impl Into, -) -> Result { - 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, } diff --git a/packages/pagetop/src/lib.rs b/packages/pagetop/src/lib.rs index 0f3b42bd..913ca22f 100644 --- a/packages/pagetop/src/lib.rs +++ b/packages/pagetop/src/lib.rs @@ -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; diff --git a/packages/pagetop/src/locale.rs b/packages/pagetop/src/locale.rs index 466914a0..80b366b3 100644 --- a/packages/pagetop/src/locale.rs +++ b/packages/pagetop/src/locale.rs @@ -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> = LazyLock::new(|| { kv![ "en" => (langid!("en-US"), "English"), @@ -118,28 +119,23 @@ pub static LANGID_FALLBACK: LazyLock = LazyLock::new(|| lang /// Sets the application's default /// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) /// through `SETTINGS.app.language`. -pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = LazyLock::new(|| { - langid_for(global::SETTINGS.app.language.as_str()).unwrap_or(&LANGID_FALLBACK) -}); +pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = + LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK)); pub fn langid_for(language: impl Into) -> 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, + args: HashMap>, } impl L10n { @@ -209,7 +207,8 @@ impl L10n { } pub fn with_arg(mut self, arg: impl Into, value: impl Into) -> 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, ) } ) diff --git a/packages/pagetop/src/prelude.rs b/packages/pagetop/src/prelude.rs index 53d80a34..541fac96 100644 --- a/packages/pagetop/src/prelude.rs +++ b/packages/pagetop/src/prelude.rs @@ -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; diff --git a/packages/pagetop/src/service.rs b/packages/pagetop/src/service.rs index 379472e3..e28be6e9 100644 --- a/packages/pagetop/src/service.rs +++ b/packages/pagetop/src/service.rs @@ -26,7 +26,7 @@ macro_rules! static_files { mod [] { include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs")); } - static $STATIC: std::sync::LazyLock = std::sync::LazyLock::new( + static $STATIC: std::sync::LazyLock = std::sync::LazyLock::new( []::$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, diff --git a/packages/pagetop/src/util.rs b/packages/pagetop/src/util.rs new file mode 100644 index 00000000..6348487e --- /dev/null +++ b/packages/pagetop/src/util.rs @@ -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(&self) -> &'static str { + let type_name = std::any::type_name::(); + 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) -> &'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, + relative_path: impl Into, +) -> Result { + 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, +/// 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` 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), + } + }); + }; +} diff --git a/packages/pagetop/src/util/config.rs b/packages/pagetop/src/util/config.rs new file mode 100644 index 00000000..98c02f38 --- /dev/null +++ b/packages/pagetop/src/util/config.rs @@ -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 = 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 +}); diff --git a/packages/pagetop/src/config/data.rs b/packages/pagetop/src/util/data.rs similarity index 96% rename from packages/pagetop/src/config/data.rs rename to packages/pagetop/src/util/data.rs index 22fe8359..b62803fb 100644 --- a/packages/pagetop/src/config/data.rs +++ b/packages/pagetop/src/util/data.rs @@ -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; diff --git a/packages/pagetop/src/config/de.rs b/packages/pagetop/src/util/de.rs similarity index 99% rename from packages/pagetop/src/config/de.rs rename to packages/pagetop/src/util/de.rs index 875219af..55a58037 100644 --- a/packages/pagetop/src/config/de.rs +++ b/packages/pagetop/src/util/de.rs @@ -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; diff --git a/packages/pagetop/src/config/error.rs b/packages/pagetop/src/util/error.rs similarity index 100% rename from packages/pagetop/src/config/error.rs rename to packages/pagetop/src/util/error.rs diff --git a/packages/pagetop/src/config/file.rs b/packages/pagetop/src/util/file.rs similarity index 95% rename from packages/pagetop/src/config/file.rs rename to packages/pagetop/src/util/file.rs index 00f0c34d..643f4ea1 100644 --- a/packages/pagetop/src/config/file.rs +++ b/packages/pagetop/src/util/file.rs @@ -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}; diff --git a/packages/pagetop/src/config/file/source.rs b/packages/pagetop/src/util/file/source.rs similarity index 100% rename from packages/pagetop/src/config/file/source.rs rename to packages/pagetop/src/util/file/source.rs diff --git a/packages/pagetop/src/config/file/toml.rs b/packages/pagetop/src/util/file/toml.rs similarity index 96% rename from packages/pagetop/src/config/file/toml.rs rename to packages/pagetop/src/util/file/toml.rs index e8fa06c6..88e8230f 100644 --- a/packages/pagetop/src/config/file/toml.rs +++ b/packages/pagetop/src/util/file/toml.rs @@ -1,4 +1,4 @@ -use crate::config::value::{Value, ValueKind}; +use crate::util::value::{Value, ValueKind}; use toml; diff --git a/packages/pagetop/src/config/path.rs b/packages/pagetop/src/util/path.rs similarity index 98% rename from packages/pagetop/src/config/path.rs rename to packages/pagetop/src/util/path.rs index 72376a95..8b365d6a 100644 --- a/packages/pagetop/src/config/path.rs +++ b/packages/pagetop/src/util/path.rs @@ -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; diff --git a/packages/pagetop/src/config/path/parser.rs b/packages/pagetop/src/util/path/parser.rs similarity index 100% rename from packages/pagetop/src/config/path/parser.rs rename to packages/pagetop/src/util/path/parser.rs diff --git a/packages/pagetop/src/config/source.rs b/packages/pagetop/src/util/source.rs similarity index 95% rename from packages/pagetop/src/config/source.rs rename to packages/pagetop/src/util/source.rs index 5e693b68..5b1ae11d 100644 --- a/packages/pagetop/src/config/source.rs +++ b/packages/pagetop/src/util/source.rs @@ -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; diff --git a/packages/pagetop/src/config/value.rs b/packages/pagetop/src/util/value.rs similarity index 99% rename from packages/pagetop/src/config/value.rs rename to packages/pagetop/src/util/value.rs index 29d62cfe..cee2dc32 100644 --- a/packages/pagetop/src/config/value.rs +++ b/packages/pagetop/src/util/value.rs @@ -1,4 +1,4 @@ -use crate::config::error::*; +use crate::util::error::*; use serde::de::{Deserialize, Deserializer, Visitor};