Skip to content

Commit

Permalink
feat: Add "env_prefix" attribute for struct derive
Browse files Browse the repository at this point in the history
This attribute allows to add an annotation like so `#[envconfig(env_prefix = "...")]`
on top of a struct. The `env_prefix` will be applied to every config
attribute in the struct.
  • Loading branch information
Greesb committed May 28, 2023
1 parent ef1aff6 commit 50d1dce
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 13 deletions.
62 changes: 49 additions & 13 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, Meta, Expr};
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,8 +87,9 @@ fn impl_envconfig_for_struct(
}
}

fn gen_field_assign(field: &Field, source: Source) -> proc_macro2::TokenStream {
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

Expand All @@ -80,25 +100,41 @@ fn gen_field_assign(field: &Field, source: Source) -> proc_macro2::TokenStream {
}

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 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(
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);
}

0 comments on commit 50d1dce

Please sign in to comment.