pagetop/src/config.rs

267 lines
10 KiB
Rust

//! Carga las opciones de configuración.
//!
//! Estos ajustes se obtienen de archivos [TOML](https://toml.io) como pares `clave = valor` que se
//! mapean a estructuras **fuertemente tipadas** y valores predefinidos.
//!
//! Siguiendo la metodología [Twelve-Factor App](https://12factor.net/config), `PageTop` separa el
//! **código** de la **configuración**, lo que permite tener configuraciones diferentes para cada
//! despliegue, como *dev*, *staging* o *production*, sin modificar el código fuente.
//!
//!
//! # Orden de carga
//!
//! Si tu aplicación necesita archivos de configuración, crea un directorio `config` en la raíz del
//! proyecto, al mismo nivel que el archivo *Cargo.toml* o que el binario de la aplicación.
//!
//! `PageTop` carga en este orden, y siempre de forma opcional, los siguientes archivos TOML:
//!
//! 1. **config/common.toml**, para ajustes comunes a todos los entornos. Este enfoque simplifica el
//! mantenimiento al centralizar los valores de configuración comunes.
//!
//! 2. **config/{rm}.toml**, donde `{rm}` es el valor de la variable de entorno `PAGETOP_RUN_MODE`:
//!
//! * Si `PAGETOP_RUN_MODE` no está definida, se asume el valor `default`, y `PageTop` intentará
//! cargar *config/default.toml* si el archivo existe.
//!
//! * Útil para definir configuraciones específicas por entorno, garantizando que cada uno (p.ej.
//! *dev*, *staging* o *production*) disponga de sus propias opciones, como claves de API,
//! URLs o ajustes de rendimiento, sin afectar a los demás.
//!
//! 3. **config/local.{rm}.toml**, útil para configuraciones locales específicas de la máquina o de
//! la ejecución:
//!
//! * Permite añadir o sobrescribir ajustes propios del entorno. Por ejemplo, `local.dev.toml`
//! para desarrollo o `local.production.toml` para retoques en producción.
//!
//! * Facilita que cada desarrollador adapte la configuración a su equipo en un entorno dado. Por
//! lo general no se comparte ni se sube al sistema de control de versiones.
//!
//! 4. **config/local.toml**, para ajustes locales válidos en cualquier entorno, ideal para cambios
//! rápidos o valores temporales que no dependan de un entorno concreto.
//!
//! Los archivos se combinan en el orden anterior, cada archivo sobrescribe a los anteriores en caso
//! de conflicto.
//!
//!
//! # Cómo añadir opciones de configuración a tu código
//!
//! Añade [*serde*](https://docs.rs/serde) en tu archivo *Cargo.toml* con la *feature* `derive`:
//!
//! ```toml
//! [dependencies]
//! serde = { version = "1.0", features = ["derive"] }
//! ```
//!
//! Y usa la macro [`include_config!`](crate::include_config) para inicializar tus ajustes en una
//! estructura con tipos seguros. Por ejemplo:
//!
//! ```rust,no_run
//! use pagetop::prelude::*;
//! use serde::Deserialize;
//!
//! include_config!(SETTINGS: Settings => [
//! // [myapp]
//! "myapp.name" => "Value Name",
//! "myapp.width" => 900,
//! "myapp.height" => 320,
//! ]);
//!
//! #[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,
//! }
//! ```
//!
//! De esta forma estás añadiendo una nueva sección `[myapp]` a la configuración, igual que existen
//! `[app]` o `[server]` en las opciones globales de [`Settings`](crate::global::Settings).
//!
//! Se recomienda proporcionar siempre valores por defecto o usar `Option<T>` para los ajustes
//! opcionales.
//!
//! Si la configuración no se inicializa correctamente, la aplicación lanzará *panic* y detendrá la
//! ejecución.
//!
//! Las estructuras de configuración son de **sólo lectura** durante la ejecución.
//!
//!
//! # Usando tus opciones de configuración
//!
//! ```rust,ignore
//! use pagetop::prelude::*;
//! use crate::config;
//!
//! fn global_settings() {
//! println!("Nombre de la app: {}", &global::SETTINGS.app.name);
//! println!("Descripción: {}", &global::SETTINGS.app.description);
//! println!("Run mode: {}", &global::SETTINGS.app.run_mode);
//! }
//!
//! fn extension_settings() {
//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
//! println!("{}", &config::SETTINGS.myapp.width);
//! }
//! ```
use config::builder::DefaultState;
use config::{Config, ConfigBuilder, File};
use std::env;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
// Nombre del directorio de configuración por defecto.
const DEFAULT_CONFIG_DIR: &str = "config";
// Modo de ejecución por defecto.
const DEFAULT_RUN_MODE: &str = "default";
/// Valores originales cargados desde los archivos de configuración como pares `clave = valor`.
pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(|| {
// Determina el directorio de configuración:
// - Usa CONFIG_DIR si está definido en el entorno (p.ej.: CONFIG_DIR=/etc/myapp ./myapp).
// - Si no, intenta DEFAULT_CONFIG_DIR dentro del proyecto (en CARGO_MANIFEST_DIR).
// - Si nada de esto aplica, entonces usa DEFAULT_CONFIG_DIR relativo al ejecutable.
let config_dir: PathBuf = if let Ok(env_dir) = env::var("CONFIG_DIR") {
env_dir.into()
} else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let manifest_config = Path::new(&manifest_dir).join(DEFAULT_CONFIG_DIR);
if manifest_config.exists() {
manifest_config
} else {
DEFAULT_CONFIG_DIR.into()
}
} else {
DEFAULT_CONFIG_DIR.into()
};
// Determina el modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Por defecto usa
// DEFAULT_RUN_MODE si no está definida (p.ej.: PAGETOP_RUN_MODE=production ./myapp).
let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| DEFAULT_RUN_MODE.into());
Config::builder()
// 1. Configuración común para todos los entornos (common.toml).
.add_source(File::from(config_dir.join("common.toml")).required(false))
// 2. Configuración específica del entorno (p.ej.: default.toml, production.toml).
.add_source(File::from(config_dir.join(format!("{rm}.toml"))).required(false))
// 3. Configuración local reservada para cada entorno (p.ej.: local.default.toml).
.add_source(File::from(config_dir.join(format!("local.{rm}.toml"))).required(false))
// 4. Configuración local común (local.toml).
.add_source(File::from(config_dir.join("local.toml")).required(false))
// Guarda el modo de ejecución explícitamente.
.set_override("app.run_mode", rm)
.expect("Failed to set application run mode")
});
/// Incluye los ajustes necesarios de la configuración anticipando valores por defecto.
///
/// ### Sintaxis
///
/// Hay que añadir en nuestra librería el siguiente código:
///
/// ```rust,ignore
/// include_config!(SETTINGS: Settings => [
/// "ruta.clave" => valor,
/// // …
/// ]);
/// ```
///
/// donde:
///
/// * **`SETTINGS_NAME`** es el nombre de la variable global que se usará para referenciar los
/// ajustes. Se recomienda usar `SETTINGS`, aunque no es obligatorio.
/// * **`Settings_Type`** es la referencia a la estructura que define los tipos para deserializar la
/// configuración. Debe implementar `Deserialize` (derivable con `#[derive(Deserialize)]`).
/// * **Lista de pares** con las claves TOML que requieran valores por defecto. Siguen la notación
/// `"seccion.subclave"` para coincidir con el árbol TOML.
///
/// ### Ejemplo básico
///
/// ```rust,no_run
/// use pagetop::prelude::*;
/// use serde::Deserialize;
///
/// include_config!(SETTINGS: BlogSettings => [
/// // [blog]
/// "blog.title" => "Mi Blog",
/// "blog.port" => 8080,
/// ]);
///
/// #[derive(Debug, Deserialize)]
/// pub struct BlogSettings {
/// pub blog: Blog,
/// }
///
/// #[derive(Debug, Deserialize)]
/// pub struct Blog {
/// pub title: String,
/// pub description: Option<String>,
/// pub port: u16,
/// }
///
/// fn print_title() {
/// // Lectura en tiempo de ejecución.
/// println!("Título: {}", SETTINGS.blog.title);
/// }
/// ```
///
/// ### Buenas prácticas
///
/// * **Valores por defecto**. Declara un valor por defecto para cada clave obligatoria. Las claves
/// opcionales pueden ser `Option<T>`.
///
/// * **Secciones únicas**. Agrupa tus claves dentro de una sección exclusiva (p.ej. `[blog]`) para
/// evitar colisiones con otras librerías.
///
/// * **Solo lectura**. La variable generada es inmutable durante toda la vida del programa. Para
/// configurar distintos entornos (*dev*, *staging*, *prod*) usa los archivos TOML descritos en la
/// documentación de [`config`](crate::config).
///
/// * **Errores explícitos**. Si la deserialización falla, la macro lanzará un `panic!` con un
/// mensaje que indica la estructura problemática, facilitando la depuración.
///
/// ### Requisitos
///
/// * Dependencia `serde` con la *feature* `derive`.
/// * Las claves deben coincidir con los campos (*snake case*) de tu estructura `Settings_Type`.
///
/// ```toml
/// [dependencies]
/// serde = { version = "1.0", features = ["derive"] }
/// ```
#[macro_export]
macro_rules! include_config {
( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => {
#[doc = concat!(
"Referencia a los ajustes de configuración deserializados de [`",
stringify!($Settings_Type),
"`]."
)]
#[doc = ""]
#[doc = "Valores por defecto:"]
#[doc = "```text"]
$(
#[doc = concat!($k, " = ", stringify!($v))]
)*
#[doc = "```"]
pub static $SETTINGS_NAME: std::sync::LazyLock<$Settings_Type> =
std::sync::LazyLock::new(|| {
let mut settings = $crate::config::CONFIG_VALUES.clone();
$(
settings = settings.set_default($k, $v).unwrap();
)*
settings
.build()
.expect(concat!("Failed to build config for ", stringify!($Settings_Type)))
.try_deserialize::<$Settings_Type>()
.expect(concat!("Error parsing settings for ", stringify!($Settings_Type)))
});
};
}