Merge pull request #22 from icetemple/optional-env

Optional env
This commit is contained in:
Dmitriy Pleshevskiy 2021-04-16 00:24:01 +02:00 committed by GitHub
commit e398fec00f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 196 additions and 90 deletions

View file

@ -1,14 +1,8 @@
use crate::ast::*;
use crate::utils::{is_option_type, vec_to_token_stream_2};
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
fn vec_to_token_stream_2<T>(input: &Vec<T>) -> Vec<TokenStream2>
where
T: ToTokens,
{
input.iter().map(|ns| ns.into_token_stream()).collect()
}
impl ToTokens for RootNamespace {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
@ -131,7 +125,7 @@ impl ToTokens for Variable {
let env_name = &self
.env_name
.clone()
.unwrap_or(name.to_string().to_uppercase());
.unwrap_or_else(|| name.to_string().to_uppercase());
let meta = vec_to_token_stream_2(&self.meta);
let get_variable: TokenStream2 = if self.concat_parts.is_some() {
@ -145,6 +139,8 @@ impl ToTokens for Variable {
} 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 {
quote!(::itconfig::get_env_or_panic(#env_name))
};

View file

@ -1,12 +1,15 @@
#![recursion_limit = "256"]
#![deny(clippy::all)]
#![forbid(unsafe_code)]
mod ast;
mod expand;
mod parse;
mod utils;
extern crate proc_macro;
extern crate proc_macro2;
use self::proc_macro::TokenStream;
use ast::RootNamespace;
use quote::ToTokens;

View file

@ -117,7 +117,7 @@ impl Parse for RootNamespace {
match attr.parse_meta()? {
Meta::List(MetaList { nested, .. }) => {
let message =
format!("expected #[config(name = \"...\")] or #[config(unwrap)]");
"expected #[config(name = \"...\")] or #[config(unwrap)]".to_string();
match nested.first().unwrap() {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
@ -127,7 +127,7 @@ impl Parse for RootNamespace {
if path.is_ident("name") {
name = Some(Ident::new(&lit_str.value(), Span::call_site()));
} else {
Err(Error::new_spanned(attr, message))?;
return Err(Error::new_spanned(attr, message));
}
}
NestedMeta::Meta(Meta::Path(path)) => {
@ -135,17 +135,17 @@ impl Parse for RootNamespace {
name = None;
with_module = false;
} else {
Err(Error::new_spanned(attr, message))?;
return Err(Error::new_spanned(attr, message));
}
}
_ => {
Err(Error::new_spanned(attr, message))?;
return Err(Error::new_spanned(attr, message));
}
}
}
_ => {
let message = format!("expected #[config(...)]");
Err(Error::new_spanned(attr, message))?;
let message = "expected #[config(...)]".to_string();
return Err(Error::new_spanned(attr, message));
}
}
} else {
@ -166,7 +166,7 @@ impl Parse for RootNamespace {
let prefix = String::new();
let namespaces = namespaces
.into_iter()
.map(fill_env_prefix(prefix.clone()))
.map(fill_env_prefix(prefix))
.collect();
Ok(RootNamespace {
@ -231,7 +231,10 @@ impl Parse for Variable {
if content.peek(Ident::peek_any) {
let concat_var: Variable = content.parse()?;
let name = &concat_var.name;
let env_name = &concat_var.env_name.clone().unwrap_or(name.to_string());
let env_name = &concat_var
.env_name
.clone()
.unwrap_or_else(|| name.to_string());
let get_variable = if concat_var.initial.is_some() {
let initial = concat_var.initial.as_ref().unwrap();
@ -245,8 +248,10 @@ impl Parse for Variable {
let part: Lit = content.parse()?;
tmp_vec.push(quote!(#part.to_string()));
}
content.parse::<Comma>().ok();
}
concat_parts = Some(tmp_vec);
} else {
initial = input

View file

@ -0,0 +1,36 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::{Path, Type};
const OPTION_PATH_IDENTS: &[&str] = &["Option|", "std|option|Option|", "core|option|Option|"];
pub fn vec_to_token_stream_2<T>(input: &[T]) -> Vec<TokenStream2>
where
T: ToTokens,
{
input.iter().map(|ns| ns.into_token_stream()).collect()
}
fn path_ident(path: &Path) -> String {
path.segments
.iter()
.into_iter()
.fold(String::with_capacity(250), |mut acc, v| {
acc.push_str(&v.ident.to_string());
acc.push('|');
acc
})
}
fn is_option_path_ident(path_ident: String) -> bool {
OPTION_PATH_IDENTS.iter().any(|s| path_ident == *s)
}
pub fn is_option_type(ty: &Type) -> bool {
match ty {
Type::Path(ty_path) => {
ty_path.qself.is_none() && is_option_path_ident(path_ident(&ty_path.path))
}
_ => false,
}
}

View file

@ -531,3 +531,29 @@ mod test_case_22 {
assert_eq!(config::STATIC_CONCAT_VARIABLE(), "static part".to_string())
}
}
mod test_case_23 {
use std::env;
itconfig::config! {
SOMETHING: Option<&'static str>,
#[env_name = "SOMETHING"]
STD_SOMETHING: std::option::Option<&'static str>,
#[env_name = "SOMETHING"]
CORE_SOMETHING: core::option::Option<&'static str>,
NOTHING: Option<&'static str>,
}
#[test]
fn optional_variables() {
config::init();
env::set_var("SOMETHING", "hello world");
assert_eq!(config::SOMETHING(), Some("hello world"));
assert_eq!(config::STD_SOMETHING(), Some("hello world"));
assert_eq!(config::CORE_SOMETHING(), Some("hello world"));
assert_eq!(config::NOTHING(), None);
}
}

View file

@ -1,74 +0,0 @@
use itconfig::EnvError::*;
use itconfig::*;
use std::env;
#[test]
#[should_panic(expected = "Environment variable \"TEST_CASE_1\" is missing")]
fn get_missing_env() {
get_env_or_panic::<String>("TEST_CASE_1");
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_2\"")]
fn get_env_with_invalid_value() {
let env_name = "TEST_CASE_2";
env::set_var(&env_name, "30r");
get_env_or_panic::<u32>(env_name);
}
#[test]
fn get_result_of_missing_env() {
let env_name = String::from("TEST_CASE_3");
let env_val = get_env::<String>(&env_name);
assert_eq!(env_val, Err(MissingVariable(env_name)))
}
#[test]
fn get_result_of_env_with_invalid_value() {
let env_name = String::from("TEST_CASE_4");
env::set_var(&env_name, "30r");
let env_val = get_env::<u32>(&env_name);
assert_eq!(env_val, Err(FailedToParse(env_name)))
}
#[test]
fn get_result_of_env_successfully() {
env::set_var("TEST_CASE_5", "30");
let env_var = get_env("TEST_CASE_5");
assert_eq!(env_var, Ok(30));
}
#[test]
fn get_missing_env_with_default_value() {
let flag: bool = get_env_or_default("TEST_CASE_6", "true");
assert_eq!(flag, true);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_7\"")]
fn get_invalid_env_with_default_value() {
env::set_var("TEST_CASE_7", "30r");
get_env_or_default::<u32, _>("TEST_CASE_7", 30);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_8\"")]
fn get_env_with_invalid_default_value() {
get_env_or_default::<u32, _>("TEST_CASE_8", "30r");
}
#[test]
fn get_env_with_default_successfully() {
env::set_var("TEST_CASE_9", "10");
let env_val: u32 = get_env_or_default("TEST_CASE_9", 30);
assert_eq!(env_val, 10)
}
#[test]
fn get_missing_env_with_set_default_value() {
let flag: bool = get_env_or_set_default("TEST_CASE_10", "true");
assert_eq!(flag, true);
let env_var = env::var("TEST_CASE_10");
assert_eq!(env_var, Ok(String::from("true")))
}

View file

@ -1,6 +1,34 @@
use crate::prelude::*;
use std::env;
/// Same as get_env but returns Option enum instead Result
///
/// Example
/// -------
///
/// ```rust
/// # extern crate itconfig;
/// # use itconfig::maybe_get_env;
/// use std::env;
///
/// fn main () {
/// env::set_var("HOST", "https://example.com");
///
/// let host: Option<&'static str> = maybe_get_env("HOST");
/// let not_existence_host: Option<&'static str> = maybe_get_env("NOT_EXISTENCE_HOST");
///
/// assert_eq!(host, Some("https://example.com"));
/// assert_eq!(not_existence_host, None);
/// }
/// ```
///
pub fn maybe_get_env<T>(env_name: &str) -> Option<T>
where
T: FromEnvString,
{
get_env(env_name).ok()
}
/// This function is similar as `get_env`, but it unwraps result with panic on error.
///
/// Panics
@ -137,3 +165,89 @@ where
fn make_panic<T>(e: EnvError) -> T {
panic!("{}", e)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Environment variable \"TEST_CASE_1\" is missing")]
fn get_missing_env() {
get_env_or_panic::<String>("TEST_CASE_1");
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_2\"")]
fn get_env_with_invalid_value() {
let env_name = "TEST_CASE_2";
env::set_var(&env_name, "30r");
get_env_or_panic::<u32>(env_name);
}
#[test]
fn get_result_of_missing_env() {
let env_name = String::from("TEST_CASE_3");
let env_val = get_env::<String>(&env_name);
assert_eq!(env_val, Err(EnvError::MissingVariable(env_name)))
}
#[test]
fn get_result_of_env_with_invalid_value() {
let env_name = String::from("TEST_CASE_4");
env::set_var(&env_name, "30r");
let env_val = get_env::<u32>(&env_name);
assert_eq!(env_val, Err(EnvError::FailedToParse(env_name)))
}
#[test]
fn get_result_of_env_successfully() {
env::set_var("TEST_CASE_5", "30");
let env_var = get_env("TEST_CASE_5");
assert_eq!(env_var, Ok(30));
}
#[test]
fn get_missing_env_with_default_value() {
let flag: bool = get_env_or_default("TEST_CASE_6", "true");
assert_eq!(flag, true);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_7\"")]
fn get_invalid_env_with_default_value() {
env::set_var("TEST_CASE_7", "30r");
get_env_or_default::<u32, _>("TEST_CASE_7", 30);
}
#[test]
#[should_panic(expected = "Failed to parse environment variable \"TEST_CASE_8\"")]
fn get_env_with_invalid_default_value() {
get_env_or_default::<u32, _>("TEST_CASE_8", "30r");
}
#[test]
fn get_env_with_default_successfully() {
env::set_var("TEST_CASE_9", "10");
let env_val: u32 = get_env_or_default("TEST_CASE_9", 30);
assert_eq!(env_val, 10)
}
#[test]
fn get_missing_env_with_set_default_value() {
let flag: bool = get_env_or_set_default("TEST_CASE_10", "true");
assert_eq!(flag, true);
let env_var = env::var("TEST_CASE_10");
assert_eq!(env_var, Ok(String::from("true")))
}
#[test]
fn get_optional_env() {
env::set_var("TEST_CASE_11", "something");
let something: Option<&'static str> = maybe_get_env("TEST_CASE_11");
assert_eq!(something, Some("something"));
let nothing: Option<&'static str> = maybe_get_env("TEST_CASE_11_NONE");
assert_eq!(nothing, None);
}
}