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 27, 2023
1 parent 84b5146 commit 14c12a9
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* Environment variable can be automatically derived from a field name (e.g. `db_host` will be tried to loaded from `DB_HOST` env var)
* Add install step in README

#### v0.10.2 - 2023-04-27
* Add envconfig annotation for top-level (non-nested) structs with `env_prefix` attribute

#### v0.10.1 - 2023-05-27
* Upgrade `syn` crate dependency to 2.0.18 (from 1.0.17)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

```rust
[dependencies]
envconfig = "0.10.1"
envconfig = "0.10.2"
```

## Usage
Expand Down
4 changes: 2 additions & 2 deletions envconfig/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "envconfig"
version = "0.10.1"
version = "0.10.2"
authors = ["Sergey Potapov <[email protected]>"]
description = "Build a config structure from environment variables without boilerplate."
categories = ["config", "web-programming"]
Expand All @@ -15,4 +15,4 @@ edition = "2018"
[dev-dependencies]

[dependencies]
envconfig_derive = { version = "0.10.1", path = "../envconfig_derive" }
envconfig_derive = { version = "0.10.2", path = "../envconfig_derive" }
2 changes: 1 addition & 1 deletion envconfig_derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "envconfig_derive"
version = "0.10.1"
version = "0.10.2"
authors = ["Sergey Potapov <[email protected]>"]
description = "Build a config structure from environment variables without boilerplate."
categories = ["config", "web-programming"]
Expand Down
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 14c12a9

Please sign in to comment.