diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f7ef4..0c7246e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 916cb66..41c8d7c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ```rust [dependencies] -envconfig = "0.10.1" +envconfig = "0.10.2" ``` ## Usage diff --git a/envconfig/Cargo.toml b/envconfig/Cargo.toml index e702e10..5adc9b2 100644 --- a/envconfig/Cargo.toml +++ b/envconfig/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "envconfig" -version = "0.10.1" +version = "0.10.2" authors = ["Sergey Potapov "] description = "Build a config structure from environment variables without boilerplate." categories = ["config", "web-programming"] @@ -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" } diff --git a/envconfig_derive/Cargo.toml b/envconfig_derive/Cargo.toml index 3dce6e3..4dd45fe 100644 --- a/envconfig_derive/Cargo.toml +++ b/envconfig_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "envconfig_derive" -version = "0.10.1" +version = "0.10.2" authors = ["Sergey Potapov "] description = "Build a config structure from environment variables without boilerplate." categories = ["config", "web-programming"] diff --git a/envconfig_derive/src/lib.rs b/envconfig_derive/src/lib.rs index 2d47aac..babecd1 100644 --- a/envconfig_derive/src/lib.rs +++ b/envconfig_derive/src/lib.rs @@ -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 { @@ -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 = 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"), @@ -37,13 +55,14 @@ fn impl_envconfig(input: &DeriveInput) -> proc_macro2::TokenStream { fn impl_envconfig_for_struct( struct_name: &Ident, fields: &Punctuated, + 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 { @@ -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 @@ -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) -> 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( diff --git a/test_suite/tests/env_prefix.rs b/test_suite/tests/env_prefix.rs new file mode 100644 index 0000000..40a04fa --- /dev/null +++ b/test_suite/tests/env_prefix.rs @@ -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); +}