Skip to content
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,39 @@ pub struct Config {
}
```

### Nested configs with prefix

You can also nest configs with a prefix, which will strip the prefix from environment variables before passing them to the nested config. This is useful for grouping related environment variables.

```rust
#[derive(Envconfig)]
pub struct DbConfig {
#[envconfig(from = "HOST")] // Will use DB_HOST from environment
pub host: String,

#[envconfig(from = "PORT", default = "5432")] // Will use DB_PORT from environment
pub port: u16,
}

#[derive(Envconfig)]
pub struct CacheConfig {
#[envconfig(from = "HOST")] // Will use CACHE_HOST from environment
pub host: String,

#[envconfig(from = "PORT", default = "6379")] // Will use CACHE_PORT from environment
pub port: u16,
}

#[derive(Envconfig)]
pub struct Config {
#[envconfig(nested, prefix = "DB_")] // <---
db: DbConfig,

#[envconfig(nested, prefix = "CACHE_")] // <---
cache: CacheConfig,
}
```


### Custom types

Expand Down
34 changes: 34 additions & 0 deletions envconfig/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,40 @@
//! assert_eq!(config.http_port, 8080);
//! ```
//!
//! ## Nested Configuration with Prefixes
//!
//! You can also nest configuration structures with prefixes:
//!
//! ```
//! use std::env;
//! use envconfig::Envconfig;
//!
//! #[derive(Envconfig)]
//! struct DbConfig {
//! #[envconfig(from = "HOST")] // Will look for DB_HOST
//! pub host: String,
//!
//! #[envconfig(from = "PORT")] // Will look for DB_PORT
//! pub port: u16,
//! }
//!
//! #[derive(Envconfig)]
//! struct Config {
//! #[envconfig(nested, prefix = "DB_")]
//! pub db: DbConfig,
//! }
//!
//! // Set environment variables with prefixes
//! env::set_var("DB_HOST", "localhost");
//! env::set_var("DB_PORT", "5432");
//!
//! // Initialize config from environment variables
//! let config = Config::init_from_env().unwrap();
//!
//! assert_eq!(config.db.host, "localhost");
//! assert_eq!(config.db.port, 5432);
//! ```
//!
//! The library uses `std::str::FromStr` trait to convert environment variables into custom
//! data type. So, if your data type does not implement `std::str::FromStr` the program
//! will not compile.
Expand Down
69 changes: 59 additions & 10 deletions envconfig_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,16 @@ fn gen_field_assign(field: &Field, source: &Source) -> proc_macro2::TokenStream
// If nested attribute is present
let nested_value_opt = find_item_in_list(&list, "nested");
match nested_value_opt {
Some(MatchingItem::NoValue) => return gen_field_assign_for_struct_type(field, source),
Some(MatchingItem::NoValue) => {
// Check if there's a prefix attribute
let prefix_opt = find_item_in_list(&list, "prefix");
let prefix = match prefix_opt {
Some(MatchingItem::WithValue(prefix_lit)) => Some(prefix_lit),
Some(MatchingItem::NoValue) => panic!("`prefix` attribute must have a value"),
None => None,
};
return gen_field_assign_for_struct_type(field, source, prefix);
}
Some(MatchingItem::WithValue(_)) => {
panic!("`nested` attribute must not have a value")
}
Expand Down Expand Up @@ -157,17 +166,57 @@ fn gen(
/// Generates the derived field assignment for a (nested) struct type
///
/// # Panics
/// Panics if the field type is not a path
fn gen_field_assign_for_struct_type(field: &Field, source: &Source) -> proc_macro2::TokenStream {
/// Panics if the field type is not a path or if the prefix is not a string literal
fn gen_field_assign_for_struct_type(field: &Field, source: &Source, prefix: Option<&Lit>) -> proc_macro2::TokenStream {
let ident: &Option<Ident> = &field.ident;

match &field.ty {
syn::Type::Path(path) => match source {
Source::Environment => quote! {
#ident: #path :: init_from_env()?
},
Source::HashMap => quote! {
#ident: #path :: init_from_hashmap(hashmap)?
},
syn::Type::Path(path) => {
// Use pattern matching to handle the prefix
let Some(prefix_lit) = prefix else {
// If there's no prefix, use the simple form
return match source {
Source::Environment => quote! {
#ident: #path :: init_from_env()?
},
Source::HashMap => quote! {
#ident: #path :: init_from_hashmap(hashmap)?
},
};
};

// Otherwise, process with prefix
let prefix_str = match prefix_lit {
Lit::Str(s) => s.value(),
_ => panic!("Expected prefix to be a string literal"),
};

match source {
Source::Environment => quote! {
#ident: {
// Create a new hashmap with prefixed environment variables
let mut prefixed_env = ::std::collections::HashMap::new();
for (key, value) in ::std::env::vars() {
if let Some(suffix) = key.strip_prefix(#prefix_str) {
prefixed_env.insert(suffix.to_string(), value);
}
}
#path :: init_from_hashmap(&prefixed_env)?
}
},
Source::HashMap => quote! {
#ident: {
// Create a new hashmap with prefixed variables
let mut prefixed_map = ::std::collections::HashMap::new();
for (key, value) in hashmap.iter() {
if key.starts_with(#prefix_str) {
prefixed_map.insert(key[#prefix_str.len()..].to_string(), value.clone());
}
}
#path :: init_from_hashmap(&prefixed_map)?
}
},
}
},
_ => panic!("Expected field type to be a path: {ident:?}",),
}
Expand Down
145 changes: 145 additions & 0 deletions test_suite/tests/nested_prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
extern crate envconfig;

use envconfig::{Envconfig, Error};
use std::collections::HashMap;
use std::env;

#[derive(Envconfig)]
pub struct DBConfig {
#[envconfig(from = "HOST")]
pub host: String,
#[envconfig(from = "PORT")]
pub port: u16,
}

#[derive(Envconfig)]
pub struct Config {
#[envconfig(nested, prefix = "DB_")]
pub db: DBConfig,

#[envconfig(nested, prefix = "CACHE_")]
pub cache: CacheConfig,
}

#[derive(Envconfig)]
pub struct CacheConfig {
#[envconfig(from = "HOST")]
pub host: String,

#[envconfig(from = "PORT", default = "6379")]
pub port: u16,
}

#[derive(Envconfig)]
pub struct MultiConfig {
#[envconfig(nested, prefix = "DB1_")]
pub db1: DBConfig,

#[envconfig(nested, prefix = "DB2_")]
pub db2: DBConfig,
}

fn setup() {
// Clean up all environment variables we might use in tests
env::remove_var("DB_HOST");
env::remove_var("DB_PORT");
env::remove_var("CACHE_HOST");
env::remove_var("CACHE_PORT");
env::remove_var("DB1_HOST");
env::remove_var("DB1_PORT");
env::remove_var("DB2_HOST");
env::remove_var("DB2_PORT");
}

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

env::set_var("DB_HOST", "localhost");
env::set_var("DB_PORT", "5432");
env::set_var("CACHE_HOST", "redis");
// CACHE_PORT uses default value

let config = Config::init_from_env().unwrap();
assert_eq!(config.db.host, "localhost");
assert_eq!(config.db.port, 5432u16);
assert_eq!(config.cache.host, "redis");
assert_eq!(config.cache.port, 6379u16);
}

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

let mut hashmap = HashMap::new();
hashmap.insert("DB_HOST".to_string(), "localhost".to_string());
hashmap.insert("DB_PORT".to_string(), "5432".to_string());
hashmap.insert("CACHE_HOST".to_string(), "redis".to_string());
// CACHE_PORT uses default value

let config = Config::init_from_hashmap(&hashmap).unwrap();
assert_eq!(config.db.host, "localhost");
assert_eq!(config.db.port, 5432u16);
assert_eq!(config.cache.host, "redis");
assert_eq!(config.cache.port, 6379u16);
}

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

env::set_var("DB_HOST", "localhost");
env::set_var("CACHE_HOST", "redis");
// DB_PORT is missing

let err = Config::init_from_env().err().unwrap();
let expected_err = Error::EnvVarMissing { name: "PORT" };
assert_eq!(err, expected_err);
}

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

let mut hashmap = HashMap::new();
hashmap.insert("DB_HOST".to_string(), "localhost".to_string());
hashmap.insert("CACHE_HOST".to_string(), "redis".to_string());
// DB_PORT is missing

let err = Config::init_from_hashmap(&hashmap).err().unwrap();
let expected_err = Error::EnvVarMissing { name: "PORT" };
assert_eq!(err, expected_err);
}

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

env::set_var("DB1_HOST", "primary");
env::set_var("DB1_PORT", "5432");
env::set_var("DB2_HOST", "replica");
env::set_var("DB2_PORT", "5433");

let config = MultiConfig::init_from_env().unwrap();
assert_eq!(config.db1.host, "primary");
assert_eq!(config.db1.port, 5432u16);
assert_eq!(config.db2.host, "replica");
assert_eq!(config.db2.port, 5433u16);
}

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

let mut hashmap = HashMap::new();
hashmap.insert("DB1_HOST".to_string(), "primary".to_string());
hashmap.insert("DB1_PORT".to_string(), "5432".to_string());
hashmap.insert("DB2_HOST".to_string(), "replica".to_string());
hashmap.insert("DB2_PORT".to_string(), "5433".to_string());

let config = MultiConfig::init_from_hashmap(&hashmap).unwrap();
assert_eq!(config.db1.host, "primary");
assert_eq!(config.db1.port, 5432u16);
assert_eq!(config.db2.host, "replica");
assert_eq!(config.db2.port, 5433u16);
}