diff --git a/pagetop/src/config.rs b/pagetop/src/config.rs new file mode 100644 index 00000000..caa3471f --- /dev/null +++ b/pagetop/src/config.rs @@ -0,0 +1,195 @@ +//! Retrieve settings values from configuration files. +//! +//! 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 [`include_config!`](crate::include_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, +//! } +//! +//! include_config!(SETTINGS: Settings from [ +//! // [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::concat_string; +use crate::config::data::ConfigData; +use crate::config::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 rm = 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(&concat_string!(config_dir, "/common.toml")).required(false)) + .expect("Failed to merge common configuration (common.toml)") + // Add the environment-specific configuration. Defaults to 'default.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false)) + .expect(&format!("Failed to merge {rm}.toml configuration")) + // Add reserved local configuration for the environment. Defaults to 'local.default.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false)) + .expect("Failed to merge reserved local environment configuration") + // Add the general reserved local configuration. Defaults to 'local.toml'. + .merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false)) + .expect("Failed to merge general reserved local configuration") + // Save the execution mode. + .set("app.run_mode", rm) + .expect("Failed to set application run mode"); + + settings +}); + +#[macro_export] +macro_rules! include_config { + ( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => { + #[doc = concat!( + "Assigned or predefined values for configuration settings associated to the ", + "[`", stringify!($Settings), "`] type." + )] + pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| { + let mut settings = $crate::config::CONFIG_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/pagetop/src/util/config/data.rs b/pagetop/src/config/data.rs similarity index 96% rename from pagetop/src/util/config/data.rs rename to pagetop/src/config/data.rs index b84b8eb4..22fe8359 100644 --- a/pagetop/src/util/config/data.rs +++ b/pagetop/src/config/data.rs @@ -1,7 +1,7 @@ -use crate::util::config::error::*; -use crate::util::config::path; -use crate::util::config::source::Source; -use crate::util::config::value::Value; +use crate::config::error::*; +use crate::config::path; +use crate::config::source::Source; +use crate::config::value::Value; use serde::de::Deserialize; diff --git a/pagetop/src/util/config/de.rs b/pagetop/src/config/de.rs similarity index 99% rename from pagetop/src/util/config/de.rs rename to pagetop/src/config/de.rs index 7433f6c6..875219af 100644 --- a/pagetop/src/util/config/de.rs +++ b/pagetop/src/config/de.rs @@ -1,6 +1,6 @@ -use crate::util::config::data::ConfigData; -use crate::util::config::error::*; -use crate::util::config::value::{Table, Value, ValueKind}; +use crate::config::data::ConfigData; +use crate::config::error::*; +use crate::config::value::{Table, Value, ValueKind}; use serde::de; use serde::forward_to_deserialize_any; diff --git a/pagetop/src/util/config/error.rs b/pagetop/src/config/error.rs similarity index 100% rename from pagetop/src/util/config/error.rs rename to pagetop/src/config/error.rs diff --git a/pagetop/src/util/config/file.rs b/pagetop/src/config/file.rs similarity index 94% rename from pagetop/src/util/config/file.rs rename to pagetop/src/config/file.rs index 3c0a78ee..00f0c34d 100644 --- a/pagetop/src/util/config/file.rs +++ b/pagetop/src/config/file.rs @@ -1,9 +1,9 @@ mod source; mod toml; -use crate::util::config::error::*; -use crate::util::config::source::Source; -use crate::util::config::value::Value; +use crate::config::error::*; +use crate::config::source::Source; +use crate::config::value::Value; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/pagetop/src/util/config/file/source.rs b/pagetop/src/config/file/source.rs similarity index 100% rename from pagetop/src/util/config/file/source.rs rename to pagetop/src/config/file/source.rs diff --git a/pagetop/src/util/config/file/toml.rs b/pagetop/src/config/file/toml.rs similarity index 96% rename from pagetop/src/util/config/file/toml.rs rename to pagetop/src/config/file/toml.rs index bb84233c..e8fa06c6 100644 --- a/pagetop/src/util/config/file/toml.rs +++ b/pagetop/src/config/file/toml.rs @@ -1,4 +1,4 @@ -use crate::util::config::value::{Value, ValueKind}; +use crate::config::value::{Value, ValueKind}; use toml; diff --git a/pagetop/src/util/config/path.rs b/pagetop/src/config/path.rs similarity index 98% rename from pagetop/src/util/config/path.rs rename to pagetop/src/config/path.rs index 0b45bb96..72376a95 100644 --- a/pagetop/src/util/config/path.rs +++ b/pagetop/src/config/path.rs @@ -1,5 +1,5 @@ -use crate::util::config::error::*; -use crate::util::config::value::{Value, ValueKind}; +use crate::config::error::*; +use crate::config::value::{Value, ValueKind}; use std::collections::HashMap; use std::str::FromStr; diff --git a/pagetop/src/util/config/path/parser.rs b/pagetop/src/config/path/parser.rs similarity index 100% rename from pagetop/src/util/config/path/parser.rs rename to pagetop/src/config/path/parser.rs diff --git a/pagetop/src/util/config/source.rs b/pagetop/src/config/source.rs similarity index 94% rename from pagetop/src/util/config/source.rs rename to pagetop/src/config/source.rs index 6cac5a24..5e693b68 100644 --- a/pagetop/src/util/config/source.rs +++ b/pagetop/src/config/source.rs @@ -1,6 +1,6 @@ -use crate::util::config::error::*; -use crate::util::config::path; -use crate::util::config::value::{Value, ValueKind}; +use crate::config::error::*; +use crate::config::path; +use crate::config::value::{Value, ValueKind}; use std::collections::HashMap; use std::fmt::Debug; diff --git a/pagetop/src/util/config/value.rs b/pagetop/src/config/value.rs similarity index 99% rename from pagetop/src/util/config/value.rs rename to pagetop/src/config/value.rs index dc933b0b..29d62cfe 100644 --- a/pagetop/src/util/config/value.rs +++ b/pagetop/src/config/value.rs @@ -1,4 +1,4 @@ -use crate::util::config::error::*; +use crate::config::error::*; use serde::de::{Deserialize, Deserializer, Visitor}; diff --git a/pagetop/src/lib.rs b/pagetop/src/lib.rs index 7ab5dade..889f4f14 100644 --- a/pagetop/src/lib.rs +++ b/pagetop/src/lib.rs @@ -91,6 +91,8 @@ pub type Weight = i8; // Useful functions and macros. pub mod util; +// Retrieve and apply settings values from configuration files. +pub mod config; // Application tracing and event logging. pub mod trace; // HTML in code. diff --git a/pagetop/src/prelude.rs b/pagetop/src/prelude.rs index a222e88a..c1489152 100644 --- a/pagetop/src/prelude.rs +++ b/pagetop/src/prelude.rs @@ -9,7 +9,9 @@ pub use crate::{AutoDefault, StaticResources, TypeId, Weight}; // MACROS. // crate::util -pub use crate::{include_config, kv}; +pub use crate::kv; +// crate::config +pub use crate::include_config; // crate::locale pub use crate::include_locales; // crate::service diff --git a/pagetop/src/util.rs b/pagetop/src/util.rs index ed5ca946..a34ee599 100644 --- a/pagetop/src/util.rs +++ b/pagetop/src/util.rs @@ -1,7 +1,5 @@ //! Useful functions and macros. -pub mod config; - use crate::trace; use std::io; @@ -161,138 +159,3 @@ macro_rules! kv { 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 [`include_config!`](crate::include_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, -/// } -/// -/// include_config!(SETTINGS: Settings from [ -/// // [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! include_config { - ( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => { - #[doc = concat!( - "Assigned or predefined values for configuration settings associated to the ", - "[`", stringify!($Settings), "`] type." - )] - pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| { - let mut settings = $crate::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/pagetop/src/util/config.rs b/pagetop/src/util/config.rs deleted file mode 100644 index 5c57d880..00000000 --- a/pagetop/src/util/config.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Retrieve settings values from configuration files. - -mod data; -mod de; -mod error; -mod file; -mod path; -mod source; -mod value; - -use crate::concat_string; -use crate::util::config::data::ConfigData; -use crate::util::config::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 rm = 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(&concat_string!(config_dir, "/common.toml")).required(false)) - .expect("Failed to merge common configuration (common.toml)") - // Add the environment-specific configuration. Defaults to 'default.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/", rm, ".toml")).required(false)) - .expect(&format!("Failed to merge {rm}.toml configuration")) - // Add reserved local configuration for the environment. Defaults to 'local.default.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".toml")).required(false)) - .expect("Failed to merge reserved local environment configuration") - // Add the general reserved local configuration. Defaults to 'local.toml'. - .merge(File::with_name(&concat_string!(config_dir, "/local.toml")).required(false)) - .expect("Failed to merge general reserved local configuration") - // Save the execution mode. - .set("app.run_mode", rm) - .expect("Failed to set application run mode"); - - settings -});