🚧 Review config settings
This commit is contained in:
parent
41cedf2541
commit
4a4dd26cf5
9 changed files with 358 additions and 371 deletions
187
src/config.rs
187
src/config.rs
|
|
@ -1,63 +1,73 @@
|
||||||
//! Retrieve and apply settings values from configuration files.
|
//! Load configuration settings.
|
||||||
//!
|
//!
|
||||||
//! Carga la configuración de la aplicación en forma de pares `clave = valor` recogidos en archivos
|
//! These settings are loaded from [TOML](https://toml.io) files as `key = value` pairs and mapped
|
||||||
//! [TOML](https://toml.io).
|
//! into type-safe structures with predefined values.
|
||||||
//!
|
//!
|
||||||
//! La metodología [The Twelve-Factor App](https://12factor.net/es/) define **la configuración de
|
//! Following the [Twelve-Factor App](https://12factor.net/config) methodology, `PageTop` separates
|
||||||
//! una aplicación como todo lo que puede variar entre despliegues**, diferenciando entre entornos
|
//! code from configuration. This approach allows configurations to vary across deployments, such as
|
||||||
//! de desarrollo, pre-producción, producción, etc.
|
//! development, staging, or production, without changing the codebase.
|
||||||
//!
|
|
||||||
//! 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
|
//! # Loading configuration settings
|
||||||
//!
|
//!
|
||||||
//! Si tu aplicación requiere archivos de configuración debes crear un directorio *config* al mismo
|
//! If your application requires configuration files, create a `config` directory in the root of
|
||||||
//! nivel del archivo *Cargo.toml* de tu proyecto (o del ejecutable binario de la aplicación).
|
//! your project, at the same level as the *Cargo.toml* file or the application's binary.
|
||||||
//!
|
//!
|
||||||
//! `PageTop` se encargará de cargar todos los ajustes de configuración de tu aplicación leyendo los
|
//! `PageTop` automatically loads configuration settings by reading the following TOML files in
|
||||||
//! siguientes archivos TOML en este orden (todos los archivos son opcionales):
|
//! order (all files are optional):
|
||||||
//!
|
//!
|
||||||
//! 1. **config/common.toml**, útil para los ajustes comunes a cualquier entorno. Estos valores
|
//! 1. **config/common.toml**, for settings shared across all environments. This approach simplifies
|
||||||
//! podrán ser sobrescritos al fusionar los archivos de configuración restantes.
|
//! maintenance by centralizing common configuration values.
|
||||||
//!
|
//!
|
||||||
//! 2. **config/{file}.toml**, donde *{file}* se define con la variable de entorno
|
//! 2. **config/{rm}.toml**, where `{rm}` corresponds to the environment variable
|
||||||
//! `PAGETOP_RUN_MODE`:
|
//! `PAGETOP_RUN_MODE`:
|
||||||
//!
|
//!
|
||||||
//! * Si no está definida se asumirá *default* por defecto y `PageTop` intentará cargar el
|
//! * If `PAGETOP_RUN_MODE` is not set, it defaults to `default`, and `PageTop` attempts to load
|
||||||
//! archivo *config/default.toml* si existe.
|
//! *config/default.toml* if available.
|
||||||
//!
|
//!
|
||||||
//! * De esta manera podrás tener diferentes ajustes de configuración para diferentes entornos
|
//! * Useful for environment-specific configurations, ensuring that each environment
|
||||||
//! de ejecución. Por ejemplo, para *devel.toml*, *staging.toml* o *production.toml*. O
|
//! (e.g., development, staging, production) has its own settings without affecting others,
|
||||||
//! también para *server1.toml* o *server2.toml*. Sólo uno será cargado.
|
//! such as API keys, URLs, or performance-related adjustments.
|
||||||
//!
|
//!
|
||||||
//! * Normalmente estos archivos suelen ser idóneos para incluir contraseñas o configuración
|
//! 3. **config/local.{rm}.toml**, useful for local machine-specific configurations:
|
||||||
//! 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.
|
//! * This file allows you to add or override settings specific to the environment. For example,
|
||||||
|
//! `local.devel.toml` for development or `local.production.toml` for production tweaks.
|
||||||
|
//!
|
||||||
|
//! * It enables developers to tailor settings for their machines within a given environment and
|
||||||
|
//! is typically not shared or committed to version control systems.
|
||||||
|
//!
|
||||||
|
//! 4. **config/local.toml**, for general local settings across all environments, ideal for quick
|
||||||
|
//! adjustments or temporary values not tied to any specific environment.
|
||||||
|
//!
|
||||||
|
//! The configuration settings are merged in the order listed above, with later files overriding
|
||||||
|
//! earlier ones if there are conflicts.
|
||||||
//!
|
//!
|
||||||
//!
|
//!
|
||||||
//! # Cómo añadir ajustes de configuración
|
//! # Adding configuration settings
|
||||||
//!
|
//!
|
||||||
//! Para proporcionar a tu **módulo** sus propios ajustes de configuración, añade
|
//! To give your **module** its own configuration settings, add [*serde*](https://docs.rs/serde) as
|
||||||
//! [*serde*](https://docs.rs/serde) en las dependencias de tu archivo *Cargo.toml* habilitando la
|
//! a dependency in your *Cargo.toml* file with the `derive` feature enabled:
|
||||||
//! característica `derive`:
|
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! [dependencies]
|
//! [dependencies]
|
||||||
//! serde = { version = "1.0", features = ["derive"] }
|
//! serde = { version = "1.0", features = ["derive"] }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Y luego inicializa con la macro [`config_defaults!`](crate::config_defaults) tus ajustes, usando
|
//! Then, use the [`include_config!`](crate::include_config) macro to initialize your settings with
|
||||||
//! tipos seguros y asignando los valores predefinidos para la estructura asociada:
|
//! type-safe structures and predefined values:
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use pagetop::prelude::*;
|
//! use pagetop::prelude::*;
|
||||||
//! use serde::Deserialize;
|
//! use serde::Deserialize;
|
||||||
//!
|
//!
|
||||||
|
//! include_config!(SETTINGS: Settings => [
|
||||||
|
//! // [myapp]
|
||||||
|
//! "myapp.name" => "Value Name",
|
||||||
|
//! "myapp.width" => 900,
|
||||||
|
//! "myapp.height" => 320,
|
||||||
|
//! ]);
|
||||||
|
//!
|
||||||
//! #[derive(Debug, Deserialize)]
|
//! #[derive(Debug, Deserialize)]
|
||||||
//! pub struct Settings {
|
//! pub struct Settings {
|
||||||
//! pub myapp: MyApp,
|
//! pub myapp: MyApp,
|
||||||
|
|
@ -70,31 +80,26 @@
|
||||||
//! pub width: u16,
|
//! pub width: u16,
|
||||||
//! pub height: 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`]).
|
//! This is how global configuration settings are declared (see [`SETTINGS`](crate::global::SETTINGS)).
|
||||||
//!
|
//!
|
||||||
//! Puedes usar la [sintaxis TOML](https://toml.io/en/v1.0.0#table) para añadir tu nueva sección
|
//! You can add a new `[myapp]` section in the configuration files using the
|
||||||
//! `[myapp]` en los archivos de configuración, del mismo modo que se añaden `[log]` o `[server]` en
|
//! [TOML syntax](https://toml.io/en/v1.0.0#table), just like the `[log]` or `[server]` sections in
|
||||||
//! los ajustes globales (ver [`Settings`]).
|
//! the global settings (see [`Settings`](crate::global::Settings)).
|
||||||
//!
|
//!
|
||||||
//! Se recomienda inicializar todos los ajustes con valores predefinidos, o utilizar la notación
|
//! It is recommended to initialize all settings with predefined values or use `Option<T>` for
|
||||||
//! `Option<T>` si van a ser tratados en el código como opcionales.
|
//! optional settings handled within the code.
|
||||||
//!
|
//!
|
||||||
//! Si no pueden inicializarse correctamente los ajustes de configuración, entonces la aplicación
|
//! If configuration settings fail to initialize correctly, the application will panic and stop
|
||||||
//! ejecutará un panic! y detendrá la ejecución.
|
//! execution.
|
||||||
//!
|
//!
|
||||||
//! Los ajustes de configuración siempre serán de sólo lectura.
|
//! Configuration settings are always read-only.
|
||||||
//!
|
//!
|
||||||
//!
|
//!
|
||||||
//! # Cómo usar tus nuevos ajustes de configuración
|
//! # Using your new configuration settings
|
||||||
|
//!
|
||||||
|
//! Access the settings directly in your code:
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use pagetop::prelude::*;
|
//! use pagetop::prelude::*;
|
||||||
|
|
@ -127,64 +132,58 @@ use crate::config::file::File;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
/// Directorio donde se encuentran los archivos de configuración.
|
/// Original values read from configuration files in `key = value` pairs.
|
||||||
const CONFIG_DIR: &str = "config";
|
pub static CONFIG_VALUES: 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());
|
||||||
|
|
||||||
/// Valores originales de la configuración en forma de pares `clave = valor` recogidos de los
|
// Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'.
|
||||||
/// archivos de configuración.
|
let rm = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
|
||||||
|
|
||||||
#[rustfmt::skip]
|
// Initialize config values.
|
||||||
pub static CONFIG_DATA: LazyLock<ConfigData> = LazyLock::new(|| {
|
let mut values = ConfigData::default();
|
||||||
// Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Por defecto 'default'.
|
|
||||||
let run_mode = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
|
|
||||||
|
|
||||||
// Inicializa los ajustes.
|
// Merge (optional) configuration files and set the execution mode.
|
||||||
let mut settings = ConfigData::default();
|
values
|
||||||
|
// 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 common 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");
|
||||||
|
|
||||||
// Combina los archivos (opcionales) de configuración y asigna el modo de ejecución.
|
values
|
||||||
settings
|
|
||||||
// Primero añade la configuración común a todos los entornos. Por defecto 'common.toml'.
|
|
||||||
.merge(
|
|
||||||
File::with_name(&concat_string!(CONFIG_DIR, "/common.toml"))
|
|
||||||
.required(false)
|
|
||||||
).unwrap()
|
|
||||||
// Añade la configuración específica del entorno. Por defecto 'default.toml'.
|
|
||||||
.merge(
|
|
||||||
File::with_name(&concat_string!(CONFIG_DIR, "/", run_mode, ".toml"))
|
|
||||||
.required(false)
|
|
||||||
).unwrap()
|
|
||||||
// Añade la configuración local reservada del entorno. Por defecto 'local.default.toml'.
|
|
||||||
.merge(
|
|
||||||
File::with_name(&concat_string!(CONFIG_DIR, "/local.", run_mode, ".toml"))
|
|
||||||
.required(false),
|
|
||||||
).unwrap()
|
|
||||||
// Añade la configuración local reservada general. Por defecto 'local.toml'.
|
|
||||||
.merge(
|
|
||||||
File::with_name(&concat_string!(CONFIG_DIR, "/local.toml"))
|
|
||||||
.required(false)
|
|
||||||
).unwrap()
|
|
||||||
// Salvaguarda el modo de ejecución.
|
|
||||||
.set("app.run_mode", run_mode)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
settings
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
/// Define un conjunto de ajustes de configuración usando tipos seguros y valores predefinidos.
|
macro_rules! include_config {
|
||||||
///
|
|
||||||
/// 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),* $(,)? ] ) => {
|
( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => {
|
||||||
#[doc = concat!(
|
#[doc = concat!(
|
||||||
"Assigned or predefined values for configuration settings associated to the ",
|
"Assigned or predefined values for configuration settings associated to the ",
|
||||||
"[`", stringify!($Settings), "`] type."
|
"[`", stringify!($Settings), "`] type."
|
||||||
)]
|
)]
|
||||||
pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| {
|
pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| {
|
||||||
let mut settings = $crate::config::CONFIG_DATA.clone();
|
let mut settings = $crate::config::CONFIG_VALUES.clone();
|
||||||
$(
|
$(
|
||||||
settings.set_default($key, $value).unwrap();
|
settings.set_default($key, $value).unwrap();
|
||||||
)*
|
)*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Key types and functions for creating actions, components, packages, and themes.
|
//! Key types and functions for creating actions, components, packages, and themes.
|
||||||
|
|
||||||
use crate::global::TypeInfo;
|
use crate::util::TypeInfo;
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ use crate::concat_string;
|
||||||
use crate::core::component::AnyOp;
|
use crate::core::component::AnyOp;
|
||||||
use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME};
|
use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME};
|
||||||
use crate::core::theme::{ComponentsInRegions, ThemeRef};
|
use crate::core::theme::{ComponentsInRegions, ThemeRef};
|
||||||
use crate::global::TypeInfo;
|
|
||||||
use crate::html::{html, Markup};
|
use crate::html::{html, Markup};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
|
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID};
|
||||||
use crate::service::HttpRequest;
|
use crate::service::HttpRequest;
|
||||||
|
use crate::util::TypeInfo;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::core::component::Context;
|
use crate::core::component::Context;
|
||||||
use crate::core::AnyBase;
|
use crate::core::AnyBase;
|
||||||
use crate::global::TypeInfo;
|
|
||||||
use crate::html::{html, Markup, PrepareMarkup};
|
use crate::html::{html, Markup, PrepareMarkup};
|
||||||
|
use crate::util::TypeInfo;
|
||||||
|
|
||||||
pub trait ComponentBase {
|
pub trait ComponentBase {
|
||||||
fn render(&mut self, cx: &mut Context) -> Markup;
|
fn render(&mut self, cx: &mut Context) -> Markup;
|
||||||
|
|
|
||||||
343
src/global.rs
343
src/global.rs
|
|
@ -1,115 +1,16 @@
|
||||||
//! Global settings, functions and macro helpers.
|
//! Global settings.
|
||||||
|
|
||||||
use crate::{config_defaults, trace};
|
use crate::include_config;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use std::io;
|
include_config!(SETTINGS: Settings => [
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
// *************************************************************************************************
|
|
||||||
// SETTINGS.
|
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Configuration settings for global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and
|
|
||||||
/// [`[server]`](Server) sections (see [`SETTINGS`]).
|
|
||||||
pub struct Settings {
|
|
||||||
pub app: App,
|
|
||||||
pub dev: Dev,
|
|
||||||
pub log: Log,
|
|
||||||
pub server: Server,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Section `[app]` of the configuration settings.
|
|
||||||
///
|
|
||||||
/// See [`Settings`].
|
|
||||||
pub struct App {
|
|
||||||
/// El nombre de la aplicación.
|
|
||||||
/// Por defecto: *"My App"*.
|
|
||||||
pub name: String,
|
|
||||||
/// Una descripción breve de la aplicación.
|
|
||||||
/// Por defecto: *"Developed with the amazing PageTop framework."*.
|
|
||||||
pub description: String,
|
|
||||||
/// Tema predeterminado.
|
|
||||||
/// Por defecto: *"Default"*.
|
|
||||||
pub theme: String,
|
|
||||||
/// Idioma (localización) predeterminado.
|
|
||||||
/// Por defecto: *"en-US"*.
|
|
||||||
pub language: String,
|
|
||||||
/// Dirección predeterminada para el texto: *"ltr"* (de izquierda a derecha), *"rtl"* (de
|
|
||||||
/// derecha a izquierda) o *"auto"*.
|
|
||||||
/// Por defecto: *"ltr"*.
|
|
||||||
pub direction: String,
|
|
||||||
/// Rótulo de texto ASCII al arrancar: *"Off"*, *"Slant"*, *"Small"*, *"Speed"* o *"Starwars"*.
|
|
||||||
/// Por defecto: *"Slant"*.
|
|
||||||
pub startup_banner: String,
|
|
||||||
/// Por defecto: según variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no lo está.
|
|
||||||
pub run_mode: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Section `[dev]` of the configuration settings.
|
|
||||||
///
|
|
||||||
/// See [`Settings`].
|
|
||||||
pub struct Dev {
|
|
||||||
/// Los archivos estáticos requeridos por la aplicación se integran de manera predeterminada en
|
|
||||||
/// el binario ejecutable. Sin embargo, durante el desarrollo puede resultar útil servir estos
|
|
||||||
/// archivos desde su propio directorio para evitar recompilar cada vez que se modifican. En
|
|
||||||
/// este caso bastaría con indicar la ruta completa al directorio raíz del proyecto.
|
|
||||||
/// Por defecto: *""*.
|
|
||||||
pub pagetop_project_dir: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Section `[log]` of the configuration settings.
|
|
||||||
///
|
|
||||||
/// See [`Settings`].
|
|
||||||
pub struct Log {
|
|
||||||
/// Filtro, o combinación de filtros separados por coma, para la traza de ejecución: *"Error"*,
|
|
||||||
/// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*.
|
|
||||||
/// Por ejemplo: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
|
||||||
/// Por defecto: *"Info"*.
|
|
||||||
pub tracing: String,
|
|
||||||
/// Muestra la traza en el terminal (*"Stdout"*) o queda registrada en archivos con rotación
|
|
||||||
/// *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*.
|
|
||||||
/// Por defecto: *"Stdout"*.
|
|
||||||
pub rolling: String,
|
|
||||||
/// Directorio para los archivos de traza (si `rolling` != *"Stdout"*).
|
|
||||||
/// Por defecto: *"log"*.
|
|
||||||
pub path: String,
|
|
||||||
/// Prefijo para los archivos de traza (si `rolling` != *"Stdout"*).
|
|
||||||
/// Por defecto: *"tracing.log"*.
|
|
||||||
pub prefix: String,
|
|
||||||
/// Presentación de las trazas. Puede ser *"Full"*, *"Compact"*, *"Pretty"* o *"Json"*.
|
|
||||||
/// Por defecto: *"Full"*.
|
|
||||||
pub format: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Section `[server]` of the configuration settings.
|
|
||||||
///
|
|
||||||
/// See [`Settings`].
|
|
||||||
pub struct Server {
|
|
||||||
/// Dirección del servidor web.
|
|
||||||
/// Por defecto: *"localhost"*.
|
|
||||||
pub bind_address: String,
|
|
||||||
/// Puerto del servidor web.
|
|
||||||
/// Por defecto: *8088*.
|
|
||||||
pub bind_port: u16,
|
|
||||||
/// Duración en segundos para la sesión (0 indica "hasta que se cierre el navegador").
|
|
||||||
/// Por defecto: *604800* (7 días).
|
|
||||||
pub session_lifetime: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
config_defaults!(SETTINGS: Settings => [
|
|
||||||
// [app]
|
// [app]
|
||||||
"app.name" => "My App",
|
"app.name" => "My App",
|
||||||
"app.description" => "Developed with the amazing PageTop framework.",
|
"app.description" => "Developed with the amazing PageTop framework.",
|
||||||
"app.theme" => "Default",
|
"app.theme" => "",
|
||||||
"app.language" => "en-US",
|
"app.language" => "en-US",
|
||||||
"app.direction" => "ltr",
|
"app.text_direction" => "ltr",
|
||||||
"app.startup_banner" => "Slant",
|
"app.startup_banner" => "Slant",
|
||||||
|
|
||||||
// [dev]
|
// [dev]
|
||||||
|
|
@ -128,161 +29,93 @@ config_defaults!(SETTINGS: Settings => [
|
||||||
"server.session_lifetime" => 604_800,
|
"server.session_lifetime" => 604_800,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// *************************************************************************************************
|
#[derive(Debug, Deserialize)]
|
||||||
// FUNCTIONS HELPERS.
|
/// Configuration settings for the global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and
|
||||||
// *************************************************************************************************
|
/// [`[server]`](Server) sections (see [`SETTINGS`]).
|
||||||
|
pub struct Settings {
|
||||||
pub enum TypeInfo {
|
pub app: App,
|
||||||
FullName,
|
pub dev: Dev,
|
||||||
ShortName,
|
pub log: Log,
|
||||||
NameFrom(isize),
|
pub server: Server,
|
||||||
NameTo(isize),
|
|
||||||
PartialName(isize, isize),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeInfo {
|
#[derive(Debug, Deserialize)]
|
||||||
pub fn of<T: ?Sized>(&self) -> &'static str {
|
/// Section `[app]` of the configuration settings.
|
||||||
let type_name = std::any::type_name::<T>();
|
|
||||||
match self {
|
|
||||||
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
|
/// See [`Settings`].
|
||||||
///
|
pub struct App {
|
||||||
/// * `root_path` - A string slice that holds the root path.
|
/// The name of the application.
|
||||||
/// * `relative_path` - A string slice that holds the relative path.
|
/// Default: *"My App"*.
|
||||||
///
|
pub name: String,
|
||||||
/// # Returns
|
/// A brief description of the application.
|
||||||
///
|
/// Default: *"Developed with the amazing PageTop framework."*.
|
||||||
/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`.
|
pub description: String,
|
||||||
/// * `Err` - If an I/O error occurs, returns an `io::Error`.
|
/// Default theme.
|
||||||
///
|
/// Default: *""*.
|
||||||
/// # Errors
|
pub theme: String,
|
||||||
///
|
/// Default language (localization).
|
||||||
/// This function will return an error if:
|
/// Default: *"en-US"*.
|
||||||
/// - The root path or relative path are invalid.
|
pub language: String,
|
||||||
/// - There is an issue with file system operations, such as reading the directory.
|
/// Default text direction: *"ltr"* (left-to-right), *"rtl"* (right-to-left), or *"auto"*.
|
||||||
///
|
/// Default: *"ltr"*.
|
||||||
/// # Examples
|
pub text_direction: String,
|
||||||
///
|
/// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*.
|
||||||
/// ```
|
/// Default: *"Slant"*.
|
||||||
/// let root = "/home/user";
|
pub startup_banner: String,
|
||||||
/// let relative = "documents";
|
/// Default: according to the `PAGETOP_RUN_MODE` environment variable, or *"default"* if unset.
|
||||||
/// let abs_dir = absolute_dir(root, relative).unwrap();
|
pub run_mode: String,
|
||||||
/// 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() {
|
#[derive(Debug, Deserialize)]
|
||||||
let message = format!("Path \"{absolute_dir}\" does not exist");
|
/// Section `[dev]` of the configuration settings.
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// *************************************************************************************************
|
|
||||||
// MACRO HELPERS.
|
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
/// Macro para construir grupos de pares clave-valor.
|
|
||||||
///
|
///
|
||||||
/// ```rust#ignore
|
/// See [`Settings`].
|
||||||
/// let args = kv![
|
pub struct Dev {
|
||||||
/// "userName" => "Roberto",
|
/// Static files required by the application are integrated by default into the executable
|
||||||
/// "photoCount" => 3,
|
/// binary. However, during development, it can be useful to serve these files from their own
|
||||||
/// "userGender" => "male",
|
/// directory to avoid recompilation every time they are modified. In this case, specify the
|
||||||
/// ];
|
/// full path to the project's root directory.
|
||||||
/// ```
|
/// Default: *""*.
|
||||||
macro_rules! kv {
|
pub pagetop_project_dir: String,
|
||||||
( $($key:expr => $value:expr),* $(,)? ) => {{
|
}
|
||||||
let mut a = std::collections::HashMap::new();
|
|
||||||
$(
|
#[derive(Debug, Deserialize)]
|
||||||
a.insert($key.into(), $value.into());
|
/// Section `[log]` of the configuration settings.
|
||||||
)*
|
///
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
src/lib.rs
26
src/lib.rs
|
|
@ -72,9 +72,7 @@
|
||||||
|
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
// *************************************************************************************************
|
// RE-EXPORTED *************************************************************************************
|
||||||
// RE-EXPORTED MACROS AND DERIVES.
|
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
pub use concat_string::concat_string;
|
pub use concat_string::concat_string;
|
||||||
|
|
||||||
|
|
@ -95,17 +93,16 @@ pub use std::any::TypeId;
|
||||||
|
|
||||||
pub type Weight = i8;
|
pub type Weight = i8;
|
||||||
|
|
||||||
// Global settings, functions and macro helpers.
|
|
||||||
pub mod global;
|
|
||||||
|
|
||||||
include_locales!(LOCALES_PAGETOP);
|
include_locales!(LOCALES_PAGETOP);
|
||||||
|
|
||||||
// *************************************************************************************************
|
// API *********************************************************************************************
|
||||||
// PUBLIC API.
|
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
// Retrieve and apply settings values from configuration files.
|
// Useful functions and macros.
|
||||||
|
pub mod util;
|
||||||
|
// Load configuration settings.
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
// Global settings.
|
||||||
|
pub mod global;
|
||||||
// Application tracing and event logging.
|
// Application tracing and event logging.
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
// HTML in code.
|
// HTML in code.
|
||||||
|
|
@ -114,24 +111,17 @@ pub mod html;
|
||||||
pub mod locale;
|
pub mod locale;
|
||||||
// Date and time handling.
|
// Date and time handling.
|
||||||
pub mod datetime;
|
pub mod datetime;
|
||||||
|
|
||||||
// Essential web framework.
|
// Essential web framework.
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
||||||
// Key types and functions for creating actions, components, packages, and themes.
|
// Key types and functions for creating actions, components, packages, and themes.
|
||||||
pub mod core;
|
pub mod core;
|
||||||
|
|
||||||
// Web request response variants.
|
// Web request response variants.
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
|
||||||
// Base actions, components, packages, and themes.
|
// Base actions, components, packages, and themes.
|
||||||
pub mod base;
|
pub mod base;
|
||||||
|
|
||||||
// Prepare and run the application.
|
// Prepare and run the application.
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
// *************************************************************************************************
|
// The PageTop Prelude *****************************************************************************
|
||||||
// The PageTop Prelude.
|
|
||||||
// *************************************************************************************************
|
|
||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ pub use crate::{concat_string, fn_builder, main, paste, test};
|
||||||
pub use crate::{AutoDefault, ComponentClasses};
|
pub use crate::{AutoDefault, ComponentClasses};
|
||||||
|
|
||||||
// GLOBAL.
|
// GLOBAL.
|
||||||
pub use crate::{global, HashMapResources, TypeId, Weight};
|
pub use crate::{HashMapResources, TypeId, Weight};
|
||||||
|
|
||||||
// MACROS.
|
// MACROS.
|
||||||
|
|
||||||
// crate::global
|
// crate::global
|
||||||
pub use crate::kv;
|
pub use crate::kv;
|
||||||
// crate::config
|
// crate::config
|
||||||
pub use crate::config_defaults;
|
pub use crate::include_config;
|
||||||
// crate::html
|
// crate::html
|
||||||
pub use crate::html;
|
pub use crate::html;
|
||||||
// crate::locale
|
// crate::locale
|
||||||
|
|
@ -24,6 +24,10 @@ pub use crate::actions;
|
||||||
|
|
||||||
// API.
|
// API.
|
||||||
|
|
||||||
|
pub use crate::util;
|
||||||
|
|
||||||
|
pub use crate::global;
|
||||||
|
|
||||||
pub use crate::trace;
|
pub use crate::trace;
|
||||||
|
|
||||||
pub use crate::html::*;
|
pub use crate::html::*;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ macro_rules! static_files_service {
|
||||||
let mut serve_embedded:bool = true;
|
let mut serve_embedded:bool = true;
|
||||||
$(
|
$(
|
||||||
if !$root.is_empty() && !$relative.is_empty() {
|
if !$root.is_empty() && !$relative.is_empty() {
|
||||||
if let Ok(absolute) = $crate::global::absolute_dir($root, $relative) {
|
if let Ok(absolute) = $crate::util::absolute_dir($root, $relative) {
|
||||||
$scfg.service($crate::service::ActixFiles::new(
|
$scfg.service($crate::service::ActixFiles::new(
|
||||||
$path,
|
$path,
|
||||||
absolute,
|
absolute,
|
||||||
|
|
|
||||||
161
src/util.rs
Normal file
161
src/util.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
//! Useful functions and macros.
|
||||||
|
|
||||||
|
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
|
||||||
|
}};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue