diff --git a/itconfig-macro/src/ast.rs b/itconfig-macro/src/ast.rs index 98bfec8..e3ea887 100644 --- a/itconfig-macro/src/ast.rs +++ b/itconfig-macro/src/ast.rs @@ -1,14 +1,15 @@ +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, @@ -16,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, @@ -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 7008088..6894aac 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::{vec_to_token_stream_2, SupportedBox}; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; @@ -136,13 +136,31 @@ 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 self.supported_box.clone() { + 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)), + } } else { - quote!(::itconfig::get_env_or_panic(#env_name)) + match self.supported_box.clone() { + Some(SupportedBox::Option) => { + quote!(::itconfig::maybe_get_env(#env_name)) + } + Some(SupportedBox::OptionVec(params)) => { + let sep = ¶ms.sep(); + quote!(::itconfig::maybe_get_vec_env(#env_name, #sep)) + } + Some(SupportedBox::Vec(params)) => { + let sep = ¶ms.sep(); + quote!(::itconfig::get_vec_env_or_panic(#env_name, #sep)) + } + None => { + quote!(::itconfig::get_env_or_panic(#env_name)) + } + } }; if self.is_static { diff --git a/itconfig-macro/src/parse.rs b/itconfig-macro/src/parse.rs index 9a1f923..7440da3 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, VecBoxParams}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::quote; use syn::ext::IdentExt; @@ -59,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); } @@ -71,9 +73,17 @@ 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 { - variable.meta.push(attr); + match variable.supported_box { + 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), + } } } @@ -83,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)); @@ -97,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)) @@ -220,6 +226,8 @@ impl Parse for Variable { parse_str("&'static str")? }; + let supported_box = maybe_supported_box(&ty); + if is_concat { input.parse::()?; @@ -263,6 +271,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 3d03212..fe62e95 100644 --- a/itconfig-macro/src/utils.rs +++ b/itconfig-macro/src/utils.rs @@ -1,10 +1,38 @@ 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|"]; -pub fn vec_to_token_stream_2(input: &[T]) -> Vec +#[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(",")) + } +} + +#[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, { @@ -22,15 +50,39 @@ 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(crate) 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 ty_path_ident = path_ident(&ty_path.path); + if is_option_path_ident(&ty_path_ident) { + 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) + } else if is_vec_path_ident(&ty_path_ident) { + Some(SupportedBox::Vec(Default::default())) + } else { + None + } } - _ => false, + _ => None, } } diff --git a/itconfig-tests/tests/config_macro.rs b/itconfig-tests/tests/config_macro.rs index 8cf4b75..e521d85 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]); } } @@ -547,8 +549,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 +557,57 @@ 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"]); + } +} + +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"]); + } +} + +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"])); + } +} 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..9096fc5 --- /dev/null +++ b/itconfig/src/get_vec_env.rs @@ -0,0 +1,304 @@ +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: &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: &str, default: Vec) -> Vec +where + T: FromEnvString, + D: ToEnvString, +{ + 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 +/// 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: &str, default: Vec) -> Vec +where + T: FromEnvString, + D: ToEnvString, +{ + get_vec_env_or(env_name, sep, |_| { + 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) + }) + .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: &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..441beed --- /dev/null +++ b/itconfig/src/utils.rs @@ -0,0 +1,33 @@ +use crate::{EnvError, EnvString, FromEnvString, ToEnvString}; + +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) +} + +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() +}