From 5d6b335a7ae2911cac36a11f94135112ec5d473f Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 22 Apr 2021 02:11:04 +0300 Subject: [PATCH] feat: add vec supporting --- itconfig-macro/src/expand.rs | 28 ++- itconfig-macro/src/utils.rs | 28 ++- itconfig-tests/Cargo.toml | 2 +- itconfig-tests/tests/config_macro.rs | 20 +- itconfig/src/envstr.rs | 2 +- itconfig/src/{getenv.rs => get_env.rs} | 13 +- itconfig/src/get_vec_env.rs | 322 +++++++++++++++++++++++++ itconfig/src/lib.rs | 7 +- itconfig/src/utils.rs | 13 + 9 files changed, 405 insertions(+), 30 deletions(-) rename itconfig/src/{getenv.rs => get_env.rs} (95%) create mode 100644 itconfig/src/get_vec_env.rs create mode 100644 itconfig/src/utils.rs diff --git a/itconfig-macro/src/expand.rs b/itconfig-macro/src/expand.rs index 7008088..c284628 100644 --- a/itconfig-macro/src/expand.rs +++ b/itconfig-macro/src/expand.rs @@ -1,5 +1,5 @@ use crate::ast::*; -use crate::utils::{is_option_type, vec_to_token_stream_2}; +use crate::utils::{maybe_supported_box, vec_to_token_stream_2, SupportedBox}; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; @@ -128,6 +128,8 @@ impl ToTokens for Variable { .unwrap_or_else(|| name.to_string().to_uppercase()); let meta = vec_to_token_stream_2(&self.meta); + let supported_box = maybe_supported_box(&ty); + let get_variable: TokenStream2 = if self.concat_parts.is_some() { let concat_parts = self.concat_parts.as_ref().unwrap(); quote! {{ @@ -136,13 +138,25 @@ impl ToTokens for Variable { ::std::env::set_var(#env_name, value.as_str()); value }} - } else if self.initial.is_some() { - let initial = self.initial.as_ref().unwrap(); - quote!(::itconfig::get_env_or_set_default(#env_name, #initial)) - } else if is_option_type(&self.ty) { - quote!(::itconfig::maybe_get_env(#env_name)) + } else if let Some(initial) = &self.initial { + match supported_box { + Some(SupportedBox::Vec) => { + quote!(::itconfig::get_vec_env_or_set_default(#env_name, ",", #initial)) + } + _ => quote!(::itconfig::get_env_or_set_default(#env_name, #initial)), + } } else { - quote!(::itconfig::get_env_or_panic(#env_name)) + match supported_box { + Some(SupportedBox::Option) => { + quote!(::itconfig::maybe_get_env(#env_name)) + } + Some(SupportedBox::Vec) => { + quote!(::itconfig::get_vec_env_or_panic(#env_name, ",")) + } + _ => { + quote!(::itconfig::get_env_or_panic(#env_name)) + } + } }; if self.is_static { diff --git a/itconfig-macro/src/utils.rs b/itconfig-macro/src/utils.rs index 3d03212..ebc0fa2 100644 --- a/itconfig-macro/src/utils.rs +++ b/itconfig-macro/src/utils.rs @@ -3,6 +3,13 @@ use quote::ToTokens; use syn::{Path, Type}; const OPTION_PATH_IDENTS: &[&str] = &["Option|", "std|option|Option|", "core|option|Option|"]; +const VEC_PATH_IDENTS: &[&str] = &["Vec|", "std|vec|Vec|"]; + +#[derive(Debug)] +pub enum SupportedBox { + Vec, + Option, +} pub fn vec_to_token_stream_2(input: &[T]) -> Vec where @@ -22,15 +29,26 @@ fn path_ident(path: &Path) -> String { }) } -fn is_option_path_ident(path_ident: String) -> bool { +fn is_option_path_ident(path_ident: &str) -> bool { OPTION_PATH_IDENTS.iter().any(|s| path_ident == *s) } -pub fn is_option_type(ty: &Type) -> bool { +fn is_vec_path_ident(path_ident: &str) -> bool { + VEC_PATH_IDENTS.iter().any(|s| path_ident == *s) +} + +pub fn maybe_supported_box(ty: &Type) -> Option { match ty { - Type::Path(ty_path) => { - ty_path.qself.is_none() && is_option_path_ident(path_ident(&ty_path.path)) + Type::Path(ty_path) if ty_path.qself.is_none() => { + let path_ident = path_ident(&ty_path.path); + if is_option_path_ident(&path_ident) { + Some(SupportedBox::Option) + } else if is_vec_path_ident(&path_ident) { + Some(SupportedBox::Vec) + } else { + None + } } - _ => false, + _ => None, } } diff --git a/itconfig-tests/Cargo.toml b/itconfig-tests/Cargo.toml index 7a72838..be42141 100644 --- a/itconfig-tests/Cargo.toml +++ b/itconfig-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "itconfig_tests" -version = "0.1.0" +version = "0.1.1" authors = ["Dmitriy Pleshevskiy "] edition = "2018" license = "MIT" diff --git a/itconfig-tests/tests/config_macro.rs b/itconfig-tests/tests/config_macro.rs index 8cf4b75..551328d 100644 --- a/itconfig-tests/tests/config_macro.rs +++ b/itconfig-tests/tests/config_macro.rs @@ -547,8 +547,6 @@ mod test_case_23 { #[test] fn optional_variables() { - config::init(); - env::set_var("SOMETHING", "hello world"); assert_eq!(config::SOMETHING(), Some("hello world")); @@ -557,3 +555,21 @@ mod test_case_23 { assert_eq!(config::NOTHING(), None); } } + +mod test_case_24 { + use std::env; + + itconfig::config! { + MY_VEC: Vec<&'static str>, + #[env_name = "MY_VEC"] + STD_VEC: std::vec::Vec<&'static str>, + } + + #[test] + fn vector_of_values() { + env::set_var("MY_VEC", "paypal,stripe"); + + assert_eq!(config::MY_VEC(), vec!["paypal", "stripe"]); + assert_eq!(config::STD_VEC(), vec!["paypal", "stripe"]); + } +} diff --git a/itconfig/src/envstr.rs b/itconfig/src/envstr.rs index efc7838..5908daa 100644 --- a/itconfig/src/envstr.rs +++ b/itconfig/src/envstr.rs @@ -5,7 +5,7 @@ use std::ops::Deref; /// When we read the environment variable, we automatically convert the value /// to EnvString and then convert it to your expected type. /// -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct EnvString(String); impl From for EnvString diff --git a/itconfig/src/getenv.rs b/itconfig/src/get_env.rs similarity index 95% rename from itconfig/src/getenv.rs rename to itconfig/src/get_env.rs index 4f1d90d..534c188 100644 --- a/itconfig/src/getenv.rs +++ b/itconfig/src/get_env.rs @@ -1,5 +1,6 @@ use crate::envstr::*; use crate::error::*; +use crate::utils::*; use std::env; /// Same as get_env but returns Option enum instead Result @@ -152,18 +153,6 @@ where .and_then(|env_str| parse_env_variable(env_name, env_str)) } -fn parse_env_variable(env_name: &str, env_str: EnvString) -> Result -where - T: FromEnvString, -{ - FromEnvString::from_env_string(&env_str) - .map_err(|_| EnvError::FailedToParse(env_name.to_string())) -} - -fn make_panic(e: EnvError) -> T { - panic!("{}", e) -} - #[cfg(test)] mod tests { use super::*; diff --git a/itconfig/src/get_vec_env.rs b/itconfig/src/get_vec_env.rs new file mode 100644 index 0000000..c045c5c --- /dev/null +++ b/itconfig/src/get_vec_env.rs @@ -0,0 +1,322 @@ +use crate::envstr::*; +use crate::error::*; +use crate::utils::*; +use std::env; + +/// Same as get_vec_env but returns Option enum instead Result +/// +/// Example +/// ------- +/// +/// ```rust +/// # extern crate itconfig; +/// # use itconfig::*; +/// use std::env; +/// +/// #[derive(Debug, PartialEq, Eq)] +/// enum PaymentPlatform { +/// PayPal, +/// Stripe, +/// SomethingElse, +/// } +/// +/// impl FromEnvString for PaymentPlatform { +/// type Err = &'static str; +/// +/// fn from_env_string(envstr: &EnvString) -> Result { +/// match envstr.to_lowercase().as_str() { +/// "paypal" => Ok(Self::PayPal), +/// "stripe" => Ok(Self::Stripe), +/// "smth" => Ok(Self::SomethingElse), +/// _ => Err("Unsupported payment platform"), +/// } +/// } +/// } +/// +/// +/// fn main () { +/// env::set_var("PAYMENT_PLATFORMS", "paypal,stripe"); +/// +/// let payment_platforms: Option> = maybe_get_vec_env("PAYMENT_PLATFORMS", ","); +/// assert_eq!( +/// payment_platforms, +/// Some(vec![PaymentPlatform::PayPal, PaymentPlatform::Stripe]) +/// ); +/// } +/// ``` +/// +pub fn maybe_get_vec_env(env_name: &str, sep: &'static str) -> Option> +where + T: FromEnvString, +{ + get_vec_env(env_name, sep).ok() +} + +/// This function is similar as `get_vec_env`, but it unwraps result with panic on error. +/// +/// Panics +/// ------ +/// Application will panic if environment variable is missing or cannot parse variable to +/// expected type +/// +pub fn get_vec_env_or_panic(env_name: &str, sep: &'static str) -> Vec +where + T: FromEnvString, +{ + get_vec_env(env_name, sep).unwrap_or_else(make_panic) +} + +/// Try to read environment variable, split by separator and parse each item to expected +/// type. +/// +/// Example +/// ------- +/// +/// ```rust +/// # extern crate itconfig; +/// # use itconfig::get_vec_env; +/// use std::env; +/// +/// fn main () { +/// env::set_var("DEBUG", "true"); +/// +/// let result: Vec = get_vec_env("DEBUG", ",").unwrap(); +/// +/// assert_eq!(result, vec![true]); +/// } +/// ``` +/// +pub fn get_vec_env(env_name: &str, sep: &'static str) -> Result, EnvError> +where + T: FromEnvString, +{ + get_vec_env_or(env_name, sep, |_| { + Err(EnvError::MissingVariable(env_name.to_string())) + }) +} + +/// This function is similar as `get_vec_env_or_panic`, but 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_vec_env_or_default; +/// use std::env; +/// +/// fn main () { +/// let result: Vec = get_vec_env_or_default("TESTING", ",", vec!["true"]); +/// assert_eq!(result, vec![true]); +/// } +/// ``` +/// +pub fn get_vec_env_or_default(env_name: &str, sep: &'static str, default: Vec) -> Vec +where + T: FromEnvString, + D: ToEnvString, +{ + get_vec_env_or(env_name, sep, |_| { + Ok(default.into_iter().map(EnvString::from).collect()) + }) + .unwrap_or_else(make_panic) +} + +/// This function is similar as `get_vec_env_or_default`, but the default value will be set to environment +/// variable, if env variable is missed. +/// +/// Panics +/// ------ +/// Application will panic if cannot parse variable to expected type +/// +/// Example +/// ------- +/// +/// ```rust +/// # extern crate itconfig; +/// # use itconfig::get_vec_env_or_set_default; +/// use std::env; +/// +/// fn main () { +/// let result: Vec = get_vec_env_or_set_default("TESTING", ",", vec!["true"]); +/// assert_eq!(result, vec![true]); +/// +/// let var = env::var("TESTING").unwrap(); +/// assert_eq!(var, "true"); +/// } +/// ``` +/// +pub fn get_vec_env_or_set_default( + env_name: &str, + sep: &'static str, + default: Vec, +) -> Vec +where + T: FromEnvString, + D: ToEnvString, +{ + get_vec_env_or(env_name, sep, |_| { + let default_env_strings: Vec = + default.into_iter().map(EnvString::from).collect(); + let env_val = + default_env_strings + .iter() + .enumerate() + .fold(String::new(), |mut res, (i, item)| { + if i > 0 { + res.push_str(sep); + } + res.push_str(&item); + res + }); + env::set_var(env_name, env_val.as_str()); + Ok(default_env_strings) + }) + .unwrap_or_else(make_panic) +} + +/// This function returns env variable as `EnvString` structure. You can pass callback for custom +/// default expression. Callback should return `EnvString` value or `EnvError` +pub fn get_vec_env_or(env_name: &str, sep: &'static str, cb: F) -> Result, EnvError> +where + T: FromEnvString, + F: FnOnce(env::VarError) -> Result, EnvError>, +{ + env::var(env_name) + .map(|s| { + s.split(sep) + .into_iter() + .map(|item| item.to_env_string()) + .collect() + }) + .or_else(cb) + .and_then(|items| { + items + .into_iter() + .map(|env_str| parse_env_variable(env_name, env_str)) + .collect() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SEP: &str = ","; + + #[test] + #[should_panic(expected = "Environment variable \"TEST_CASE_VEC_1\" is missing")] + fn get_missing_vec_env() { + let _: Vec<&'static str> = get_vec_env_or_panic("TEST_CASE_VEC_1", SEP); + } + + #[test] + #[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_VEC_2\"")] + fn get_vec_env_with_invalid_value() { + let env_name = "TEST_CASE_VEC_2"; + env::set_var(&env_name, "30r"); + let _: Vec = get_vec_env_or_panic(env_name, SEP); + } + + #[test] + fn get_result_of_missing_vec_env() { + let env_name = String::from("TEST_CASE_VEC_3"); + let env_val = get_vec_env::(&env_name, SEP); + assert_eq!(env_val, Err(EnvError::MissingVariable(env_name))) + } + + #[test] + fn get_result_of_vec_env_with_invalid_value() { + let env_name = String::from("TEST_CASE_VEC_4"); + env::set_var(&env_name, "30r"); + let env_val = get_vec_env::(&env_name, SEP); + assert_eq!(env_val, Err(EnvError::FailedToParse(env_name))) + } + + #[test] + fn get_result_of_vec_env_successfully() { + env::set_var("TEST_CASE_VEC_5", "30"); + let env_var = get_vec_env("TEST_CASE_VEC_5", SEP); + assert_eq!(env_var, Ok(vec![30])); + } + + #[test] + fn get_missing_vec_env_with_default_value() { + let flag: Vec = get_vec_env_or_default("TEST_CASE_VEC_6", SEP, vec!["true"]); + assert_eq!(flag, vec![true]); + } + + #[test] + #[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_VEC_7\"")] + fn get_invalid_vec_env_with_default_value() { + env::set_var("TEST_CASE_VEC_7", "30r"); + get_vec_env_or_default::("TEST_CASE_VEC_7", SEP, vec![30]); + } + + #[test] + #[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_VEC_8\"")] + fn get_vec_env_with_invalid_default_value() { + get_vec_env_or_default::("TEST_CASE_VEC_8", SEP, vec!["30r"]); + } + + #[test] + fn get_vec_env_with_default_successfully() { + env::set_var("TEST_CASE_VEC_9", "10"); + let env_val: Vec = get_vec_env_or_default("TEST_CASE_VEC_9", SEP, vec![30]); + assert_eq!(env_val, vec![10]) + } + + #[test] + fn get_missing_vec_env_with_set_default_value() { + let flag: Vec = get_vec_env_or_set_default("TEST_CASE_VEC_10", SEP, vec!["true"]); + assert_eq!(flag, vec![true]); + + let env_var = env::var("TEST_CASE_VEC_10"); + assert_eq!(env_var, Ok(String::from("true"))) + } + + #[test] + fn get_optional_vec_env() { + env::set_var("TEST_CASE_VEC_11", "something"); + let something: Option> = maybe_get_vec_env("TEST_CASE_VEC_11", SEP); + assert_eq!(something, Some(vec!["something"])); + + let nothing: Option> = maybe_get_vec_env("TEST_CASE_VEC_11_NONE", SEP); + assert_eq!(nothing, None); + } + + #[test] + fn get_custom_type_from_vec_env() { + #[derive(Debug, PartialEq, Eq)] + enum PaymentPlatform { + PayPal, + Stripe, + SomethingElse, + } + + impl FromEnvString for PaymentPlatform { + type Err = &'static str; + + fn from_env_string(envstr: &EnvString) -> Result { + match envstr.to_lowercase().as_str() { + "paypal" => Ok(Self::PayPal), + "stripe" => Ok(Self::Stripe), + "smth" => Ok(Self::SomethingElse), + _ => Err("Unsupported payment platform"), + } + } + } + + env::set_var("TEST_CASE_VEC_12", "paypal,stripe"); + let something: Option> = maybe_get_vec_env("TEST_CASE_VEC_12", SEP); + assert_eq!( + something, + Some(vec![PaymentPlatform::PayPal, PaymentPlatform::Stripe]) + ); + } +} diff --git a/itconfig/src/lib.rs b/itconfig/src/lib.rs index 54d8cfd..e0f1dfa 100644 --- a/itconfig/src/lib.rs +++ b/itconfig/src/lib.rs @@ -149,11 +149,14 @@ mod envstr; mod error; -mod getenv; +mod get_env; +mod get_vec_env; +pub(crate) mod utils; pub use self::envstr::*; pub use self::error::*; -pub use self::getenv::*; +pub use self::get_env::*; +pub use self::get_vec_env::*; #[cfg(feature = "macro")] extern crate itconfig_macro; diff --git a/itconfig/src/utils.rs b/itconfig/src/utils.rs new file mode 100644 index 0000000..9e42a11 --- /dev/null +++ b/itconfig/src/utils.rs @@ -0,0 +1,13 @@ +use crate::{EnvError, EnvString, FromEnvString}; + +pub(crate) fn parse_env_variable(env_name: &str, env_str: EnvString) -> Result +where + T: FromEnvString, +{ + FromEnvString::from_env_string(&env_str) + .map_err(|_| EnvError::FailedToParse(env_name.to_string())) +} + +pub(crate) fn make_panic(e: EnvError) -> T { + panic!("{}", e) +}