From 6b66289567f597f8aaefff40ca8161a53d3cd65d Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 16 Jan 2020 22:05:35 +0300 Subject: [PATCH] refac: change structure --- itconfig/Cargo.toml | 9 +- itconfig/src/envstr.rs | 92 +++ itconfig/src/getenv.rs | 141 ++++ itconfig/src/lib.rs | 879 +-------------------- itconfig/src/macro.rs | 706 +++++++++++++++++ itconfig_tests/tests/config_macro.rs | 4 +- itconfig_tests/tests/get_env.rs | 23 + itconfig_tests/tests/get_env_or_default.rs | 31 + 8 files changed, 1022 insertions(+), 863 deletions(-) create mode 100644 itconfig/src/envstr.rs create mode 100644 itconfig/src/getenv.rs create mode 100644 itconfig/src/macro.rs create mode 100644 itconfig_tests/tests/get_env.rs create mode 100644 itconfig_tests/tests/get_env_or_default.rs diff --git a/itconfig/Cargo.toml b/itconfig/Cargo.toml index 9c424ab..bb41512 100644 --- a/itconfig/Cargo.toml +++ b/itconfig/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "itconfig" -version = "0.8.0" +version = "0.9.0" authors = ["Dmitriy Pleshevskiy "] description = "Easy build a configs from environment variables and use it in globally." categories = ["config", "web-programming"] -keywords = ["config", "env", "macro", "configuration", "environment"] +keywords = ["config", "env", "configuration", "environment", "macro"] edition = "2018" license = "MIT" repository = "https://github.com/icetemple/itconfig-rs" @@ -19,3 +19,8 @@ travis-ci = { repository = "icetemple/itconfig-rs" } maintenance = { status = "actively-developed" } [dependencies] + +[features] +default = ['macro'] +macro = [] + diff --git a/itconfig/src/envstr.rs b/itconfig/src/envstr.rs new file mode 100644 index 0000000..c79c773 --- /dev/null +++ b/itconfig/src/envstr.rs @@ -0,0 +1,92 @@ +use std::ops::Deref; +use std::str::FromStr; + + +#[doc(hidden)] +pub trait ToEnvString { + fn to_env_string(&self) -> EnvString; +} + + +#[doc(hidden)] +pub trait FromEnvString: Sized { + type Err; + + fn from_env_string(s: &EnvString) -> Result; +} + + +impl ToEnvString for T + where + T: ToString +{ + #[inline] + fn to_env_string(&self) -> EnvString { + EnvString(self.to_string()) + } +} + + +#[doc(hidden)] +macro_rules! from_env_string_numbers_impl { + ($($ty:ty),+) => { + $( + impl FromEnvString for $ty { + type Err = <$ty as FromStr>::Err; + + #[inline] + fn from_env_string(s: &EnvString) -> Result { + s.0.parse::() + } + } + )+ + }; +} + +from_env_string_numbers_impl![ + i8, i16, i32, i64, i128, isize, + u8, u16, u32, u64, u128, usize, + f32, f64 +]; + + +impl FromEnvString for bool { + type Err = (); + + fn from_env_string(s: &EnvString) -> Result { + match s.to_lowercase().as_str() { + "true" | "t" | "yes" | "y" | "on" | "1" => Ok(true), + _ => Ok(false) + } + } +} + + +impl FromEnvString for String { + type Err = (); + + fn from_env_string(s: &EnvString) -> Result { + Ok(s.0.clone()) + } +} + + +#[doc(hidden)] +#[derive(Debug, PartialEq, Clone)] +pub struct EnvString(String); + +impl EnvString { + pub fn parse(&self) -> Result { + FromEnvString::from_env_string(self) + } +} + +impl Deref for EnvString { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + + diff --git a/itconfig/src/getenv.rs b/itconfig/src/getenv.rs new file mode 100644 index 0000000..cb70ea8 --- /dev/null +++ b/itconfig/src/getenv.rs @@ -0,0 +1,141 @@ +use std::env; +use crate::envstr::{EnvString, FromEnvString, ToEnvString}; + + +#[doc(hidden)] +macro_rules! env_panic { + (MissingVariable, $env_name:expr) => { + panic!(r#"Environment variable "{}" is missing"#, $env_name) + }; + (FailedToParse, $env_name:expr) => { + panic!(r#"Failed to parse environment variable "{}""#, $env_name) + }; +} + + +/// Try to read environment variable and parse to expected type. You may to put to argument +/// any type with `FromEnvString` trait. +/// +/// Panics +/// ------ +/// Application will panic if environment variable is missing or cannot parse variable to +/// expected type +/// +/// Example +/// ------- +/// +/// ```rust +/// # extern crate itconfig; +/// # use itconfig::get_env; +/// use std::env; +/// +/// fn main () { +/// env::set_var("DEBUG", "true"); +/// +/// let result: bool = get_env("DEBUG"); +/// +/// assert_eq!(result, true); +/// } +/// ``` +/// +pub fn get_env(env_name: &str) -> T + where + T: FromEnvString +{ + get_env_or(env_name, || env_panic!(MissingVariable, env_name)) +} + + +/// This function is similar as `get_env` but more safely. You can pass default value for +/// environment variable with `ToEnvString` trait. +/// +/// Panics +/// ------ +/// Application will panic if cannot parse variable to expected type +/// +/// Example +/// ------- +/// +/// ```rust +/// # extern crate itconfig; +/// # use itconfig::get_env_or_default; +/// use std::env; +/// +/// fn main () { +/// let result: bool = get_env_or_default("TESTING", "true"); +/// assert_eq!(result, true); +/// } +/// ``` +/// +pub fn get_env_or_default(env_name: &str, default: D) -> T + where + T: FromEnvString, + D: ToEnvString, +{ + get_env_or(env_name, || default.to_env_string()) +} + + +/// This function is similar as `get_env_or_default` but if env variable is missed, will set +/// default value to environment variable. +/// +/// Panics +/// ------ +/// Application will panic if cannot parse variable to expected type +/// +/// Example +/// ------- +/// +/// ```rust +/// # extern crate itconfig; +/// # use itconfig::get_env_or_set_default; +/// use std::env; +/// +/// fn main () { +/// let result: bool = get_env_or_set_default("TESTING", "true"); +/// assert_eq!(result, true); +/// +/// let var = env::var("TESTING").unwrap(); +/// assert_eq!(var, "true"); +/// } +/// ``` +/// +pub fn get_env_or_set_default(env_name: &str, default: D) -> T + where + T: FromEnvString, + D: ToEnvString, +{ + get_env_or(env_name, || { + let val = default.to_env_string(); + env::set_var(env_name, val.as_str()); + val + }) +} + + +/// This function returns env variable as `EnvString` structure. You can pass callback for custom +/// default expression. Callback should return `EnvString` value or `panic!` +pub fn get_env_or(env_name: &str, cb: F) -> T + where + T: FromEnvString, + F: FnOnce() -> EnvString +{ + let env_str = env::var(env_name) + .map(|s| s.to_env_string()) + .unwrap_or_else(|_| cb()); + + parse_env_variable(env_name, env_str) +} + + + +#[doc(hidden)] +fn parse_env_variable(env_name: &str, env_str: EnvString) -> T + where + T: FromEnvString +{ + env_str + .parse::() + .unwrap_or_else(|_| env_panic!(FailedToParse, env_name)) +} + diff --git a/itconfig/src/lib.rs b/itconfig/src/lib.rs index 0bb0216..3d30ca2 100644 --- a/itconfig/src/lib.rs +++ b/itconfig/src/lib.rs @@ -1,861 +1,22 @@ -//! # itconfig -//! -//! Simple configuration with macro for rust application. -//! -//! -//! ## Motivation -//! -//! I began to use rust with web programming experience where environment variables are widely used -//! and often there are more then 50 of them. First I looked at already created libraries. -//! But there it's necessary to initialise structure that needs to be moved to each function -//! where you need variable. It uses little bit memory, but configuration lifetime is as long -//! as application lifetime. Because of it I decided to create my own library. -//! -//! -//! ## Example usage -//! -//! ```rust -//! #[macro_use] extern crate itconfig; -//! use std::env; -//! // use dotenv::dotenv; -//! -//! config! { -//! DEBUG: bool => true, -//! HOST: String => "127.0.0.1", -//! -//! DATABASE_URL < ( -//! "postgres://", -//! POSTGRES_USERNAME => "user", -//! ":", -//! POSTGRES_PASSWORD => "pass", -//! "@", -//! POSTGRES_HOST => "localhost:5432", -//! "/", -//! POSTGRES_DB => "test", -//! ), -//! -//! APP { -//! ARTICLE { -//! PER_PAGE: u32 => 15, -//! } -//! -//! #[cfg(feature = "companies")] -//! COMPANY { -//! #[env_name = "INSTITUTIONS_PER_PAGE"] -//! PER_PAGE: u32 => 15, -//! } -//! } -//! -//! FEATURE { -//! NEW_MENU: bool => false, -//! -//! COMPANY { -//! PROFILE: bool => false, -//! } -//! } -//! } -//! -//! fn main () { -//! // dotenv().ok(); -//! env::set_var("FEATURE_NEW_MENU", "t"); -//! -//! cfg::init(); -//! assert_eq!(cfg::HOST(), String::from("127.0.0.1")); -//! assert_eq!(cfg::DATABASE_URL(), String::from("postgres://user:pass@localhost:5432/test")); -//! assert_eq!(cfg::APP::ARTICLE::PER_PAGE(), 15); -//! assert_eq!(cfg::FEATURE::NEW_MENU(), true); -//! } -//! ``` +// Rustc lints. +#![deny(unused_imports)] +///////////////////////////////////////////////////////////////////////////// + +mod getenv; +pub mod envstr; + +pub use self::getenv::*; + +pub mod prelude { + pub use crate::envstr::*; +} + + +#[cfg(feature = "macro")] +#[allow(unused_imports)] +#[macro_use] +mod r#macro; +#[cfg(feature = "macro")] #[doc(hidden)] -macro_rules! __impl_from_for_numbers { - ($($ty:ty),+) => { - $( - impl From for $ty { - fn from(env: EnvValue) -> Self { - env.0.parse::().unwrap() - } - } - )* - } -} - - -#[derive(Debug)] -#[doc(hidden)] -pub struct EnvValue(String); - -__impl_from_for_numbers![ - i8, i16, i32, i64, i128, isize, - u8, u16, u32, u64, u128, usize, - f32, f64 -]; - -impl From for bool { - fn from(env: EnvValue) -> Self { - match env.0.to_lowercase().as_str() { - "true" | "1" | "t" | "on" => true, - _ => false, - } - } -} - -impl From for EnvValue { - fn from(val: String) -> Self { - Self(val) - } -} - -impl From for String { - fn from(env: EnvValue) -> Self { - env.0 - } -} - - -/// Creates new public mod with function fo get each environment variable of mapping. -/// -/// All variables are required and program will panic if some variables haven't value, but you -/// can add default value for specific variable. -/// -/// Starts with v0.6.0 if you don't have an optional variable, the variable is set automatically. -/// -/// Example usage -/// ------------- -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// # use std::env; -/// # env::set_var("DATABASE_URL", "postgres://u:p@localhost:5432/db"); -/// config! { -/// DATABASE_URL: String, -/// } -/// # cfg::init() -/// ``` -/// -/// Config with default value -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// # use std::env; -/// # env::set_var("DATABASE_URL", "postgres://u:p@localhost:5432/db"); -/// config! { -/// DATABASE_URL: String, -/// HOST: String => "127.0.0.1", -/// } -/// # cfg::init() -/// ``` -/// -/// By default itconfig lib creates module with 'cfg' name. But you can use simple meta instruction -/// if you want to rename module. In the example below we renamed module to 'configuration' -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// # use std::env; -/// env::set_var("DEBUG", "t"); -/// -/// config! { -/// #![mod_name = configuration] -/// -/// DEBUG: bool, -/// } -/// -/// configuration::init(); -/// assert_eq!(configuration::DEBUG(), true); -/// ``` -/// -/// Namespaces -/// ---------- -/// -/// You can use namespaces for env variables -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// # use std::env; -/// env::set_var("DEBUG", "t"); -/// env::set_var("DATABASE_USERNAME", "user"); -/// env::set_var("DATABASE_PASSWORD", "pass"); -/// env::set_var("DATABASE_HOST", "localhost"); -/// -/// config! { -/// DEBUG: bool, -/// DATABASE { -/// USERNAME: String, -/// PASSWORD: String, -/// HOST: String, -/// } -/// } -/// # cfg::init() -/// ``` -/// -/// Now you can use nested structure in namespaces without limits :) -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// config! { -/// FIRST { -/// SECOND { -/// THIRD { -/// FOO: bool => true, -/// } -/// } -/// } -/// } -/// # cfg::init(); -/// ``` -/// -/// Namespaces supports custom meta -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// config! { -/// #[cfg(feature = "first")] -/// FIRST { -/// #[cfg(feature = "second")] -/// SECOND { -/// #[cfg(feature = "third")] -/// THIRD { -/// FOO: bool => true, -/// } -/// } -/// } -/// } -/// # cfg::init(); -/// ``` -/// -/// Meta -/// ---- -/// -/// If you want to read custom env name for variable you can change it manually. -/// -/// **A variable in the nameespace will lose environment prefix** -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// # use std::env; -/// env::set_var("MY_CUSTOM_NAME", "95"); -/// -/// config! { -/// #[env_name = "MY_CUSTOM_NAME"] -/// PER_PAGE: i32, -/// -/// APP { -/// #[env_name = "MY_CUSTOM_NAME"] -/// RECIPES_PER_PAGE: i32, -/// } -/// } -/// -/// cfg::init(); -/// assert_eq!(cfg::PER_PAGE(), 95); -/// assert_eq!(cfg::APP::RECIPES_PER_PAGE(), 95); -/// ``` -/// -/// Also you can add custom meta for each variable. For example feature configurations. -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// config! { -/// #[cfg(feature = "postgres")] -/// DATABASE_URL: String, -/// -/// #[cfg(not(feature = "postgres"))] -/// DATABASE_URL: String, -/// } -/// # fn main() {} -/// ``` -/// -/// Concatenate -/// ----------- -/// -/// Try to concatenate env variable or strings or both to you env variable. It's easy! -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// # use std::env; -/// env::set_var("POSTGRES_USERNAME", "user"); -/// env::set_var("POSTGRES_PASSWORD", "pass"); -/// -/// config! { -/// DATABASE_URL < ( -/// "postgres://", -/// POSTGRES_USERNAME, -/// ":", -/// POSTGRES_PASSWORD, -/// "@", -/// POSTGRES_HOST => "localhost:5432", -/// "/", -/// POSTGRES_DB => "test", -/// ), -/// } -/// -/// cfg::init(); -/// assert_eq!(cfg::DATABASE_URL(), "postgres://user:pass@localhost:5432/test".to_string()) -/// ``` -/// -/// Concatinated variables can be only strings and support all features like namespaces and meta. -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// config! { -/// CONCATED_NAMESPACE { -/// #[env_name = "DATABASE_URL"] -/// CONCAT_ENVVAR < ( -/// "postgres://", -/// NOT_DEFINED_PG_USERNAME => "user", -/// ":", -/// NOT_DEFINED_PG_PASSWORD => "pass", -/// "@", -/// NOT_DEFINED_PG_HOST => "localhost:5432", -/// "/", -/// NOT_DEFINED_PG_DB => "test", -/// ), -/// } -/// } -/// -/// cfg::init(); -/// ``` -/// -/// --- -/// -/// This module will also contain helper method: -/// -------------------------------------------- -/// -/// ```rust -/// pub fn init() {} -/// ``` -/// -/// Run this at the main function for check all required variables without default value. -/// -/// Panics -/// ------ -/// -/// If you miss some required variables your application will panic at startup. -/// -/// Examples -/// -------- -/// -/// ```rust -/// #[macro_use] extern crate itconfig; -/// // use dotenv::dotenv; -/// -/// config! { -/// DEBUG: bool => true, -/// HOST: String => "127.0.0.1", -/// } -/// -/// fn main () { -/// // dotenv().ok(); -/// cfg::init(); -/// assert_eq!(cfg::HOST(), String::from("127.0.0.1")); -/// } -/// ``` -/// -#[macro_export(local_inner_macros)] -macro_rules! config { - ($($tokens:tt)*) => { - __itconfig_parse_module! { - tokens = [$($tokens)*], - name = cfg, - } - } -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_invalid_syntax { - () => { - compile_error!( - "Invalid `config!` syntax. Please see the `config!` macro docs for more info.\ - `https://docs.rs/itconfig/latest/itconfig/macro.config.html`" - ); - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_parse_module { - // Find module name - ( - tokens = [ - #![mod_name = $mod_name:ident] - $($rest:tt)* - ], - name = $ignore:tt, - ) => { - __itconfig_parse_module! { - tokens = [$($rest)*], - name = $mod_name, - } - }; - - // Done parsing module - ( - tokens = $tokens:tt, - name = $name:tt, - ) => { - __itconfig_parse_variables! { - tokens = $tokens, - variables = [], - namespaces = [], - module = { - env_prefix = "", - name = $name, - meta = [], - }, - } - }; - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_invalid_syntax!(); - }; -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_parse_variables { - // Find namespace - ( - tokens = [ - $(#$meta:tt)* - $ns_name:ident { $($ns_tokens:tt)* } - $($rest:tt)* - ], - $($args:tt)* - ) => { - __itconfig_parse_variables! { - tokens = [$($ns_tokens)*], - variables = [], - namespaces = [], - module = { - env_prefix = concat!(stringify!($ns_name), "_"), - name = $ns_name, - meta = [$(#$meta)*], - }, - callback = { - tokens = [$($rest)*], - $($args)* - }, - } - }; - - // Find concatenated variable - ( - tokens = [ - $(#$meta:tt)* - $name:ident < ($($inner:tt)+), - $($rest:tt)* - ], - $($args:tt)* - ) => { - __itconfig_parse_variables! { - current_variable = { - unparsed_meta = [$(#$meta)*], - meta = [], - unparsed_concat = [$($inner)+], - concat = [], - name = $name, - ty = String, - }, - tokens = [$($rest)*], - $($args)* - } - }; - - // Find variable - ( - tokens = [ - $(#$meta:tt)* - $name:ident : $ty:ty$( => $default:expr)?, - $($rest:tt)* - ], - $($args:tt)* - ) => { - __itconfig_parse_variables! { - current_variable = { - unparsed_meta = [$(#$meta)*], - meta = [], - unparsed_concat = [], - concat = [], - name = $name, - ty = $ty, - $(default = $default,)? - }, - tokens = [$($rest)*], - $($args)* - } - }; - - // Find meta with custom env name - ( - current_variable = { - unparsed_meta = [ - #[env_name = $env_name:expr] - $($rest:tt)* - ], - meta = $meta:tt, - unparsed_concat = $unparsed_concat:tt, - concat = $concat:tt, - name = $name:ident, - $($current_variable:tt)* - }, - $($args:tt)* - ) => { - __itconfig_parse_variables! { - current_variable = { - unparsed_meta = [$($rest)*], - meta = $meta, - unparsed_concat = $unparsed_concat, - concat = $concat, - name = $name, - env_name = $env_name, - $($current_variable)* - }, - $($args)* - } - }; - - // Find stranger meta - ( - current_variable = { - unparsed_meta = [ - #$stranger_meta:tt - $($rest:tt)* - ], - meta = [$(#$meta:tt,)*], - $($current_variable:tt)* - }, - $($args:tt)* - ) => { - __itconfig_parse_variables! { - current_variable = { - unparsed_meta = [$($rest)*], - meta = [$(#$meta,)* #$stranger_meta,], - $($current_variable)* - }, - $($args)* - } - }; - - // Parse concat params - ( - current_variable = { - unparsed_meta = $unparsed_meta:tt, - meta = $meta:tt, - unparsed_concat = [ - $concat_param:tt$( => $default:expr)?, - $($rest:tt)* - ], - concat = [$($concat:expr,)*], - $($current_variable:tt)* - }, - $($args:tt)* - ) => { - __itconfig_parse_variables! { - current_variable = { - unparsed_meta = $unparsed_meta, - meta = $meta, - unparsed_concat = [$($rest)*], - concat = [$($concat,)* __itconfig_concat_param!($concat_param$( => $default)?),], - $($current_variable)* - }, - $($args)* - } - }; - - // Done parsing variable - ( - current_variable = { - unparsed_meta = [], - meta = $meta:tt, - unparsed_concat = [], - $($current_variable:tt)* - }, - tokens = $tokens:tt, - variables = [$($variables:tt,)*], - $($args:tt)* - ) => { - __itconfig_parse_variables! { - tokens = $tokens, - variables = [$($variables,)* { meta = $meta, $($current_variable)* },], - $($args)* - } - }; - - // Done parsing all variables of namespace - ( - tokens = [], - variables = $ns_variables:tt, - namespaces = $ns_namespaces:tt, - module = $ns_module:tt, - callback = { - tokens = $tokens:tt, - variables = $variables:tt, - namespaces = [$($namespaces:tt,)*], - $($args:tt)* - }, - ) => { - __itconfig_parse_variables! { - tokens = $tokens, - variables = $variables, - namespaces = [ - $($namespaces,)* - { - variables = $ns_variables, - namespaces = $ns_namespaces, - module = $ns_module, - }, - ], - $($args)* - } - }; - - // Done parsing all variables - ( - tokens = [], - $($args:tt)* - ) => { - __itconfig_impl_namespace!($($args)*); - }; - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_invalid_syntax!(); - }; -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_impl_namespace { - ( - variables = [$({ - meta = $var_meta:tt, - concat = $var_concat:tt, - name = $var_name:ident, - $($variable:tt)* - },)*], - namespaces = [$({ - variables = $ns_variable:tt, - namespaces = $ns_namespaces:tt, - module = { - env_prefix = $ns_env_prefix:expr, - name = $ns_mod_name:ident, - meta = [$(#$ns_meta:tt)*], - }, - },)*], - module = { - env_prefix = $env_prefix:expr, - name = $mod_name:ident, - meta = [$(#$meta:tt)*], - }, - ) => { - $(#$meta)* - pub mod $mod_name { - #![allow(non_snake_case)] - use std::env; - use itconfig::EnvValue; - - $(__itconfig_impl_namespace! { - variables = $ns_variable, - namespaces = $ns_namespaces, - module = { - env_prefix = $ns_env_prefix, - name = $ns_mod_name, - meta = [$(#$ns_meta)*], - }, - })* - - pub fn init() { - $($var_name();)* - $( - $(#$ns_meta)* - $ns_mod_name::init(); - )* - } - - $(__itconfig_variable! { - meta = $var_meta, - concat = $var_concat, - name = $var_name, - env_prefix = $env_prefix, - $($variable)* - })* - } - }; - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_invalid_syntax!(); - }; -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_concat_param { - // Find env parameter with default value - ($env_name:ident => $default:expr) => { - __itconfig_variable_helper!(stringify!($env_name).to_uppercase(), $default, default) - }; - - // Find env parameter without default value - ($env_name:ident) => { - env_or!(stringify!($env_name).to_uppercase()) - }; - - // Find string parameter - ($str:expr) => ( $str.to_string() ); - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_invalid_syntax!(); - }; -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_variable { - // Set default env name - ( - meta = $meta:tt, - concat = $concat:tt, - name = $name:ident, - env_prefix = $env_prefix:expr, - ty = $ty:ty, - $($args:tt)* - ) => { - __itconfig_variable! { - meta = $meta, - concat = $concat, - name = $name, - env_prefix = $env_prefix, - env_name = concat!($env_prefix, stringify!($name)).to_uppercase(), - ty = $ty, - $($args)* - } - }; - - // Add method for concatenated variable - ( - meta = [$(#$meta:tt,)*], - concat = [$($concat:expr,)+], - name = $name:ident, - env_prefix = $env_prefix:expr, - env_name = $env_name:expr, - ty = $ty:ty, - $($args:tt)* - ) => { - $(#$meta)* - pub fn $name() -> $ty { - let value_parts: Vec = vec!($($concat),+); - let value = value_parts.join(""); - __itconfig_variable_helper!(@setenv $env_name, value) - } - }; - - // Add method for env variable - ( - meta = [$(#$meta:tt,)*], - concat = [], - name = $name:ident, - env_prefix = $env_prefix:expr, - env_name = $env_name:expr, - ty = $ty:ty, - $(default = $default:expr,)? - ) => { - $(#$meta)* - pub fn $name() -> $ty { - env_or!($env_name$(, $default)?) - } - }; - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_invalid_syntax!(); - }; -} - -/// This macro returns environment variable by name and converts variable to desired type -/// or returns default value. -/// -/// Panics -/// ------ -/// If you don't pass default value, macro will panic -/// -/// Examples -/// -------- -/// -/// ```rust -/// # #[macro_use] extern crate itconfig; -/// let url: String = env_or!("DATABASE_URL", "127.0.0.1".to_string()); -/// assert_eq!(url, "127.0.0.1".to_string()); -/// ``` -#[macro_export(local_inner_macro)] -macro_rules! env_or { - // Env without default value - ($env_name:expr) => { - __itconfig_variable_helper!($env_name, format!(r#"Cannot read "{}" environment variable"#, $env_name), panic); - }; - - // Env with default value - ($env_name:expr, $default:expr) => { - __itconfig_variable_helper!($env_name, $default, setenv); - }; - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_env_or_invalid_syntax!(); - }; -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_env_or_invalid_syntax { - () => { - compile_error!( - "Invalid `env_or!` syntax. Please see the `env_or!` macro docs for more info.\ - `https://docs.rs/itconfig/latest/itconfig/macro.env_or.html`" - ); - }; -} - - -#[macro_export] -#[doc(hidden)] -macro_rules! __itconfig_variable_helper { - // Get env variable - ($env_name:expr, $default:expr, $token:tt) => {{ - use std::env; - use itconfig::EnvValue; - env::var($env_name) - .map(|val| __itconfig_variable_helper!(val)) - .unwrap_or_else(|_| __itconfig_variable_helper!(@$token $env_name, $default)) - }}; - - // Returns converted env variable - ($(@default $env_name:expr,)? $default:expr) => {{ - EnvValue::from($default.to_string()).into() - }}; - - // Set default value for env variable and returns default - (@setenv $env_name:expr, $default:expr) => {{ - env::set_var($env_name, $default.to_string()); - __itconfig_variable_helper!($default) - }}; - - // Make panic for env variable - (@panic $env_name:expr, $default:expr) => { - panic!($default); - }; - - // Invalid syntax - ($($tokens:tt)*) => { - __itconfig_env_or_invalid_syntax!(); - }; -} - +pub use r#macro::*; \ No newline at end of file diff --git a/itconfig/src/macro.rs b/itconfig/src/macro.rs new file mode 100644 index 0000000..ef426cd --- /dev/null +++ b/itconfig/src/macro.rs @@ -0,0 +1,706 @@ +/// Creates new public mod with function fo get each environment variable of mapping. +/// +/// All variables are required and program will panic if some variables haven't value, but you +/// can add default value for specific variable. +/// +/// Starts with v0.6.0 if you don't have an optional variable, the variable is set automatically. +/// +/// Example usage +/// ------------- +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// # use std::env; +/// # env::set_var("DATABASE_URL", "postgres://u:p@localhost:5432/db"); +/// config! { +/// DATABASE_URL: String, +/// } +/// # cfg::init() +/// ``` +/// +/// Config with default value +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// # use std::env; +/// # env::set_var("DATABASE_URL", "postgres://u:p@localhost:5432/db"); +/// config! { +/// DATABASE_URL: String, +/// HOST: String => "127.0.0.1", +/// } +/// # cfg::init() +/// ``` +/// +/// By default itconfig lib creates module with 'cfg' name. But you can use simple meta instruction +/// if you want to rename module. In the example below we renamed module to 'configuration' +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// # use std::env; +/// env::set_var("DEBUG", "t"); +/// +/// config! { +/// #![mod_name = configuration] +/// +/// DEBUG: bool, +/// } +/// +/// configuration::init(); +/// assert_eq!(configuration::DEBUG(), true); +/// ``` +/// +/// Namespaces +/// ---------- +/// +/// You can use namespaces for env variables +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// # use std::env; +/// env::set_var("DEBUG", "t"); +/// env::set_var("DATABASE_USERNAME", "user"); +/// env::set_var("DATABASE_PASSWORD", "pass"); +/// env::set_var("DATABASE_HOST", "localhost"); +/// +/// config! { +/// DEBUG: bool, +/// DATABASE { +/// USERNAME: String, +/// PASSWORD: String, +/// HOST: String, +/// } +/// } +/// # cfg::init() +/// ``` +/// +/// Now you can use nested structure in namespaces without limits :) +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// config! { +/// FIRST { +/// SECOND { +/// THIRD { +/// FOO: bool => true, +/// } +/// } +/// } +/// } +/// # cfg::init(); +/// ``` +/// +/// Namespaces supports custom meta +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// config! { +/// #[cfg(feature = "first")] +/// FIRST { +/// #[cfg(feature = "second")] +/// SECOND { +/// #[cfg(feature = "third")] +/// THIRD { +/// FOO: bool => true, +/// } +/// } +/// } +/// } +/// # cfg::init(); +/// ``` +/// +/// Meta +/// ---- +/// +/// If you want to read custom env name for variable you can change it manually. +/// +/// **A variable in the nameespace will lose environment prefix** +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// # use std::env; +/// env::set_var("MY_CUSTOM_NAME", "95"); +/// +/// config! { +/// #[env_name = "MY_CUSTOM_NAME"] +/// PER_PAGE: i32, +/// +/// APP { +/// #[env_name = "MY_CUSTOM_NAME"] +/// RECIPES_PER_PAGE: i32, +/// } +/// } +/// +/// cfg::init(); +/// assert_eq!(cfg::PER_PAGE(), 95); +/// assert_eq!(cfg::APP::RECIPES_PER_PAGE(), 95); +/// ``` +/// +/// Also you can add custom meta for each variable. For example feature configurations. +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// config! { +/// #[cfg(feature = "postgres")] +/// DATABASE_URL: String, +/// +/// #[cfg(not(feature = "postgres"))] +/// DATABASE_URL: String, +/// } +/// # fn main() {} +/// ``` +/// +/// Concatenate +/// ----------- +/// +/// Try to concatenate env variable or strings or both to you env variable. It's easy! +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// # use std::env; +/// env::set_var("POSTGRES_USERNAME", "user"); +/// env::set_var("POSTGRES_PASSWORD", "pass"); +/// +/// config! { +/// DATABASE_URL < ( +/// "postgres://", +/// POSTGRES_USERNAME, +/// ":", +/// POSTGRES_PASSWORD, +/// "@", +/// POSTGRES_HOST => "localhost:5432", +/// "/", +/// POSTGRES_DB => "test", +/// ), +/// } +/// +/// cfg::init(); +/// assert_eq!(cfg::DATABASE_URL(), "postgres://user:pass@localhost:5432/test".to_string()) +/// ``` +/// +/// Concatinated variables can be only strings and support all features like namespaces and meta. +/// +/// ```rust +/// # #[macro_use] extern crate itconfig; +/// config! { +/// CONCATED_NAMESPACE { +/// #[env_name = "DATABASE_URL"] +/// CONCAT_ENVVAR < ( +/// "postgres://", +/// NOT_DEFINED_PG_USERNAME => "user", +/// ":", +/// NOT_DEFINED_PG_PASSWORD => "pass", +/// "@", +/// NOT_DEFINED_PG_HOST => "localhost:5432", +/// "/", +/// NOT_DEFINED_PG_DB => "test", +/// ), +/// } +/// } +/// +/// cfg::init(); +/// ``` +/// +/// --- +/// +/// This module will also contain helper method: +/// -------------------------------------------- +/// +/// ```rust +/// pub fn init() {} +/// ``` +/// +/// Run this at the main function for check all required variables without default value. +/// +/// Panics +/// ------ +/// +/// If you miss some required variables your application will panic at startup. +/// +/// Examples +/// -------- +/// +/// ```rust +/// #[macro_use] extern crate itconfig; +/// // use dotenv::dotenv; +/// +/// config! { +/// DEBUG: bool => true, +/// HOST: String => "127.0.0.1", +/// } +/// +/// fn main () { +/// // dotenv().ok(); +/// cfg::init(); +/// assert_eq!(cfg::HOST(), String::from("127.0.0.1")); +/// } +/// ``` +/// +#[macro_export(local_inner_macros)] +macro_rules! config { + ($($tokens:tt)*) => { + __itconfig_parse_module! { + tokens = [$($tokens)*], + name = cfg, + } + } +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __itconfig_invalid_syntax { + () => { + compile_error!( + "Invalid `config!` syntax. Please see the `config!` macro docs for more info.\ + `https://docs.rs/itconfig/latest/itconfig/macro.config.html`" + ); + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! __itconfig_parse_module { + // Find module name + ( + tokens = [ + #![mod_name = $mod_name:ident] + $($rest:tt)* + ], + name = $ignore:tt, + ) => { + __itconfig_parse_module! { + tokens = [$($rest)*], + name = $mod_name, + } + }; + + // Done parsing module + ( + tokens = $tokens:tt, + name = $name:tt, + ) => { + __itconfig_parse_variables! { + tokens = $tokens, + variables = [], + namespaces = [], + module = { + env_prefix = "", + name = $name, + meta = [], + }, + } + }; + + // Invalid syntax + ($($tokens:tt)*) => { + __itconfig_invalid_syntax!(); + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __itconfig_parse_variables { + // Find namespace + ( + tokens = [ + $(#$meta:tt)* + $ns_name:ident { $($ns_tokens:tt)* } + $($rest:tt)* + ], + $($args:tt)* + ) => { + __itconfig_parse_variables! { + tokens = [$($ns_tokens)*], + variables = [], + namespaces = [], + module = { + env_prefix = concat!(stringify!($ns_name), "_"), + name = $ns_name, + meta = [$(#$meta)*], + }, + callback = { + tokens = [$($rest)*], + $($args)* + }, + } + }; + + // Find concatenated variable + ( + tokens = [ + $(#$meta:tt)* + $name:ident < ($($inner:tt)+), + $($rest:tt)* + ], + $($args:tt)* + ) => { + __itconfig_parse_variables! { + current_variable = { + unparsed_meta = [$(#$meta)*], + meta = [], + unparsed_concat = [$($inner)+], + concat = [], + name = $name, + ty = String, + }, + tokens = [$($rest)*], + $($args)* + } + }; + + // Find variable + ( + tokens = [ + $(#$meta:tt)* + $name:ident : $ty:ty$( => $default:expr)?, + $($rest:tt)* + ], + $($args:tt)* + ) => { + __itconfig_parse_variables! { + current_variable = { + unparsed_meta = [$(#$meta)*], + meta = [], + unparsed_concat = [], + concat = [], + name = $name, + ty = $ty, + $(default = $default,)? + }, + tokens = [$($rest)*], + $($args)* + } + }; + + // Find meta with custom env name + ( + current_variable = { + unparsed_meta = [ + #[env_name = $env_name:expr] + $($rest:tt)* + ], + meta = $meta:tt, + unparsed_concat = $unparsed_concat:tt, + concat = $concat:tt, + name = $name:ident, + $($current_variable:tt)* + }, + $($args:tt)* + ) => { + __itconfig_parse_variables! { + current_variable = { + unparsed_meta = [$($rest)*], + meta = $meta, + unparsed_concat = $unparsed_concat, + concat = $concat, + name = $name, + env_name = $env_name, + $($current_variable)* + }, + $($args)* + } + }; + + // Find stranger meta + ( + current_variable = { + unparsed_meta = [ + #$stranger_meta:tt + $($rest:tt)* + ], + meta = [$(#$meta:tt,)*], + $($current_variable:tt)* + }, + $($args:tt)* + ) => { + __itconfig_parse_variables! { + current_variable = { + unparsed_meta = [$($rest)*], + meta = [$(#$meta,)* #$stranger_meta,], + $($current_variable)* + }, + $($args)* + } + }; + + // Parse concat params + ( + current_variable = { + unparsed_meta = $unparsed_meta:tt, + meta = $meta:tt, + unparsed_concat = [ + $concat_param:tt$( => $default:expr)?, + $($rest:tt)* + ], + concat = [$($concat:expr,)*], + $($current_variable:tt)* + }, + $($args:tt)* + ) => { + __itconfig_parse_variables! { + current_variable = { + unparsed_meta = $unparsed_meta, + meta = $meta, + unparsed_concat = [$($rest)*], + concat = [$($concat,)* __itconfig_concat_param!($concat_param$( => $default)?),], + $($current_variable)* + }, + $($args)* + } + }; + + // Done parsing variable + ( + current_variable = { + unparsed_meta = [], + meta = $meta:tt, + unparsed_concat = [], + $($current_variable:tt)* + }, + tokens = $tokens:tt, + variables = [$($variables:tt,)*], + $($args:tt)* + ) => { + __itconfig_parse_variables! { + tokens = $tokens, + variables = [$($variables,)* { meta = $meta, $($current_variable)* },], + $($args)* + } + }; + + // Done parsing all variables of namespace + ( + tokens = [], + variables = $ns_variables:tt, + namespaces = $ns_namespaces:tt, + module = $ns_module:tt, + callback = { + tokens = $tokens:tt, + variables = $variables:tt, + namespaces = [$($namespaces:tt,)*], + $($args:tt)* + }, + ) => { + __itconfig_parse_variables! { + tokens = $tokens, + variables = $variables, + namespaces = [ + $($namespaces,)* + { + variables = $ns_variables, + namespaces = $ns_namespaces, + module = $ns_module, + }, + ], + $($args)* + } + }; + + // Done parsing all variables + ( + tokens = [], + $($args:tt)* + ) => { + __itconfig_impl_namespace!($($args)*); + }; + + // Invalid syntax + ($($tokens:tt)*) => { + __itconfig_invalid_syntax!(); + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __itconfig_impl_namespace { + ( + variables = [$({ + meta = $var_meta:tt, + concat = $var_concat:tt, + name = $var_name:ident, + $($variable:tt)* + },)*], + namespaces = [$({ + variables = $ns_variable:tt, + namespaces = $ns_namespaces:tt, + module = { + env_prefix = $ns_env_prefix:expr, + name = $ns_mod_name:ident, + meta = [$(#$ns_meta:tt)*], + }, + },)*], + module = { + env_prefix = $env_prefix:expr, + name = $mod_name:ident, + meta = [$(#$meta:tt)*], + }, + ) => { + $(#$meta)* + pub mod $mod_name { + #![allow(non_snake_case)] + + $(__itconfig_impl_namespace! { + variables = $ns_variable, + namespaces = $ns_namespaces, + module = { + env_prefix = $ns_env_prefix, + name = $ns_mod_name, + meta = [$(#$ns_meta)*], + }, + })* + + pub fn init() { + $($var_name();)* + $( + $(#$ns_meta)* + $ns_mod_name::init(); + )* + } + + $(__itconfig_variable! { + meta = $var_meta, + concat = $var_concat, + name = $var_name, + env_prefix = $env_prefix, + $($variable)* + })* + } + }; + + // Invalid syntax + ($($tokens:tt)*) => { + __itconfig_invalid_syntax!(); + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __itconfig_concat_param { + // Find env parameter with default value + ($env_name:ident => $default:expr) => ( + itconfig::get_env_or_default( + stringify!($env_name).to_uppercase().as_str(), + $default + ) + ); + + // Find env parameter without default value + ($env_name:ident) => ( + itconfig::get_env( + stringify!($env_name).to_uppercase().as_str() + ) + ); + + // Find string parameter + ($str:expr) => ( $str.to_string() ); + + // Invalid syntax + ($($tokens:tt)*) => { + __itconfig_invalid_syntax!(); + }; +} + + +#[macro_export] +#[doc(hidden)] +macro_rules! __itconfig_variable { + // Set default env name + ( + meta = $meta:tt, + concat = $concat:tt, + name = $name:ident, + env_prefix = $env_prefix:expr, + ty = $ty:ty, + $($args:tt)* + ) => { + __itconfig_variable! { + meta = $meta, + concat = $concat, + name = $name, + env_prefix = $env_prefix, + env_name = concat!($env_prefix, stringify!($name)).to_uppercase(), + ty = $ty, + $($args)* + } + }; + + // Add method for concatenated variable +// ( +// meta = [$(#$meta:tt,)*], +// concat = [$($concat:expr,)+], +// name = $name:ident, +// env_prefix = $env_prefix:expr, +// env_name = $env_name:expr, +// ty = $ty:ty, +// $($args:tt)* +// ) => { +// $(#$meta)* +// pub fn $name() -> $ty { +// let value_parts: Vec = vec!($($concat),+); +// let value = value_parts.join(""); +// __itconfig_variable_helper!(@setenv $env_name, value) +// } +// }; + + // Add method for env variable + ( + meta = [$(#$meta:tt,)*], + concat = $concat:tt, + name = $name:ident, + env_prefix = $env_prefix:expr, + env_name = $env_name:expr, + ty = $ty:ty, + $(default = $default:expr,)? + ) => { + $(#$meta)* + pub fn $name() -> $ty { + __itconfig_variable! { + @inner + concat = $concat, + env_name = $env_name, + $(default = $default,)? + } + } + }; + + ( + @inner + concat = [$($concat:expr,)+], + env_name = $env_name:expr, + $($args:tt)* + ) => ( + let value_parts: Vec = vec!($($concat),+); + let value = value_parts.join(""); + std::env::set_var($env_name, value.as_str()); + value + ); + + ( + @inner + concat = [], + env_name = $env_name:expr, + ) => ( + itconfig::get_env($env_name.to_string().as_str()) + ); + + ( + @inner + concat = [], + env_name = $env_name:expr, + default = $default:expr, + ) => ( + itconfig::get_env_or_set_default( + $env_name.to_string().as_str(), + $default + ) + ); + + // Invalid syntax + ($($tokens:tt)*) => { + __itconfig_invalid_syntax!(); + }; +} + diff --git a/itconfig_tests/tests/config_macro.rs b/itconfig_tests/tests/config_macro.rs index 3f8685d..4bd7d02 100644 --- a/itconfig_tests/tests/config_macro.rs +++ b/itconfig_tests/tests/config_macro.rs @@ -6,7 +6,7 @@ extern crate itconfig; #[test] -#[should_panic(expected = "Cannot read \"MISS_VARIABLE\" environment variable")] +#[should_panic(expected = "Environment variable \"MISS_VARIABLE\" is missing")] fn should_panic_if_miss_env_variable() { config! { MISS_VARIABLE: bool, @@ -334,7 +334,7 @@ fn setting_default_concat_env_variable() { #[test] -#[should_panic(expected = "Cannot read \"PG_USERNAME\" environment variable")] +#[should_panic(expected = "Environment variable \"PG_USERNAME\" is missing")] fn concatenate_not_defined_environment_variables() { config! { DATABASE_URL < ( diff --git a/itconfig_tests/tests/get_env.rs b/itconfig_tests/tests/get_env.rs new file mode 100644 index 0000000..4dc4f5b --- /dev/null +++ b/itconfig_tests/tests/get_env.rs @@ -0,0 +1,23 @@ +use std::env; +use itconfig::*; + +#[test] +#[should_panic(expected = "Environment variable \"TEST_CASE_1\" is missing")] +fn missing_env_variable() { + get_env::("TEST_CASE_1"); +} + +#[test] +#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_2\"")] +fn cannot_parse_env_variable() { + env::set_var("TEST_CASE_2", "30r"); + get_env::("TEST_CASE_2"); +} + +#[test] +fn get_env_successfully() { + env::set_var("TEST_CASE_3", "30"); + let a: u32 = get_env("TEST_CASE_3"); + + assert_eq!(a, 30); +} diff --git a/itconfig_tests/tests/get_env_or_default.rs b/itconfig_tests/tests/get_env_or_default.rs new file mode 100644 index 0000000..53e6af9 --- /dev/null +++ b/itconfig_tests/tests/get_env_or_default.rs @@ -0,0 +1,31 @@ +use std::env; +use itconfig::*; + +#[test] +fn missing_env_variable() { + let flag: bool = get_env_or_default("DEFAULT_TEST_CASE_1", "true"); + assert_eq!(flag, true); + +// let var: String = env::var("DEFAULT_TEST_CASE_1").unwrap(); +// assert_eq!(var, "true"); +} + +#[test] +#[should_panic(expected = "Failed to parse environment variable \"DEFAULT_TEST_CASE_2\"")] +fn cannot_parse_env_variable() { + env::set_var("DEFAULT_TEST_CASE_2", "30r"); + let _: u32 = get_env_or_default("DEFAULT_TEST_CASE_2", 30); +} + +#[test] +#[should_panic(expected = "Failed to parse environment variable \"DEFAULT_TEST_CASE_2\"")] +fn cannot_parse_default_value() { + let _: u32 = get_env_or_default("DEFAULT_TEST_CASE_2", "30r"); +} + +#[test] +fn get_env_successfully() { + let a: u32 = get_env_or_default("DEFAULT_TEST_CASE_3", 30); + + assert_eq!(a, 30); +}