Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add envconfig annotation for non-nested structs + env_prefix attribute #43

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions envconfig_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ edition = "2018"
proc-macro = true

[dependencies]
syn = "1.0.17"
quote = "1.0.3"
proc-macro2 = "1.0.9"
syn = { version = "2.0.18", features = ["parsing"] }
quote = "1.0.27"
proc-macro2 = "1.0.58"
135 changes: 72 additions & 63 deletions envconfig_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{Attribute, DeriveInput, Field, Fields, Ident, Lit, Meta, NestedMeta};
use syn::{Attribute, DeriveInput, Field, Fields, Ident, Meta, Lit, Expr, ExprLit};

#[proc_macro_derive(Envconfig, attributes(envconfig))]
pub fn derive(input: TokenStream) -> TokenStream {
Expand All @@ -23,9 +23,27 @@ fn impl_envconfig(input: &DeriveInput) -> proc_macro2::TokenStream {
use syn::Data::*;
let struct_name = &input.ident;

let input_attr = fetch_envconfig_attr_from_attrs(&input.attrs);
let input_ident_opt: Option<Ident> = Some(input.ident.clone());
let env_prefix = match input_attr {
Some(ref attr) => find_item_in_attr_meta(&input_ident_opt, attr, "env_prefix"),
None => None,
};

let env_prefix_str = match env_prefix {
Some(Expr::Lit(ref prefix)) => {
match &prefix.lit {
Lit::Str(lit_prefix) => lit_prefix.value(),
_ => panic!("Expected `env_prefix` value to be a literal string"),
}
},
None => "".to_string(),
_ => panic!("Expected `env_prefix` value to be a literal string"),
};

let inner_impl = match input.data {
Struct(ref ds) => match ds.fields {
Fields::Named(ref fields) => impl_envconfig_for_struct(struct_name, &fields.named),
Fields::Named(ref fields) => impl_envconfig_for_struct(struct_name, &fields.named, env_prefix_str),
_ => panic!("envconfig supports only named fields"),
},
_ => panic!("envconfig only supports non-tuple structs"),
Expand All @@ -37,13 +55,14 @@ fn impl_envconfig(input: &DeriveInput) -> proc_macro2::TokenStream {
fn impl_envconfig_for_struct(
struct_name: &Ident,
fields: &Punctuated<Field, Comma>,
env_prefix: String,
) -> proc_macro2::TokenStream {
let field_assigns_env = fields
.iter()
.map(|field| gen_field_assign(field, Source::Environment));
.map(|field| gen_field_assign(field, Source::Environment, &env_prefix));
let field_assigns_hashmap = fields
.iter()
.map(|field| gen_field_assign(field, Source::HashMap));
.map(|field| gen_field_assign(field, Source::HashMap, &env_prefix));

quote! {
impl Envconfig for #struct_name {
Expand All @@ -68,44 +87,60 @@ fn impl_envconfig_for_struct(
}
}

fn gen_field_assign(field: &Field, source: Source) -> proc_macro2::TokenStream {
let attr = fetch_envconfig_attr_from_field(field);
fn gen_field_assign(field: &Field, source: Source, env_prefix: &str) -> proc_macro2::TokenStream {
let attr = fetch_envconfig_attr_from_attrs(&field.attrs);

if let Some(attr) = attr {
// if #[envconfig(...)] is there
let list = fetch_list_from_attr(field, attr);

// If nested attribute is present
let nested_value_opt = find_item_in_list(field, &list, "nested");
let nested_value_opt = find_item_in_attr_meta(&field.ident, &attr, "nested");
if nested_value_opt.is_some() {
return gen_field_assign_for_struct_type(field, source);
}

let opt_default = find_item_in_list(field, &list, "default");
let opt_default = find_item_in_attr_meta(&field.ident, &attr, "default");
let from_opt = find_item_in_attr_meta(&field.ident, &attr, "from");

let from_opt = find_item_in_list(field, &list, "from");
let env_var = match from_opt {
Some(v) => quote! { #v },
None => field_to_env_var(field),
let env_var_name: String = match from_opt {
Some(Expr::Lit(v)) => format!("{}{}", env_prefix, get_exprlit_str_value(&v, &field.ident)),
None => field_to_env_var(field, env_prefix),
_ => panic!(
"Expected '{}' field option 'from' type to be a String",
to_s(&field.ident)
),
};
let env_var = quote! { #env_var_name };

gen(field, env_var, opt_default, source)
} else {
// if #[envconfig(...)] is not present
let env_var = field_to_env_var(field);
let env_var_name = field_to_env_var(field, env_prefix);
let env_var = quote! { #env_var_name };
gen(field, env_var, None, source)
}
}

fn field_to_env_var(field: &Field) -> proc_macro2::TokenStream {
let field_name = field.clone().ident.unwrap().to_string().to_uppercase();
quote! { #field_name }
fn get_exprlit_str_value(exprlit_obj: &ExprLit, field_ident: &Option<Ident>) -> String {
match &exprlit_obj.lit {
Lit::Str(lit_prefix) => lit_prefix.value(),
_ => panic!(
"Expected '{}' field option 'from' type to be a String",
to_s(field_ident)
),
}
}


fn field_to_env_var(field: &Field, env_prefix: &str) -> String {
let ident_name = field.clone().ident.unwrap().to_string().to_uppercase();
format!("{}{}", env_prefix, ident_name)
}

fn gen(
field: &Field,
from: proc_macro2::TokenStream,
opt_default: Option<&Lit>,
opt_default: Option<Expr>,
source: Source,
) -> proc_macro2::TokenStream {
let field_type = &field.ty;
Expand Down Expand Up @@ -134,7 +169,7 @@ fn gen_field_assign_for_struct_type(field: &Field, source: Source) -> proc_macro
fn gen_field_assign_for_optional_type(
field: &Field,
from: proc_macro2::TokenStream,
opt_default: Option<&Lit>,
opt_default: Option<Expr>,
source: Source,
) -> proc_macro2::TokenStream {
let field_name = &field.ident;
Expand All @@ -156,7 +191,7 @@ fn gen_field_assign_for_optional_type(
fn gen_field_assign_for_non_optional_type(
field: &Field,
from: proc_macro2::TokenStream,
opt_default: Option<&Lit>,
opt_default: Option<Expr>,
source: Source,
) -> proc_macro2::TokenStream {
let field_name = &field.ident;
Expand All @@ -182,57 +217,31 @@ fn gen_field_assign_for_non_optional_type(
}
}

fn fetch_envconfig_attr_from_field(field: &Field) -> Option<&Attribute> {
field.attrs.iter().find(|a| {
let path = &a.path;
fn fetch_envconfig_attr_from_attrs(attrs: &Vec<Attribute>) -> Option<&Attribute> {
attrs.iter().find(|a| {
let path = &a.path();
let name = quote!(#path).to_string();
name == "envconfig"
})
}

fn fetch_list_from_attr(field: &Field, attr: &Attribute) -> Punctuated<NestedMeta, Comma> {
let opt_meta = attr.parse_meta().unwrap_or_else(|err| {
panic!(
"Can not interpret meta of `envconfig` attribute on field `{}`: {}",
field_name(field),
err
)
});

match opt_meta {
Meta::List(l) => l.nested,
_ => panic!(
"`envconfig` attribute on field `{}` must contain a list",
field_name(field)
),
}
}

fn find_item_in_list<'f, 'l, 'n>(
field: &'f Field,
list: &'l Punctuated<NestedMeta, Comma>,
item_name: &'n str,
) -> Option<&'l Lit> {
list.iter()
.map(|item| match item {
NestedMeta::Meta(meta) => match meta {
Meta::NameValue(name_value) => name_value,
_ => panic!(
fn find_item_in_attr_meta<'n>(attr_parent_ident: &Option<Ident>, attr: &Attribute, item_name: &'n str) -> Option<Expr> {
let nested = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated).ok()?;
for meta in nested {
if meta.path().is_ident(item_name) {
match meta.require_name_value() {
Ok(m) => {
return Some(m.value.clone());
},
Err(_) => panic!(
"`envconfig` attribute on field `{}` must contain name/value item",
field_name(field)
to_s(attr_parent_ident)
),
},
_ => panic!(
"Failed to process `envconfig` attribute on field `{}`",
field_name(field)
),
})
.find(|name_value| name_value.path.is_ident(item_name))
.map(|item| &item.lit)
}
}
}
}

fn field_name(field: &Field) -> String {
to_s(&field.ident)
None
}

fn to_s<T: quote::ToTokens>(node: &T) -> String {
Expand Down
52 changes: 52 additions & 0 deletions test_suite/tests/env_prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
extern crate envconfig;

use envconfig::{Envconfig};
use std::env;

#[derive(Envconfig)]
#[envconfig(env_prefix = "TEST_")]
pub struct ConfigWithoutFrom {
pub db_host: String,
pub db_port: u16,
}

fn setup() {
env::remove_var("TEST_DB_HOST");
env::remove_var("TEST_DB_PORT");
}

#[test]
fn test_init_from_env_with_env_prefix_and_no_envconfig_on_attributes() {
setup();

env::set_var("TEST_DB_HOST", "localhost");
env::set_var("TEST_DB_PORT", "5432");


let config = ConfigWithoutFrom::init_from_env().unwrap();
assert_eq!(config.db_host, "localhost");
assert_eq!(config.db_port, 5432u16);
}

#[derive(Envconfig)]
#[envconfig(env_prefix = "TEST_")]
pub struct ConfigWithFrom {
#[envconfig(from = "DB_HOST")]
pub db_host: String,

#[envconfig(from = "DB_PORT")]
pub db_port: u16,
}

#[test]
fn test_init_from_env_with_env_prefix_and_envconfig_on_attributes() {
setup();

env::set_var("TEST_DB_HOST", "localhost");
env::set_var("TEST_DB_PORT", "5433");


let config = ConfigWithFrom::init_from_env().unwrap();
assert_eq!(config.db_host, "localhost");
assert_eq!(config.db_port, 5433u16);
}