diff --git a/README.md b/README.md index 71a1ad5..fc6d1a6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,17 @@ config! { DEBUG: bool => true, HOST: String => "127.0.0.1".to_string(), + DATABASE_URL < ( + "postgres://", + POSTGRES_USERNAME => "user", + ":", + POSTGRES_PASSWORD => "pass", + "@", + POSTGRES_HOST => "localhost:5432", + "/", + POSTGRES_DB => "test", + ), + NAMESPACE { #[env_name = "MY_CUSTOM_NAME"] FOO: bool, @@ -37,8 +48,9 @@ fn main () { env::set_var("MY_CUSTOM_NAME", "t"); cfg::init(); - assert_eq(cfg::HOST(), String::from("127.0.0.1")); - assert_eq(cfg::NAMESPACE::FOO(), true); + 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::NAMESPACE::FOO(), true); } ``` @@ -55,7 +67,7 @@ cargo test * [x] Custom env name * [x] Support feature config and other meta directives * [x] Add default value to env if env is not found -* [ ] Concat env variables to one variable +* [x] Concat env variables to one variable ## License diff --git a/itconfig/Cargo.toml b/itconfig/Cargo.toml index 68cf56e..49f5f98 100644 --- a/itconfig/Cargo.toml +++ b/itconfig/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "itconfig" -version = "0.6.0" +version = "0.7.0" authors = ["Dmitriy Pleshevskiy "] description = "Easy build a configs from environment variables and use it in globally." categories = ["config", "web-programming"] diff --git a/itconfig/README.md b/itconfig/README.md index a7c6cc7..7ab51fa 100644 --- a/itconfig/README.md +++ b/itconfig/README.md @@ -16,6 +16,17 @@ config! { DEBUG: bool => true, HOST: String => "127.0.0.1".to_string(), + DATABASE_URL < ( + "postgres://", + POSTGRES_USERNAME => "user", + ":", + POSTGRES_PASSWORD => "pass", + "@", + POSTGRES_HOST => "localhost:5432", + "/", + POSTGRES_DB => "test", + ), + NAMESPACE { #[env_name = "MY_CUSTOM_NAME"] FOO: bool, @@ -33,8 +44,9 @@ fn main () { env::set_var("MY_CUSTOM_NAME", "t"); cfg::init(); - assert_eq(cfg::HOST(), String::from("127.0.0.1")); - assert_eq(cfg::NAMESPACE::FOO(), true); + 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::NAMESPACE::FOO(), true); } ``` @@ -45,7 +57,7 @@ fn main () { * [x] Custom env name * [x] Support feature config and other meta directives * [x] Add default value to env if env is not found -* [ ] Concat env variables to one variable +* [x] Concat env variables to one variable ## License diff --git a/itconfig/src/lib.rs b/itconfig/src/lib.rs index 7825335..d100b80 100644 --- a/itconfig/src/lib.rs +++ b/itconfig/src/lib.rs @@ -12,7 +12,18 @@ //! //! config! { //! DEBUG: bool => true, -//! HOST: String => "127.0.0.1".to_string(), +//! HOST: String => "127.0.0.1", +//! +//! DATABASE_URL < ( +//! "postgres://", +//! POSTGRES_USERNAME => "user", +//! ":", +//! POSTGRES_PASSWORD => "pass", +//! "@", +//! POSTGRES_HOST => "localhost:5432", +//! "/", +//! POSTGRES_DB => "test", +//! ), //! //! NAMESPACE { //! #[env_name = "MY_CUSTOM_NAME"] @@ -28,6 +39,7 @@ //! //! 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::NAMESPACE::FOO(), true); //! } //! ``` @@ -83,6 +95,8 @@ impl From for String { /// 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 /// ------------- /// @@ -104,7 +118,7 @@ impl From for String { /// # env::set_var("DATABASE_URL", "postgres://u:p@localhost:5432/db"); /// config! { /// DATABASE_URL: String, -/// HOST: String => "127.0.0.1".to_string(), +/// HOST: String => "127.0.0.1", /// } /// # cfg::init() /// ``` @@ -127,6 +141,9 @@ impl From for String { /// assert_eq!(configuration::DEBUG(), true); /// ``` /// +/// Namespaces +/// ---------- +/// /// You can use namespaces for env variables /// /// ```rust @@ -148,6 +165,9 @@ impl From for String { /// # 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** @@ -186,6 +206,57 @@ impl From for 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: @@ -211,7 +282,7 @@ impl From for String { /// /// config! { /// DEBUG: bool => true, -/// HOST: String => "127.0.0.1".to_string(), +/// HOST: String => "127.0.0.1", /// } /// /// fn main () { @@ -308,6 +379,29 @@ macro_rules! __itconfig_parse_variables { } }; + // 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 = [ @@ -321,6 +415,8 @@ macro_rules! __itconfig_parse_variables { current_variable = { unparsed_meta = [$(#$meta)*], meta = [], + unparsed_concat = [], + concat = [], name = $name, ty = $ty, $(default = $default,)? @@ -338,6 +434,8 @@ macro_rules! __itconfig_parse_variables { $($rest:tt)* ], meta = $meta:tt, + unparsed_concat = $unparsed_concat:tt, + concat = $concat:tt, name = $name:ident, $($current_variable:tt)* }, @@ -347,6 +445,8 @@ macro_rules! __itconfig_parse_variables { current_variable = { unparsed_meta = [$($rest)*], meta = $meta, + unparsed_concat = $unparsed_concat, + concat = $concat, name = $name, env_name = $env_name, $($current_variable)* @@ -363,7 +463,6 @@ macro_rules! __itconfig_parse_variables { $($rest:tt)* ], meta = [$(#$meta:tt,)*], - name = $name:ident, $($current_variable:tt)* }, $($args:tt)* @@ -372,7 +471,32 @@ macro_rules! __itconfig_parse_variables { current_variable = { unparsed_meta = [$($rest)*], meta = [$(#$meta,)* #$stranger_meta,], - name = $name, + $($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)* @@ -383,6 +507,8 @@ macro_rules! __itconfig_parse_variables { ( current_variable = { unparsed_meta = [], + meta = $meta:tt, + unparsed_concat = [], $($current_variable:tt)* }, tokens = $tokens:tt, @@ -391,7 +517,7 @@ macro_rules! __itconfig_parse_variables { ) => { __itconfig_parse_variables! { tokens = $tokens, - variables = [$($variables,)* { $($current_variable)* },], + variables = [$($variables,)* { meta = $meta, $($current_variable)* },], $($args)* } }; @@ -445,12 +571,14 @@ macro_rules! __itconfig_impl { ( variables = [$({ meta = $var_meta:tt, + concat = $var_concat:tt, name = $var_name:ident, $($variable:tt)* },)*], namespaces = [$({ variables = [$({ meta = $ns_var_meta:tt, + concat = $ns_var_concat:tt, name = $ns_var_name:ident, $($ns_variables:tt)* },)*], @@ -464,10 +592,17 @@ macro_rules! __itconfig_impl { ) => { pub mod $mod_name { #![allow(non_snake_case)] + use std::env; + use itconfig::EnvValue; + $( pub mod $ns_name { + use std::env; + use itconfig::EnvValue; + $(__itconfig_variable! { meta = $ns_var_meta, + concat = $ns_var_concat, name = $ns_var_name, env_prefix = $ns_env_prefix, $($ns_variables)* @@ -483,6 +618,7 @@ macro_rules! __itconfig_impl { $(__itconfig_variable! { meta = $var_meta, + concat = $var_concat, name = $var_name, env_prefix = $env_prefix, $($variable)* @@ -497,12 +633,36 @@ macro_rules! __itconfig_impl { } +#[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, @@ -510,6 +670,7 @@ macro_rules! __itconfig_variable { ) => { __itconfig_variable! { meta = $meta, + concat = $concat, name = $name, env_prefix = $env_prefix, env_name = concat!($env_prefix, stringify!($name)).to_uppercase(), @@ -518,9 +679,28 @@ macro_rules! __itconfig_variable { } }; - // Add method + // 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, @@ -556,29 +736,66 @@ macro_rules! __itconfig_variable { /// ``` #[macro_export(local_inner_macro)] macro_rules! env_or { + // Env without default value ($env_name:expr) => { - env_or!($env_name, format!(r#"Cannot read "{}" environment variable"#, $env_name), panic); + __itconfig_variable_helper!($env_name, format!(r#"Cannot read "{}" environment variable"#, $env_name), panic); }; + // Env with default value ($env_name:expr, $default:expr) => { - env_or!($env_name, $default, default); + __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| EnvValue::from(val).into()) - .unwrap_or_else(|_| env_or!(@$token $env_name, $default)) + .map(|val| __itconfig_variable_helper!(val)) + .unwrap_or_else(|_| __itconfig_variable_helper!(@$token $env_name, $default)) }}; - (@default $env_name:expr, $default:expr) => {{ + // 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()); - $default + __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!(); + }; } diff --git a/itconfig_tests/tests/config_macro.rs b/itconfig_tests/tests/config_macro.rs index 00c16df..7373439 100644 --- a/itconfig_tests/tests/config_macro.rs +++ b/itconfig_tests/tests/config_macro.rs @@ -1,11 +1,12 @@ use std::env; +use std::env::VarError; #[macro_use] extern crate itconfig; #[test] -#[should_panic] +#[should_panic(expected = "Cannot read \"MISS_VARIABLE\" environment variable")] fn should_panic_if_miss_env_variable() { config! { MISS_VARIABLE: bool, @@ -55,13 +56,15 @@ fn different_types_with_default_value() { config! { NUMBER: i32 => 30, BOOL: bool => true, + STR: String => "str", STRING: String => "string".to_string(), } cfg::init(); assert_eq!(cfg::NUMBER(), 30); assert_eq!(cfg::BOOL(), true); - assert_eq!(cfg::STRING(), "string"); + assert_eq!(cfg::STR(), "str".to_string()); + assert_eq!(cfg::STRING(), "string".to_string()); } #[test] @@ -157,12 +160,12 @@ fn change_configuration_module_name() { #[test] fn configuration_with_namespace() { - env::set_var("POSTGRES_HOST", "t"); + env::set_var("DB_HOST", "t"); config! { DEBUG: bool => true, - POSTGRES { + DB { HOST: bool, PORT: bool => true, USERNAME: bool => true, @@ -173,8 +176,7 @@ fn configuration_with_namespace() { cfg::init(); assert_eq!(cfg::DEBUG(), true); - assert_eq!(cfg::POSTGRES::HOST(), true); - env::remove_var("POSTGRES_HOST"); + assert_eq!(cfg::DB::HOST(), true); } @@ -194,8 +196,6 @@ fn configuration_variables_and_namespace_in_lowercase() { cfg::init(); assert_eq!(cfg::testing(), true); assert_eq!(cfg::namespace::foo(), true); - env::remove_var("TESTING"); - env::remove_var("NAMESPACE_FOO"); } @@ -216,7 +216,6 @@ fn custom_environment_name_for_variable() { cfg::init(); assert_eq!(cfg::PER_PAGE(), 95); assert_eq!(cfg::APP::RECIPES_PER_PAGE(), 95); - env::remove_var("MY_CUSTOM_NAME"); } #[test] @@ -239,13 +238,12 @@ fn stranger_meta_data() { #[cfg(feature = "postgres")] assert_eq!(cfg::DATABASE_URL(), "95"); - env::remove_var("MY_CUSTOM_NAME"); } #[test] fn setting_default_env_variable() { config! { - DEFAULT_ENV_STRING: String => "localhost".to_string(), + DEFAULT_ENV_STRING: String => "localhost", DEFAULT_ENV_BOOLEAN: bool => true, DEFAULT_ENV_UINT: u32 => 40, DEFAULT_ENV_FLOAT: f64 => 40.9, @@ -259,3 +257,135 @@ fn setting_default_env_variable() { assert_eq!(env::var("DEFAULT_ENV_FLOAT"), Ok("40.9".to_string())); } + +#[test] +fn concatenate_environment_variables() { + env::set_var("POSTGRES_USERNAME", "user"); + env::set_var("POSTGRES_PASSWORD", "pass"); + env::set_var("POSTGRES_HOST", "localhost"); + env::set_var("POSTGRES_DB", "test"); + + config! { + DATABASE_URL < ( + "postgres://", + POSTGRES_USERNAME, + ":", + POSTGRES_PASSWORD, + "@", + POSTGRES_HOST, + "/", + POSTGRES_DB, + ), + } + + cfg::init(); + assert_eq!(cfg::DATABASE_URL(), String::from("postgres://user:pass@localhost/test")); +} + + +#[test] +fn setting_default_concat_env_variable() { + env::set_var("SETTING_DEFAULT_CONCAT_ENV_VARIABLE", "custom"); + + config! { + DEFAULT_CONCAT_ENV < ( + "string", + "/", + SETTING_DEFAULT_CONCAT_ENV_VARIABLE, + ), + } + + cfg::init(); + assert_eq!(env::var("DEFAULT_CONCAT_ENV"), Ok("string/custom".to_string())); +} + + +#[test] +#[should_panic(expected = "Cannot read \"PG_USERNAME\" environment variable")] +fn concatenate_not_defined_environment_variables() { + config! { + DATABASE_URL < ( + "postgres://", + PG_USERNAME, + ":", + PG_PASSWORD, + "@", + PG_HOST, + "/", + PG_DB, + ), + } + cfg::init(); +} + + +#[test] +fn default_value_for_concatenate_env_parameter() { + config! { + CONCATENATED_DATABASE_URL < ( + "postgres://", + NOT_DEFINED_PG_USERNAME => "user", + ":", + NOT_DEFINED_PG_PASSWORD => "pass", + "@", + NOT_DEFINED_PG_HOST => "localhost:5432", + "/", + NOT_DEFINED_PG_DB => "test", + ), + } + + cfg::init(); + assert_eq!( + env::var("CONCATENATED_DATABASE_URL"), + Ok("postgres://user:pass@localhost:5432/test".to_string()) + ); +} + +#[test] +fn envname_meta_for_concatenated_env_variable() { + config! { + #[env_name = "CUSTOM_CONCAT_ENVNAME"] + 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(); + assert_eq!( + env::var("CUSTOM_CONCAT_ENVNAME"), + Ok("postgres://user:pass@localhost:5432/test".to_string()) + ); + assert_eq!(env::var("CONCAT_ENVVAR"), Err(VarError::NotPresent)); +} + +#[test] +fn concatenated_environment_variable_in_namespace() { + config! { + CONCATED_NAMESPACE { + 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(); + assert_eq!( + env::var("CONCATED_NAMESPACE_CONCAT_ENVVAR"), + Ok("postgres://user:pass@localhost:5432/test".to_string()) + ); + assert_eq!(env::var("CONCAT_ENVVAR"), Err(VarError::NotPresent)); +}