From 5d6b335a7ae2911cac36a11f94135112ec5d473f Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 22 Apr 2021 02:11:04 +0300 Subject: [PATCH 1/4] 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) +} From 70b670b07c477700f13a89bf4422e4e26daaa7c2 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 22 Apr 2021 11:41:46 +0300 Subject: [PATCH 2/4] feat: add custom separator attribute --- itconfig-macro/src/ast.rs | 2 ++ itconfig-macro/src/expand.rs | 18 ++++++++-------- itconfig-macro/src/parse.rs | 12 ++++++++++- itconfig-macro/src/utils.rs | 6 +++--- itconfig-tests/Cargo.toml | 2 +- itconfig-tests/tests/config_macro.rs | 25 +++++++++++++++++++++- itconfig/src/get_vec_env.rs | 32 ++++++---------------------- itconfig/src/utils.rs | 22 ++++++++++++++++++- 8 files changed, 78 insertions(+), 41 deletions(-) diff --git a/itconfig-macro/src/ast.rs b/itconfig-macro/src/ast.rs index 98bfec8..4425e34 100644 --- a/itconfig-macro/src/ast.rs +++ b/itconfig-macro/src/ast.rs @@ -1,3 +1,4 @@ +use crate::utils::SupportedBox; use proc_macro2::TokenStream as TokenStream2; use syn::{Attribute, Expr, Ident, Type}; @@ -24,4 +25,5 @@ pub struct Variable { pub concat_parts: Option>, pub env_name: Option, pub meta: Vec, + pub supported_box: Option, } diff --git a/itconfig-macro/src/expand.rs b/itconfig-macro/src/expand.rs index c284628..29eb1d8 100644 --- a/itconfig-macro/src/expand.rs +++ b/itconfig-macro/src/expand.rs @@ -1,5 +1,5 @@ use crate::ast::*; -use crate::utils::{maybe_supported_box, vec_to_token_stream_2, SupportedBox}; +use crate::utils::{vec_to_token_stream_2, SupportedBox}; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; @@ -128,8 +128,6 @@ 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! {{ @@ -139,19 +137,21 @@ impl ToTokens for Variable { value }} } else if let Some(initial) = &self.initial { - match supported_box { - Some(SupportedBox::Vec) => { - quote!(::itconfig::get_vec_env_or_set_default(#env_name, ",", #initial)) + match self.supported_box.clone() { + Some(SupportedBox::Vec { sep }) => { + let sep = &sep.unwrap_or_else(|| String::from(",")); + quote!(::itconfig::get_vec_env_or_set_default(#env_name, #sep, #initial)) } _ => quote!(::itconfig::get_env_or_set_default(#env_name, #initial)), } } else { - match supported_box { + match self.supported_box.clone() { Some(SupportedBox::Option) => { quote!(::itconfig::maybe_get_env(#env_name)) } - Some(SupportedBox::Vec) => { - quote!(::itconfig::get_vec_env_or_panic(#env_name, ",")) + Some(SupportedBox::Vec { sep }) => { + let sep = &sep.unwrap_or_else(|| String::from(",")); + quote!(::itconfig::get_vec_env_or_panic(#env_name, #sep)) } _ => { quote!(::itconfig::get_env_or_panic(#env_name)) diff --git a/itconfig-macro/src/parse.rs b/itconfig-macro/src/parse.rs index 9a1f923..9551de4 100644 --- a/itconfig-macro/src/parse.rs +++ b/itconfig-macro/src/parse.rs @@ -1,4 +1,5 @@ use crate::ast::*; +use crate::utils::{maybe_supported_box, SupportedBox}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::quote; use syn::ext::IdentExt; @@ -73,7 +74,13 @@ fn parse_namespace_content( if attr.path.is_ident("env_name") { variable.env_name = parse_attribute(attr, "env_name", &variable.env_name)?; } else { - variable.meta.push(attr); + match variable.supported_box { + Some(SupportedBox::Vec { sep: current_sep }) if attr.path.is_ident("sep") => { + let sep = parse_attribute(attr, "sep", ¤t_sep)?; + variable.supported_box = Some(SupportedBox::Vec { sep }); + } + _ => variable.meta.push(attr), + } } } @@ -220,6 +227,8 @@ impl Parse for Variable { parse_str("&'static str")? }; + let supported_box = maybe_supported_box(&ty); + if is_concat { input.parse::()?; @@ -263,6 +272,7 @@ impl Parse for Variable { input.parse::().ok(); Ok(Variable { + supported_box, is_static, name, ty, diff --git a/itconfig-macro/src/utils.rs b/itconfig-macro/src/utils.rs index ebc0fa2..6aa7812 100644 --- a/itconfig-macro/src/utils.rs +++ b/itconfig-macro/src/utils.rs @@ -5,9 +5,9 @@ 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)] +#[derive(Debug, Clone)] pub enum SupportedBox { - Vec, + Vec { sep: Option }, Option, } @@ -44,7 +44,7 @@ pub fn maybe_supported_box(ty: &Type) -> Option { if is_option_path_ident(&path_ident) { Some(SupportedBox::Option) } else if is_vec_path_ident(&path_ident) { - Some(SupportedBox::Vec) + Some(SupportedBox::Vec { sep: None }) } else { None } diff --git a/itconfig-tests/Cargo.toml b/itconfig-tests/Cargo.toml index be42141..7a72838 100644 --- a/itconfig-tests/Cargo.toml +++ b/itconfig-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "itconfig_tests" -version = "0.1.1" +version = "0.1.0" authors = ["Dmitriy Pleshevskiy "] edition = "2018" license = "MIT" diff --git a/itconfig-tests/tests/config_macro.rs b/itconfig-tests/tests/config_macro.rs index 551328d..233e2fe 100644 --- a/itconfig-tests/tests/config_macro.rs +++ b/itconfig-tests/tests/config_macro.rs @@ -506,6 +506,7 @@ mod test_case_22 { "static ", STATIC_CONCAT_PART => "part", ), + static STATIC_VEC: Vec => vec![1], } #[test] @@ -528,7 +529,8 @@ mod test_case_22 { assert_eq!(config::STATIC_USIZE(), 1); assert_eq!(config::STATIC_F32(), 1.0); assert_eq!(config::STATIC_F64(), 1.0); - assert_eq!(config::STATIC_CONCAT_VARIABLE(), "static part".to_string()) + assert_eq!(config::STATIC_CONCAT_VARIABLE(), "static part".to_string()); + assert_eq!(config::STATIC_VEC(), vec![1]); } } @@ -573,3 +575,24 @@ mod test_case_24 { assert_eq!(config::STD_VEC(), vec!["paypal", "stripe"]); } } + +mod test_case_25 { + use std::env; + + itconfig::config! { + #[sep = ";"] + CUSTOM_SEP_MY_VEC: Vec<&'static str>, + + #[env_name = "CUSTOM_SEP_MY_VEC"] + #[sep = ";"] + CUSTOM_SEP_STD_VEC: std::vec::Vec<&'static str>, + } + + #[test] + fn custom_separator_for_vector() { + env::set_var("CUSTOM_SEP_MY_VEC", "paypal;stripe"); + + assert_eq!(config::CUSTOM_SEP_MY_VEC(), vec!["paypal", "stripe"]); + assert_eq!(config::CUSTOM_SEP_STD_VEC(), vec!["paypal", "stripe"]); + } +} diff --git a/itconfig/src/get_vec_env.rs b/itconfig/src/get_vec_env.rs index c045c5c..9096fc5 100644 --- a/itconfig/src/get_vec_env.rs +++ b/itconfig/src/get_vec_env.rs @@ -86,7 +86,7 @@ where /// } /// ``` /// -pub fn get_vec_env(env_name: &str, sep: &'static str) -> Result, EnvError> +pub fn get_vec_env(env_name: &str, sep: &str) -> Result, EnvError> where T: FromEnvString, { @@ -116,15 +116,12 @@ where /// } /// ``` /// -pub fn get_vec_env_or_default(env_name: &str, sep: &'static str, default: Vec) -> Vec +pub fn get_vec_env_or_default(env_name: &str, sep: &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) + get_vec_env_or(env_name, sep, |_| Ok(vec_to_env_strings(default))).unwrap_or_else(make_panic) } /// This function is similar as `get_vec_env_or_default`, but the default value will be set to environment @@ -151,29 +148,14 @@ where /// } /// ``` /// -pub fn get_vec_env_or_set_default( - env_name: &str, - sep: &'static str, - default: Vec, -) -> Vec +pub fn get_vec_env_or_set_default(env_name: &str, sep: &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 - }); + let default_env_strings = vec_to_env_strings(default); + let env_val = join(&default_env_strings, sep); env::set_var(env_name, env_val.as_str()); Ok(default_env_strings) }) @@ -182,7 +164,7 @@ where /// 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> +pub fn get_vec_env_or(env_name: &str, sep: &str, cb: F) -> Result, EnvError> where T: FromEnvString, F: FnOnce(env::VarError) -> Result, EnvError>, diff --git a/itconfig/src/utils.rs b/itconfig/src/utils.rs index 9e42a11..441beed 100644 --- a/itconfig/src/utils.rs +++ b/itconfig/src/utils.rs @@ -1,4 +1,4 @@ -use crate::{EnvError, EnvString, FromEnvString}; +use crate::{EnvError, EnvString, FromEnvString, ToEnvString}; pub(crate) fn parse_env_variable(env_name: &str, env_str: EnvString) -> Result where @@ -11,3 +11,23 @@ where pub(crate) fn make_panic(e: EnvError) -> T { panic!("{}", e) } + +pub(crate) fn join(env_strings: &[EnvString], sep: &str) -> String { + env_strings + .iter() + .enumerate() + .fold(String::new(), |mut res, (i, item)| { + if i > 0 { + res.push_str(sep); + } + res.push_str(&item); + res + }) +} + +pub(crate) fn vec_to_env_strings(values: Vec) -> Vec +where + T: ToEnvString, +{ + values.into_iter().map(EnvString::from).collect() +} From c21f265eea6ded15bbaf0ba8a7d34393dfdd51e5 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 22 Apr 2021 23:14:48 +0300 Subject: [PATCH 3/4] feat: add support optional vec --- itconfig-macro/src/expand.rs | 10 +++++++--- itconfig-macro/src/parse.rs | 4 ++-- itconfig-macro/src/utils.rs | 28 +++++++++++++++++++++------- itconfig-tests/tests/config_macro.rs | 15 +++++++++++++++ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/itconfig-macro/src/expand.rs b/itconfig-macro/src/expand.rs index 29eb1d8..3aa9dae 100644 --- a/itconfig-macro/src/expand.rs +++ b/itconfig-macro/src/expand.rs @@ -138,7 +138,7 @@ impl ToTokens for Variable { }} } else if let Some(initial) = &self.initial { match self.supported_box.clone() { - Some(SupportedBox::Vec { sep }) => { + Some(SupportedBox::Vec(sep)) => { let sep = &sep.unwrap_or_else(|| String::from(",")); quote!(::itconfig::get_vec_env_or_set_default(#env_name, #sep, #initial)) } @@ -149,11 +149,15 @@ impl ToTokens for Variable { Some(SupportedBox::Option) => { quote!(::itconfig::maybe_get_env(#env_name)) } - Some(SupportedBox::Vec { sep }) => { + Some(SupportedBox::OptionVec(sep)) => { + let sep = &sep.unwrap_or_else(|| String::from(",")); + quote!(::itconfig::maybe_get_vec_env(#env_name, #sep)) + } + Some(SupportedBox::Vec(sep)) => { let sep = &sep.unwrap_or_else(|| String::from(",")); quote!(::itconfig::get_vec_env_or_panic(#env_name, #sep)) } - _ => { + None => { quote!(::itconfig::get_env_or_panic(#env_name)) } } diff --git a/itconfig-macro/src/parse.rs b/itconfig-macro/src/parse.rs index 9551de4..9755b1a 100644 --- a/itconfig-macro/src/parse.rs +++ b/itconfig-macro/src/parse.rs @@ -75,9 +75,9 @@ fn parse_namespace_content( variable.env_name = parse_attribute(attr, "env_name", &variable.env_name)?; } else { match variable.supported_box { - Some(SupportedBox::Vec { sep: current_sep }) if attr.path.is_ident("sep") => { + Some(SupportedBox::Vec(current_sep)) if attr.path.is_ident("sep") => { let sep = parse_attribute(attr, "sep", ¤t_sep)?; - variable.supported_box = Some(SupportedBox::Vec { sep }); + variable.supported_box = Some(SupportedBox::Vec(sep)); } _ => variable.meta.push(attr), } diff --git a/itconfig-macro/src/utils.rs b/itconfig-macro/src/utils.rs index 6aa7812..1c4d71a 100644 --- a/itconfig-macro/src/utils.rs +++ b/itconfig-macro/src/utils.rs @@ -1,14 +1,15 @@ use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; -use syn::{Path, Type}; +use syn::{GenericArgument, Path, PathArguments, Type}; const OPTION_PATH_IDENTS: &[&str] = &["Option|", "std|option|Option|", "core|option|Option|"]; const VEC_PATH_IDENTS: &[&str] = &["Vec|", "std|vec|Vec|"]; #[derive(Debug, Clone)] pub enum SupportedBox { - Vec { sep: Option }, + Vec(Option), Option, + OptionVec(Option), } pub fn vec_to_token_stream_2(input: &[T]) -> Vec @@ -40,11 +41,24 @@ fn is_vec_path_ident(path_ident: &str) -> bool { pub fn maybe_supported_box(ty: &Type) -> Option { match ty { 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 { sep: None }) + let ty_path_ident = path_ident(&ty_path.path); + if is_option_path_ident(&ty_path_ident) { + match &ty_path.path.segments.iter().last().unwrap().arguments { + PathArguments::AngleBracketed(params) => match params.args.first() { + Some(GenericArgument::Type(Type::Path(inner_ty_path))) => { + let ty_path_ident = path_ident(&inner_ty_path.path); + if is_vec_path_ident(&ty_path_ident) { + Some(SupportedBox::OptionVec(None)) + } else { + Some(SupportedBox::Option) + } + } + _ => Some(SupportedBox::Option), + }, + _ => Some(SupportedBox::Option), + } + } else if is_vec_path_ident(&ty_path_ident) { + Some(SupportedBox::Vec(None)) } else { None } diff --git a/itconfig-tests/tests/config_macro.rs b/itconfig-tests/tests/config_macro.rs index 233e2fe..e521d85 100644 --- a/itconfig-tests/tests/config_macro.rs +++ b/itconfig-tests/tests/config_macro.rs @@ -596,3 +596,18 @@ mod test_case_25 { assert_eq!(config::CUSTOM_SEP_STD_VEC(), vec!["paypal", "stripe"]); } } + +mod test_case_26 { + use std::env; + + itconfig::config! { + OPTION_VEC: Option>, + } + + #[test] + fn optional_vec() { + env::set_var("OPTION_VEC", "paypal,stripe"); + + assert_eq!(config::OPTION_VEC(), Some(vec!["paypal", "stripe"])); + } +} From 13e200eedfc7f4b1ef924cc7cd3c81f27402c095 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 22 Apr 2021 23:39:32 +0300 Subject: [PATCH 4/4] refac: some cosmetic changes --- itconfig-macro/src/ast.rs | 6 ++-- itconfig-macro/src/expand.rs | 12 ++++---- itconfig-macro/src/parse.rs | 23 +++++++------- itconfig-macro/src/utils.rs | 60 ++++++++++++++++++++++++------------ 4 files changed, 60 insertions(+), 41 deletions(-) diff --git a/itconfig-macro/src/ast.rs b/itconfig-macro/src/ast.rs index 4425e34..e3ea887 100644 --- a/itconfig-macro/src/ast.rs +++ b/itconfig-macro/src/ast.rs @@ -2,14 +2,14 @@ use crate::utils::SupportedBox; use proc_macro2::TokenStream as TokenStream2; use syn::{Attribute, Expr, Ident, Type}; -pub struct RootNamespace { +pub(crate) struct RootNamespace { pub name: Option, pub variables: Vec, pub namespaces: Vec, pub meta: Vec, } -pub struct Namespace { +pub(crate) struct Namespace { pub name: Ident, pub variables: Vec, pub namespaces: Vec, @@ -17,7 +17,7 @@ pub struct Namespace { pub meta: Vec, } -pub struct Variable { +pub(crate) struct Variable { pub is_static: bool, pub name: Ident, pub ty: Type, diff --git a/itconfig-macro/src/expand.rs b/itconfig-macro/src/expand.rs index 3aa9dae..6894aac 100644 --- a/itconfig-macro/src/expand.rs +++ b/itconfig-macro/src/expand.rs @@ -138,8 +138,8 @@ impl ToTokens for Variable { }} } else if let Some(initial) = &self.initial { match self.supported_box.clone() { - Some(SupportedBox::Vec(sep)) => { - let sep = &sep.unwrap_or_else(|| String::from(",")); + Some(SupportedBox::Vec(params)) => { + let sep = ¶ms.sep(); quote!(::itconfig::get_vec_env_or_set_default(#env_name, #sep, #initial)) } _ => quote!(::itconfig::get_env_or_set_default(#env_name, #initial)), @@ -149,12 +149,12 @@ impl ToTokens for Variable { Some(SupportedBox::Option) => { quote!(::itconfig::maybe_get_env(#env_name)) } - Some(SupportedBox::OptionVec(sep)) => { - let sep = &sep.unwrap_or_else(|| String::from(",")); + Some(SupportedBox::OptionVec(params)) => { + let sep = ¶ms.sep(); quote!(::itconfig::maybe_get_vec_env(#env_name, #sep)) } - Some(SupportedBox::Vec(sep)) => { - let sep = &sep.unwrap_or_else(|| String::from(",")); + Some(SupportedBox::Vec(params)) => { + let sep = ¶ms.sep(); quote!(::itconfig::get_vec_env_or_panic(#env_name, #sep)) } None => { diff --git a/itconfig-macro/src/parse.rs b/itconfig-macro/src/parse.rs index 9755b1a..7440da3 100644 --- a/itconfig-macro/src/parse.rs +++ b/itconfig-macro/src/parse.rs @@ -1,5 +1,5 @@ use crate::ast::*; -use crate::utils::{maybe_supported_box, SupportedBox}; +use crate::utils::{maybe_supported_box, SupportedBox, VecBoxParams}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::quote; use syn::ext::IdentExt; @@ -60,7 +60,8 @@ fn parse_namespace_content( for attr in attributes { if attr.path.is_ident("env_prefix") { - namespace.env_prefix = parse_attribute(attr, "env_prefix", &namespace.env_prefix)?; + let env_prefix = parse_attribute(attr, "env_prefix", &namespace.env_prefix)?; + namespace.env_prefix = Some(env_prefix); } else { namespace.meta.push(attr); } @@ -72,12 +73,14 @@ fn parse_namespace_content( for attr in attributes { if attr.path.is_ident("env_name") { - variable.env_name = parse_attribute(attr, "env_name", &variable.env_name)?; + let env_name = parse_attribute(attr, "env_name", &variable.env_name)?; + variable.env_name = Some(env_name); } else { match variable.supported_box { - Some(SupportedBox::Vec(current_sep)) if attr.path.is_ident("sep") => { - let sep = parse_attribute(attr, "sep", ¤t_sep)?; - variable.supported_box = Some(SupportedBox::Vec(sep)); + Some(SupportedBox::Vec(params)) if attr.path.is_ident("sep") => { + let sep = parse_attribute(attr, "sep", ¶ms.sep_opt())?; + variable.supported_box = + Some(SupportedBox::Vec(VecBoxParams::new(Some(sep)))); } _ => variable.meta.push(attr), } @@ -90,11 +93,7 @@ fn parse_namespace_content( Ok(()) } -fn parse_attribute( - attr: Attribute, - name: &'static str, - var: &Option, -) -> Result> { +fn parse_attribute(attr: Attribute, name: &'static str, var: &Option) -> Result { if var.is_some() { let message = format!("You cannot use {} meta twice", &name); return Err(Error::new_spanned(attr, message)); @@ -104,7 +103,7 @@ fn parse_attribute( Meta::NameValue(MetaNameValue { lit: Lit::Str(lit_str), .. - }) => Ok(Some(lit_str.value())), + }) => Ok(lit_str.value()), _ => { let message = format!("expected #[{} = \"...\"]", &name); Err(Error::new_spanned(attr, message)) diff --git a/itconfig-macro/src/utils.rs b/itconfig-macro/src/utils.rs index 1c4d71a..fe62e95 100644 --- a/itconfig-macro/src/utils.rs +++ b/itconfig-macro/src/utils.rs @@ -5,14 +5,34 @@ use syn::{GenericArgument, Path, PathArguments, Type}; const OPTION_PATH_IDENTS: &[&str] = &["Option|", "std|option|Option|", "core|option|Option|"]; const VEC_PATH_IDENTS: &[&str] = &["Vec|", "std|vec|Vec|"]; -#[derive(Debug, Clone)] -pub enum SupportedBox { - Vec(Option), - Option, - OptionVec(Option), +#[derive(Debug, Clone, Default)] +pub(crate) struct VecBoxParams(Option); + +impl VecBoxParams { + #[inline] + pub(crate) fn new(sep: Option) -> Self { + VecBoxParams(sep) + } + + #[inline] + pub(crate) fn sep_opt(&self) -> Option { + self.0.clone() + } + + #[inline] + pub(crate) fn sep(&self) -> String { + self.0.clone().unwrap_or_else(|| String::from(",")) + } } -pub fn vec_to_token_stream_2(input: &[T]) -> Vec +#[derive(Debug, Clone)] +pub(crate) enum SupportedBox { + Vec(VecBoxParams), + Option, + OptionVec(VecBoxParams), +} + +pub(crate) fn vec_to_token_stream_2(input: &[T]) -> Vec where T: ToTokens, { @@ -38,27 +58,27 @@ 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 { +pub(crate) fn maybe_supported_box(ty: &Type) -> Option { match ty { Type::Path(ty_path) if ty_path.qself.is_none() => { let ty_path_ident = path_ident(&ty_path.path); if is_option_path_ident(&ty_path_ident) { - match &ty_path.path.segments.iter().last().unwrap().arguments { - PathArguments::AngleBracketed(params) => match params.args.first() { - Some(GenericArgument::Type(Type::Path(inner_ty_path))) => { - let ty_path_ident = path_ident(&inner_ty_path.path); - if is_vec_path_ident(&ty_path_ident) { - Some(SupportedBox::OptionVec(None)) - } else { - Some(SupportedBox::Option) - } + if let PathArguments::AngleBracketed(params) = + &ty_path.path.segments.iter().last().unwrap().arguments + { + if let Some(GenericArgument::Type(Type::Path(inner_ty_path))) = + params.args.first() + { + let ty_path_ident = path_ident(&inner_ty_path.path); + if is_vec_path_ident(&ty_path_ident) { + return Some(SupportedBox::OptionVec(Default::default())); } - _ => Some(SupportedBox::Option), - }, - _ => Some(SupportedBox::Option), + } } + + Some(SupportedBox::Option) } else if is_vec_path_ident(&ty_path_ident) { - Some(SupportedBox::Vec(None)) + Some(SupportedBox::Vec(Default::default())) } else { None }