├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── ci └── script.sh ├── envconfig ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── error.rs │ ├── lib.rs │ ├── traits.rs │ └── utils.rs ├── envconfig_derive ├── Cargo.toml ├── LICENSE ├── README.md └── src │ └── lib.rs ├── logo ├── envconfig.svg └── logo_draft.svg └── test_suite ├── Cargo.toml ├── src └── main.rs └── tests ├── basic.rs ├── default.rs ├── default_env_var.rs ├── nested.rs └── option.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | **/*.rs.bk 4 | *.sw[po] 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | install: 5 | - rustup component add rustfmt-preview 6 | - rustup component add clippy-preview 7 | script: 8 | - ./ci/script.sh 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### v0.11.0 - 2024-09-07 2 | * [breaking] Nested config fields now needs to be marked with #[envconfig(nested)] 3 | * Environment variable can be automatically derived from a field name (e.g. `db_host` will be tried to loaded from `DB_HOST` env var) 4 | * Add install step in README 5 | * Update `syn` dependency to 2.x.x 6 | 7 | #### v0.10.0 - 2021-06-8 8 | * Add `init_from_hashmap` to initialize config from `HashMap` in unit tests 9 | 10 | #### v0.9.1 - 2019-10-09 11 | * Get rid of thiserror dependency 12 | 13 | #### v0.9.0 - 2019-10-05 14 | * Use rust edition 2018 15 | * Make envconfig re-export Envconfig macro from envconfig_derive (no need to use envconfig_derive explicitly anymore) 16 | * Deprecate `init()` function in favor of `init_from_env()` 17 | 18 | #### v0.8.0 - 2019-03-31 19 | * Update `syn`, `quote` and `proc-macro2` dependencies to be `1.x.x` 20 | 21 | #### v0.7.0 - 2019-02-20 22 | * Use `thiserror` for errors 23 | 24 | #### v0.6.0 - 2019-12-22 25 | * Support nested structures 26 | 27 | #### v0.5.1 - 2019-04-14 28 | * Fix `Result` overlapping bug 29 | 30 | #### v0.5.0 - 2018-09-25 31 | * Support `default` attribute to specify default values 32 | 33 | #### v0.4.0 - 2018-09-22 34 | * Support of `Option` types 35 | * Rewrite `envconfig_derive` to use the latests versions of `syn` and `quote` crates 36 | * Improve error messages on panics 37 | * Add `skeptic` to generate tests based on README code examples 38 | 39 | #### v0.3.0 - 2018-09-16 40 | * [breaking] Use `envconfig` attribute instead of `from` in the derive macro 41 | * [breaking] Remove init_or_die() function from Envconfig trait 42 | * [breaking] In envconfig_derive: rename function envconfig() -> derive() 43 | * [improvement] Add better documentation to the crate 44 | 45 | #### v0.2.0 - 2018-09-13 46 | * [breaking] Use derive macro instead of macro_rules 47 | 48 | #### v0.1.0 - 2018-08-18 49 | * First public release 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["envconfig", "envconfig_derive", "test_suite"] 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | 8 | [workspace.lints.rust] 9 | unsafe_code = "forbid" 10 | 11 | [workspace.lints.clippy] 12 | pedantic = "warn" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2018 Sergey Potapov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test -- --test-threads=1 3 | watch: 4 | cargo watch -s "cargo test -- --test-threads=1" 5 | clean: 6 | rm -rf ./target 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Envconfig logo

2 | 3 |

4 | Build Status 5 | License 6 | Documentation 7 |

8 | 9 |

Initialize config structure from environment variables in Rust without boilerplate.

10 | 11 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua/) 12 | 13 | ## Install 14 | 15 | ```rust 16 | [dependencies] 17 | envconfig = "0.10.0" 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### Basic example 23 | 24 | Let's say you application relies on the following environment variables: 25 | 26 | * `DB_HOST` 27 | * `DB_PORT` 28 | 29 | And you want to initialize `Config` structure like this one: 30 | 31 | ```rust,ignore 32 | struct Config { 33 | db_host: String, 34 | db_port: u16, 35 | } 36 | ``` 37 | 38 | You can achieve this with the following code without boilerplate: 39 | 40 | ```rust 41 | use envconfig::Envconfig; 42 | 43 | #[derive(Envconfig)] 44 | pub struct Config { 45 | #[envconfig(from = "DB_HOST")] 46 | pub db_host: String, 47 | 48 | #[envconfig(from = "DB_PORT", default = "5432")] 49 | pub db_port: u16, 50 | } 51 | 52 | fn main() { 53 | // Assuming the following environment variables are set 54 | std::env::set_var("DB_HOST", "127.0.0.1"); 55 | 56 | // Initialize config from environment variables or terminate the process. 57 | let config = Config::init_from_env().unwrap(); 58 | 59 | assert_eq!(config.db_host, "127.0.0.1"); 60 | assert_eq!(config.db_port, 5432); 61 | } 62 | ``` 63 | 64 | ### Nested configs 65 | 66 | Configs can be nested. Just add `#[envconfig(nested)]` to nested field. 67 | 68 | ```rust 69 | #[derive(Envconfig)] 70 | pub struct DbConfig { 71 | #[envconfig(from = "DB_HOST")] 72 | pub host: String, 73 | 74 | #[envconfig(from = "DB_PORT", default = "5432")] 75 | pub port: u16, 76 | } 77 | 78 | #[derive(Envconfig)] 79 | pub struct Config { 80 | #[envconfig(nested)] // <--- 81 | db: DbConfig, 82 | 83 | #[envconfig(from = "HOSTNAME")] 84 | hostname: String, 85 | } 86 | ``` 87 | 88 | 89 | ### Custom types 90 | 91 | Under the hood envconfig relies on [`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html) trait. 92 | If you want to use a custom type as a field for config, you have to implement `FromStr` trait for your custom type. 93 | 94 | Let's say we want to extend `DbConfig` with `driver` field, which is `DbDriver` enum that represents either `Postgresql` or `Mysql`: 95 | 96 | ```rust 97 | pub enum DbDriver { 98 | Postgresql, 99 | Mysql, 100 | } 101 | 102 | impl std::str::FromStr for DbDriver { 103 | type Err = String; 104 | 105 | fn from_str(s: &str) -> Result { 106 | match s.trim().to_lowercase().as_ref() { 107 | "postgres" => Ok(DbDriver::Postgresql), 108 | "mysql" => Ok(DbDriver::Mysql), 109 | _ => Err(format!("Unknown DB driver: {s}")) 110 | } 111 | } 112 | } 113 | 114 | #[derive(Envconfig)] 115 | pub struct DbConfig { 116 | // ... 117 | #[envconfig(from = "DB_DRIVER")] 118 | pub driver: DbDriver, 119 | } 120 | ``` 121 | 122 | If this seems too cumbersome, consider using other crates like [strum](https://docs.rs/strum/latest/strum/) to derive `FromStr` automatically. 123 | 124 | ```rust 125 | use strum::EnumString; 126 | 127 | #[derive(EnumString)] 128 | pub enum DbDriver { 129 | Postgresql, 130 | Mysql, 131 | } 132 | ``` 133 | 134 | ## Testing 135 | 136 | When writing tests you should avoid using environment variables. Cargo runs Rust tests in parallel by default which means 137 | you can end up with race conditions in your tests if two or more are fighting over an environment variable. 138 | 139 | To solve this you can initialise your `struct` from a `HashMap` in your tests. The `HashMap` should 140 | match what you expect the real environment variables to be; for example `DB_HOST` environment variable becomes a 141 | `DB_HOST` key in your `HashMap`. 142 | 143 | ```rust 144 | use envconfig::Envconfig; 145 | 146 | #[derive(Envconfig)] 147 | pub struct Config { 148 | #[envconfig(from = "DB_HOST")] 149 | pub db_host: String, 150 | 151 | #[envconfig(from = "DB_PORT", default = "5432")] 152 | pub db_port: u16, 153 | } 154 | 155 | #[test] 156 | fn test_config_can_be_loaded_from_hashmap() { 157 | // Create a HashMap that looks like your environment 158 | let mut hashmap = HashMap::new(); 159 | hashmap.insert("DB_HOST".to_string(), "127.0.0.1".to_string()); 160 | 161 | // Initialize config from a HashMap to avoid test race conditions 162 | let config = Config::init_from_hashmap(&hashmap).unwrap(); 163 | 164 | assert_eq!(config.db_host, "127.0.0.1"); 165 | assert_eq!(config.db_port, 5432); 166 | } 167 | ``` 168 | 169 | ## Contributing 170 | 171 | ### Running tests 172 | 173 | Tests do some manipulation with environment variables, so to 174 | prevent flaky tests they have to be executed in a single thread: 175 | 176 | ``` 177 | cargo test -- --test-threads=1 178 | ``` 179 | 180 | ## License 181 | 182 | [MIT](https://github.com/greyblake/envconfig-rs/blob/master/LICENSE) © [Sergey Potapov](http://greyblake.com/) 183 | 184 | ## Contributors 185 | 186 | - [greyblake](https://github.com/greyblake) Potapov Sergey - creator, maintainer. 187 | - [allevo](https://github.com/allevo) Tommaso Allevi - support nested structures 188 | - [hobofan](https://github.com/hobofan) Maximilian Goisser - update dependencies 189 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make the entire script fail if one of the commands fails 4 | set -ex 5 | 6 | # Formatting 7 | cargo fmt -- --check 8 | 9 | # Clippy 10 | touch ./*/*/*.rs 11 | cargo clippy -- -W warnings 12 | 13 | # Tests 14 | cargo test -- --test-threads=1 15 | -------------------------------------------------------------------------------- /envconfig/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "envconfig" 3 | version = "0.11.0" 4 | authors = ["Serhii Potapov "] 5 | description = "Build a config structure from environment variables without boilerplate." 6 | categories = ["config", "web-programming"] 7 | keywords = ["config", "env", "macro", "configuration", "environment"] 8 | license = "MIT" 9 | repository = "https://github.com/greyblake/envconfig-rs" 10 | readme = "README.md" 11 | edition.workspace = true 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | envconfig_derive = { version = "0.11.0", path = "../envconfig_derive" } 18 | -------------------------------------------------------------------------------- /envconfig/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /envconfig/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /envconfig/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Errors resulting from calling functions in this crate 2 | 3 | use std::{error::Error as StdError, fmt}; 4 | 5 | /// Represents an error, that may be returned by `fn init_from_env()` of trait `Envconfig`. 6 | #[derive(Debug, PartialEq)] 7 | pub enum Error { 8 | EnvVarMissing { name: &'static str }, 9 | ParseError { name: &'static str }, 10 | } 11 | 12 | impl fmt::Display for Error { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | match self { 15 | Error::EnvVarMissing { name } => { 16 | write!(f, "Environment variable {name} is missing") 17 | } 18 | Error::ParseError { name } => { 19 | write!(f, "Failed to parse environment variable {name}") 20 | } 21 | } 22 | } 23 | } 24 | 25 | impl StdError for Error { 26 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 27 | None 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /envconfig/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Envconfig is a Rust library that helps to initialize configuration structure 2 | //! from environment variables. 3 | //! It makes use of custom derive macros to reduce boilerplate. 4 | //! 5 | //! Example 6 | //! 7 | //! ``` 8 | //! use std::env; 9 | //! use envconfig::Envconfig; 10 | //! 11 | //! #[derive(Envconfig)] 12 | //! struct Config { 13 | //! #[envconfig(from = "DB_HOST")] 14 | //! pub db_host: String, 15 | //! 16 | //! #[envconfig(from = "DB_PORT")] 17 | //! pub db_port: Option, 18 | //! 19 | //! #[envconfig(from = "HTTP_PORT", default = "8080")] 20 | //! pub http_port: u16, 21 | //! } 22 | //! 23 | //! // We assume that those environment variables are set somewhere outside 24 | //! env::set_var("DB_HOST", "localhost"); 25 | //! env::set_var("DB_PORT", "5432"); 26 | //! 27 | //! // Initialize config from environment variables 28 | //! let config = Config::init_from_env().unwrap(); 29 | //! 30 | //! assert_eq!(config.db_host, "localhost"); 31 | //! assert_eq!(config.db_port, Some(5432)); 32 | //! assert_eq!(config.http_port, 8080); 33 | //! ``` 34 | //! 35 | //! The library uses `std::str::FromStr` trait to convert environment variables into custom 36 | //! data type. So, if your data type does not implement `std::str::FromStr` the program 37 | //! will not compile. 38 | 39 | mod error; 40 | mod traits; 41 | mod utils; 42 | 43 | pub use error::Error; 44 | pub use traits::Envconfig; 45 | pub use utils::{load_optional_var, load_var, load_var_with_default}; 46 | 47 | // re-export derive 48 | pub use envconfig_derive::Envconfig; 49 | -------------------------------------------------------------------------------- /envconfig/src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use std::collections::HashMap; 3 | 4 | /// Indicates that structure can be initialize from environment variables. 5 | pub trait Envconfig { 6 | /// Initialize structure from environment variables. 7 | /// Deprecated in favor of [`::init_from_env()`]. 8 | /// 9 | /// # Errors 10 | /// - Environment variable is missing. 11 | /// - Failed to parse environment variable. 12 | #[deprecated( 13 | since = "0.9.0", 14 | note = "Function init() is deprecated. Please use init_from_env() instead." 15 | )] 16 | fn init() -> Result 17 | where 18 | Self: Sized; 19 | 20 | /// Initialize structure from environment variables. 21 | /// 22 | /// # Errors 23 | /// - Environment variable is missing. 24 | /// - Failed to parse environment variable. 25 | fn init_from_env() -> Result 26 | where 27 | Self: Sized; 28 | 29 | /// Initialize structure from a hashmap. 30 | /// 31 | /// # Errors 32 | /// - Environment variable is missing. 33 | /// - Failed to parse environment variable. 34 | fn init_from_hashmap(hashmap: &HashMap) -> Result 35 | where 36 | Self: Sized; 37 | } 38 | -------------------------------------------------------------------------------- /envconfig/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::str::FromStr; 3 | 4 | use crate::error::Error; 5 | use std::collections::HashMap; 6 | 7 | /// Load an environment variable by name and parse it into type `T`. 8 | /// 9 | /// This function can also use a hashmap as a fallback or for testing purposes. 10 | /// 11 | /// # Errors 12 | /// - Environment variable is not present 13 | /// - Parsing failed 14 | pub fn load_var( 15 | var_name: &'static str, 16 | hashmap: Option<&HashMap>, 17 | ) -> Result { 18 | match hashmap { 19 | None => env::var(var_name).ok(), 20 | Some(hashmap) => hashmap.get(var_name).map(std::string::ToString::to_string), 21 | } 22 | .ok_or(Error::EnvVarMissing { name: var_name }) 23 | .and_then(|string_value| { 24 | string_value 25 | .parse::() 26 | .map_err(|_| Error::ParseError { name: var_name }) 27 | }) 28 | } 29 | 30 | /// Tries to load an environment variable by name and parse it into type `T`. 31 | /// If the environment variable is not present, it returns a default value. 32 | /// 33 | /// This function can also use a hashmap as a fallback or for testing purposes. 34 | /// 35 | /// # Errors 36 | /// - Parsing failed 37 | pub fn load_var_with_default( 38 | var_name: &'static str, 39 | hashmap: Option<&HashMap>, 40 | default: &'static str, 41 | ) -> Result { 42 | let opt_var = match hashmap { 43 | None => env::var(var_name).ok(), 44 | Some(hashmap) => hashmap.get(var_name).map(std::string::ToString::to_string), 45 | }; 46 | 47 | let string_value = match opt_var { 48 | None => default, 49 | Some(ref value) => value, 50 | }; 51 | 52 | string_value 53 | .parse::() 54 | .map_err(|_| Error::ParseError { name: var_name }) 55 | } 56 | 57 | /// Tries to load an environment variable by name and parse it into type `T`. 58 | /// If the environment variable is not present, it returns `None`. 59 | /// 60 | /// This function can also use a hashmap as a fallback or for testing purposes. 61 | /// 62 | /// # Errors 63 | /// - Parsing failed 64 | pub fn load_optional_var( 65 | var_name: &'static str, 66 | hashmap: Option<&HashMap>, 67 | ) -> Result, Error> { 68 | let opt_var = match hashmap { 69 | None => env::var(var_name).ok(), 70 | Some(hashmap) => hashmap.get(var_name).map(std::string::ToString::to_string), 71 | }; 72 | 73 | match opt_var { 74 | None => Ok(None), 75 | Some(string_value) => string_value 76 | .parse::() 77 | .map(Some) 78 | .map_err(|_| Error::ParseError { name: var_name }), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /envconfig_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "envconfig_derive" 3 | version = "0.11.0" 4 | authors = ["Serhii Potapov "] 5 | description = "Build a config structure from environment variables without boilerplate." 6 | categories = ["config", "web-programming"] 7 | keywords = ["config", "env", "macro", "configuration", "environment"] 8 | license = "MIT" 9 | repository = "https://github.com/greyblake/envconfig-rs" 10 | readme = "README.md" 11 | edition.workspace = true 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [lib] 17 | proc-macro = true 18 | 19 | [dependencies] 20 | syn = { version = "2.0", features = ["parsing", "derive"] } 21 | quote = { version = "1.0", features = [] } 22 | proc-macro2 = { version = "1.0", features = [] } 23 | -------------------------------------------------------------------------------- /envconfig_derive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /envconfig_derive/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /envconfig_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Provides a derive macro that implements `Envconfig` trait. 2 | 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{ 6 | punctuated::Punctuated, token::Comma, Attribute, Data::Struct, DeriveInput, Expr, Field, 7 | Fields, Ident, Lit, Meta, MetaNameValue, Token, 8 | }; 9 | 10 | /// Custom derive for trait [`envconfig::Envconfig`] 11 | /// 12 | /// # Panics 13 | /// - The provided [`TokenStream`] cannot be parsed 14 | /// - The provided input is not a named struct 15 | /// - Invalid configuration in the `envconfig` attributes 16 | #[proc_macro_derive(Envconfig, attributes(envconfig))] 17 | pub fn derive(input: TokenStream) -> TokenStream { 18 | let derive_input: DeriveInput = syn::parse(input).unwrap(); 19 | let gen = impl_envconfig(&derive_input); 20 | gen.into() 21 | } 22 | 23 | /// Source type for envconfig variables. 24 | /// 25 | /// - `Environment`: Environment variables. 26 | /// - `HashMap`: [`std::collections::HashMap`] for mocking environments in tests. 27 | enum Source { 28 | Environment, 29 | HashMap, 30 | } 31 | 32 | /// Wrapper for [`impl_envconfig_for_struct`]. 33 | /// 34 | /// Checks if the provided input is a struct and generates the desired `impl EnvConfig` 35 | /// 36 | /// # Panics 37 | /// Panics if `input.data` isn't a struct 38 | fn impl_envconfig(input: &DeriveInput) -> proc_macro2::TokenStream { 39 | let struct_name = &input.ident; 40 | 41 | // Check if derive input is a struct and contains named fields. Panic otherwise 42 | let named_fields = match input.data { 43 | Struct(ref ds) => match ds.fields { 44 | Fields::Named(ref fields) => &fields.named, 45 | _ => panic!("envconfig supports only named fields"), 46 | }, 47 | _ => panic!("envconfig only supports non-tuple structs"), 48 | }; 49 | 50 | let inner_impl = impl_envconfig_for_struct(struct_name, named_fields); 51 | 52 | quote!(#inner_impl) 53 | } 54 | 55 | /// Generates the `impl Envconfig` blocks for the provided struct 56 | fn impl_envconfig_for_struct( 57 | struct_name: &Ident, 58 | fields: &Punctuated, 59 | ) -> proc_macro2::TokenStream { 60 | let field_assigns_env = fields 61 | .iter() 62 | .map(|field| gen_field_assign(field, &Source::Environment)); 63 | let field_assigns_hashmap = fields 64 | .iter() 65 | .map(|field| gen_field_assign(field, &Source::HashMap)); 66 | 67 | quote! { 68 | impl Envconfig for #struct_name { 69 | fn init_from_env() -> ::std::result::Result { 70 | let config = Self { 71 | #(#field_assigns_env,)* 72 | }; 73 | Ok(config) 74 | } 75 | 76 | fn init_from_hashmap(hashmap: &::std::collections::HashMap) -> ::std::result::Result { 77 | let config = Self { 78 | #(#field_assigns_hashmap,)* 79 | }; 80 | Ok(config) 81 | } 82 | 83 | #[deprecated(since="0.10.0", note="Please use `::init_from_env` instead")] 84 | fn init() -> ::std::result::Result { 85 | Self::init_from_env() 86 | } 87 | } 88 | } 89 | } 90 | 91 | /// Generates the field assignments for the config struct 92 | fn gen_field_assign(field: &Field, source: &Source) -> proc_macro2::TokenStream { 93 | let attr = fetch_envconfig_attr_from_field(field); 94 | 95 | if let Some(attr) = attr { 96 | // if #[envconfig(...)] is there 97 | let list = fetch_args_from_attr(field, attr); 98 | 99 | // If nested attribute is present 100 | let nested_value_opt = find_item_in_list(&list, "nested"); 101 | match nested_value_opt { 102 | Some(MatchingItem::NoValue) => return gen_field_assign_for_struct_type(field, source), 103 | Some(MatchingItem::WithValue(_)) => { 104 | panic!("`nested` attribute must not have a value") 105 | } 106 | None => {} 107 | } 108 | 109 | // Default value for the field 110 | let opt_default = match find_item_in_list(&list, "default") { 111 | Some(MatchingItem::WithValue(v)) => Some(v), 112 | Some(MatchingItem::NoValue) => panic!("`default` attribute must have a value"), 113 | None => None, 114 | }; 115 | 116 | // Environment variable name 117 | let from_opt = find_item_in_list(&list, "from"); 118 | let env_var = match from_opt { 119 | Some(MatchingItem::WithValue(v)) => quote! { #v }, 120 | Some(MatchingItem::NoValue) => panic!("`from` attribute must have a value"), 121 | None => field_to_env_var_name(field), 122 | }; 123 | 124 | gen(field, &env_var, opt_default, source) 125 | } else { 126 | // if #[envconfig(...)] is not present 127 | // use field name as name of the environment variable 128 | let env_var = field_to_env_var_name(field); 129 | gen(field, &env_var, None, source) 130 | } 131 | } 132 | 133 | /// Turns the field name into an uppercase [`proc_macro2::TokenStream`] 134 | /// 135 | /// # Panics 136 | /// Panics if the field does not have an identifier 137 | fn field_to_env_var_name(field: &Field) -> proc_macro2::TokenStream { 138 | let field_name = field.ident.clone().unwrap().to_string().to_uppercase(); 139 | quote! { #field_name } 140 | } 141 | 142 | /// Generates the derived field assignment for the provided field 143 | fn gen( 144 | field: &Field, 145 | from: &proc_macro2::TokenStream, 146 | opt_default: Option<&Lit>, 147 | source: &Source, 148 | ) -> proc_macro2::TokenStream { 149 | let field_type = &field.ty; 150 | if to_s(field_type).starts_with("Option ") { 151 | gen_field_assign_for_optional_type(field, from, opt_default, source) 152 | } else { 153 | gen_field_assign_for_non_optional_type(field, from, opt_default, source) 154 | } 155 | } 156 | 157 | /// Generates the derived field assignment for a (nested) struct type 158 | /// 159 | /// # Panics 160 | /// Panics if the field type is not a path 161 | fn gen_field_assign_for_struct_type(field: &Field, source: &Source) -> proc_macro2::TokenStream { 162 | let ident: &Option = &field.ident; 163 | match &field.ty { 164 | syn::Type::Path(path) => match source { 165 | Source::Environment => quote! { 166 | #ident: #path :: init_from_env()? 167 | }, 168 | Source::HashMap => quote! { 169 | #ident: #path :: init_from_hashmap(hashmap)? 170 | }, 171 | }, 172 | _ => panic!("Expected field type to be a path: {ident:?}",), 173 | } 174 | } 175 | 176 | /// Generates the derived field assignment for an optional type 177 | /// 178 | /// # Panics 179 | /// Panics if the field is an optional type with a default value 180 | fn gen_field_assign_for_optional_type( 181 | field: &Field, 182 | from: &proc_macro2::TokenStream, 183 | opt_default: Option<&Lit>, 184 | source: &Source, 185 | ) -> proc_macro2::TokenStream { 186 | let field_name = &field.ident; 187 | 188 | assert!(opt_default.is_none(), "Optional type on field `{}` with default value does not make sense and therefore is not allowed", to_s(field_name)); 189 | 190 | match source { 191 | Source::Environment => quote! { 192 | #field_name: ::envconfig::load_optional_var::<_,::std::collections::hash_map::RandomState>(#from, None)? 193 | }, 194 | Source::HashMap => quote! { 195 | #field_name: ::envconfig::load_optional_var::<_,::std::collections::hash_map::RandomState>(#from, Some(hashmap))? 196 | }, 197 | } 198 | } 199 | 200 | /// Generates the derived field assignment for non-optional types 201 | fn gen_field_assign_for_non_optional_type( 202 | field: &Field, 203 | from: &proc_macro2::TokenStream, 204 | opt_default: Option<&Lit>, 205 | source: &Source, 206 | ) -> proc_macro2::TokenStream { 207 | let field_name = &field.ident; 208 | 209 | if let Some(default) = opt_default { 210 | match source { 211 | Source::Environment => quote! { 212 | #field_name: ::envconfig::load_var_with_default::<_,::std::collections::hash_map::RandomState>(#from, None, #default)? 213 | }, 214 | Source::HashMap => quote! { 215 | #field_name: ::envconfig::load_var_with_default::<_,::std::collections::hash_map::RandomState>(#from, Some(hashmap), #default)? 216 | }, 217 | } 218 | } else { 219 | match source { 220 | Source::Environment => quote! { 221 | #field_name: ::envconfig::load_var::<_,::std::collections::hash_map::RandomState>(#from, None)? 222 | }, 223 | Source::HashMap => quote! { 224 | #field_name: ::envconfig::load_var::<_,::std::collections::hash_map::RandomState>(#from, Some(hashmap))? 225 | }, 226 | } 227 | } 228 | } 229 | 230 | /// Tries to get the (first) `envconfig` attribute from the provided field 231 | fn fetch_envconfig_attr_from_field(field: &Field) -> Option<&Attribute> { 232 | field.attrs.iter().find(|a| { 233 | let path = &a.path(); 234 | let name = quote!(#path).to_string(); 235 | name == "envconfig" 236 | }) 237 | } 238 | 239 | /// Fetches the arguments from the provided attribute 240 | /// 241 | /// # Panics 242 | /// Panics if the attribute cannot be parsed 243 | fn fetch_args_from_attr(field: &Field, attr: &Attribute) -> Vec { 244 | let opt_meta = &attr.meta; 245 | 246 | match opt_meta { 247 | Meta::List(l) => l 248 | .parse_args_with(Punctuated::::parse_terminated) 249 | .unwrap_or_else(|err| { 250 | panic!( 251 | "{:?} in `envconfig` attribute on field `{}`", 252 | err, 253 | field_name(field) 254 | ) 255 | }) 256 | .iter() 257 | .cloned() 258 | .collect(), 259 | _ => vec![opt_meta.clone()], 260 | } 261 | } 262 | 263 | /// Represents the result of a search for an item in a [`Meta`] list 264 | enum MatchingItem<'a> { 265 | WithValue(&'a Lit), 266 | NoValue, 267 | } 268 | 269 | /// Tries to find the first matching item in the provided list 270 | /// 271 | /// # Returns 272 | /// 273 | /// - `MatchingItem::WithValue(&Lit)` if a name-value pair is found 274 | /// - `MatchingItem::NoValue` if a path is found 275 | /// - `None` if no matching item is found 276 | /// 277 | /// # Panics 278 | /// 279 | /// - Multiple items with the same name exist 280 | /// - The item is not a name-value pair or a path 281 | fn find_item_in_list<'l>(list: &'l [Meta], item_name: &str) -> Option> { 282 | // Find all items with the provided name 283 | let matching_items = list 284 | .iter() 285 | .filter(|token_tree| token_tree.path().is_ident(item_name)) 286 | .collect::>(); 287 | 288 | // Check that there is at most one item with the provided name. Error otherwise 289 | assert!( 290 | matching_items.len() <= 1, 291 | "Found multiple `{item_name}` attributes in `envconfig` attribute", 292 | ); 293 | 294 | let matching_result = matching_items.first(); 295 | 296 | if let Some(meta) = matching_result { 297 | return match meta { 298 | Meta::NameValue(MetaNameValue { 299 | value: Expr::Lit(value), 300 | .. 301 | }) => Some(MatchingItem::WithValue(&value.lit)), 302 | Meta::Path(_) => Some(MatchingItem::NoValue), 303 | _ => panic!("Expected `{item_name}` to be a name-value pair or a path"), 304 | }; 305 | } 306 | 307 | None 308 | } 309 | 310 | /// Returns the name of the field as a string 311 | fn field_name(field: &Field) -> String { 312 | to_s(&field.ident) 313 | } 314 | 315 | /// Converts a [`quote::ToTokens`] to a string 316 | fn to_s(node: &T) -> String { 317 | quote!(#node).to_string() 318 | } 319 | -------------------------------------------------------------------------------- /logo/envconfig.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /logo/logo_draft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 57 | 62 | 67 | 72 | 80 | 90 | 100 | 105 | 112 | 117 | 122 | 128 | 133 | 139 | 146 | Envconfig 163 | 164 | 165 | -------------------------------------------------------------------------------- /test_suite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "envconfig_test_suite" 3 | version = "0.2.0" 4 | authors = ["Sergey Potapov "] 5 | edition.workspace = true 6 | 7 | [lints] 8 | workspace = true 9 | 10 | [dependencies] 11 | envconfig = { path = "../envconfig" } 12 | envconfig_derive = { path = "../envconfig_derive" } 13 | -------------------------------------------------------------------------------- /test_suite/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate envconfig; 2 | 3 | use envconfig::Envconfig; 4 | 5 | #[derive(Envconfig)] 6 | pub struct Config { 7 | pub port: u16, 8 | 9 | #[envconfig(from = "HOST")] 10 | pub host: String, 11 | } 12 | 13 | // Ensure custom Result can be defined in the current context. 14 | // See: https://github.com/greyblake/envconfig-rs/issues/21 15 | type Result = std::result::Result>; 16 | 17 | fn main() { 18 | let res: Result = Ok(123); 19 | println!("{res:?}"); 20 | } 21 | -------------------------------------------------------------------------------- /test_suite/tests/basic.rs: -------------------------------------------------------------------------------- 1 | extern crate envconfig; 2 | 3 | use envconfig::{Envconfig, Error}; 4 | use std::collections::HashMap; 5 | use std::env; 6 | 7 | #[derive(Envconfig)] 8 | pub struct Config { 9 | #[envconfig(from = "DB_HOST")] 10 | pub db_host: String, 11 | 12 | #[envconfig(from = "DB_PORT")] 13 | pub db_port: u16, 14 | } 15 | 16 | fn setup() { 17 | env::remove_var("DB_HOST"); 18 | env::remove_var("DB_PORT"); 19 | } 20 | 21 | #[test] 22 | fn test_inits_config_from_env_variables() { 23 | setup(); 24 | 25 | env::set_var("DB_HOST", "localhost"); 26 | env::set_var("DB_PORT", "5432"); 27 | 28 | let config = Config::init_from_env().unwrap(); 29 | assert_eq!(config.db_host, "localhost"); 30 | assert_eq!(config.db_port, 5432u16); 31 | } 32 | 33 | #[test] 34 | fn test_inits_config_from_hashmap() { 35 | setup(); 36 | 37 | let mut hashmap = HashMap::new(); 38 | hashmap.insert("DB_HOST".to_string(), "localhost".to_string()); 39 | hashmap.insert("DB_PORT".to_string(), "5432".to_string()); 40 | 41 | let config = Config::init_from_hashmap(&hashmap).unwrap(); 42 | assert_eq!(config.db_host, "localhost"); 43 | assert_eq!(config.db_port, 5432u16); 44 | } 45 | 46 | #[test] 47 | fn test_checks_presence_of_env_vars() { 48 | setup(); 49 | 50 | env::set_var("DB_HOST", "localhost"); 51 | 52 | let err = Config::init_from_env().err().unwrap(); 53 | let expected_err = Error::EnvVarMissing { name: "DB_PORT" }; 54 | assert_eq!(err, expected_err); 55 | } 56 | 57 | #[test] 58 | fn test_checks_presence_of_hashmap_keys() { 59 | setup(); 60 | 61 | let mut hashmap = HashMap::new(); 62 | hashmap.insert("DB_HOST".to_string(), "localhost".to_string()); 63 | 64 | let err = Config::init_from_hashmap(&hashmap).err().unwrap(); 65 | let expected_err = Error::EnvVarMissing { name: "DB_PORT" }; 66 | assert_eq!(err, expected_err); 67 | } 68 | 69 | #[test] 70 | fn test_fails_if_can_not_parse_db_port_from_env() { 71 | setup(); 72 | 73 | env::set_var("DB_HOST", "localhost"); 74 | env::set_var("DB_PORT", "67000"); 75 | 76 | let err = Config::init_from_env().err().unwrap(); 77 | let expected_err = Error::ParseError { name: "DB_PORT" }; 78 | assert_eq!(err, expected_err); 79 | } 80 | 81 | #[test] 82 | fn test_fails_if_can_not_parse_db_port_from_hashmap() { 83 | setup(); 84 | 85 | let mut hashmap = HashMap::new(); 86 | hashmap.insert("DB_HOST".to_string(), "localhost".to_string()); 87 | hashmap.insert("DB_PORT".to_string(), "67000".to_string()); 88 | 89 | let err = Config::init_from_hashmap(&hashmap).err().unwrap(); 90 | let expected_err = Error::ParseError { name: "DB_PORT" }; 91 | assert_eq!(err, expected_err); 92 | } 93 | 94 | #[test] 95 | fn test_custom_from_str() { 96 | use std::num::ParseIntError; 97 | use std::str::FromStr; 98 | 99 | setup(); 100 | 101 | #[derive(Debug, PartialEq)] 102 | struct Point { 103 | x: i32, 104 | y: i32, 105 | } 106 | 107 | impl FromStr for Point { 108 | type Err = ParseIntError; 109 | 110 | fn from_str(s: &str) -> Result { 111 | let coords: Vec<&str> = s 112 | .trim_matches(|p| p == '(' || p == ')') 113 | .split(',') 114 | .collect(); 115 | 116 | let x_fromstr = coords[0].parse::()?; 117 | let y_fromstr = coords[1].parse::()?; 118 | 119 | Ok(Point { 120 | x: x_fromstr, 121 | y: y_fromstr, 122 | }) 123 | } 124 | } 125 | 126 | #[derive(Envconfig)] 127 | pub struct Config { 128 | #[envconfig(from = "POINT")] 129 | point: Point, 130 | } 131 | 132 | env::set_var("POINT", "(1,2)"); 133 | 134 | let err = Config::init_from_env().unwrap(); 135 | assert_eq!(err.point, Point { x: 1, y: 2 }); 136 | 137 | setup(); 138 | 139 | let mut hashmap = HashMap::new(); 140 | hashmap.insert("POINT".to_string(), "(1,2)".to_string()); 141 | 142 | let err = Config::init_from_hashmap(&hashmap).unwrap(); 143 | assert_eq!(err.point, Point { x: 1, y: 2 }); 144 | } 145 | -------------------------------------------------------------------------------- /test_suite/tests/default.rs: -------------------------------------------------------------------------------- 1 | extern crate envconfig; 2 | 3 | use envconfig::Envconfig; 4 | use std::collections::HashMap; 5 | use std::env; 6 | 7 | #[derive(Envconfig)] 8 | pub struct Config { 9 | #[envconfig(from = "PORT", default = "5432")] 10 | pub port: u16, 11 | } 12 | 13 | fn setup() { 14 | env::remove_var("PORT"); 15 | } 16 | 17 | #[test] 18 | fn test_when_env_is_missing() { 19 | setup(); 20 | 21 | let config = Config::init_from_env().unwrap(); 22 | assert_eq!(config.port, 5432); 23 | } 24 | 25 | #[test] 26 | fn test_when_hashmap_key_is_missing() { 27 | setup(); 28 | 29 | let config = Config::init_from_hashmap(&HashMap::new()).unwrap(); 30 | assert_eq!(config.port, 5432); 31 | } 32 | 33 | #[test] 34 | fn test_when_env_is_present() { 35 | setup(); 36 | 37 | env::set_var("PORT", "8080"); 38 | let config = Config::init_from_env().unwrap(); 39 | assert_eq!(config.port, 8080); 40 | } 41 | 42 | #[test] 43 | fn test_when_hashmap_key_is_present() { 44 | setup(); 45 | 46 | let mut hashmap = HashMap::new(); 47 | hashmap.insert("PORT".to_string(), "8080".to_string()); 48 | 49 | let config = Config::init_from_hashmap(&hashmap).unwrap(); 50 | assert_eq!(config.port, 8080); 51 | } 52 | -------------------------------------------------------------------------------- /test_suite/tests/default_env_var.rs: -------------------------------------------------------------------------------- 1 | extern crate envconfig; 2 | 3 | use envconfig::{Envconfig, Error}; 4 | use std::collections::HashMap; 5 | use std::env; 6 | 7 | #[derive(Envconfig)] 8 | pub struct Config { 9 | pub db_host: String, 10 | pub db_port: Option, 11 | } 12 | 13 | fn setup() { 14 | env::remove_var("DB_HOST"); 15 | env::remove_var("DB_PORT"); 16 | } 17 | 18 | #[test] 19 | fn test_derives_env_variable_names_automatically() { 20 | setup(); 21 | 22 | env::set_var("DB_HOST", "db.example.com"); 23 | env::set_var("DB_PORT", "5050"); 24 | 25 | let config = Config::init_from_env().unwrap(); 26 | assert_eq!(config.db_host, "db.example.com"); 27 | assert_eq!(config.db_port, Some(5050)); 28 | } 29 | 30 | #[test] 31 | fn test_derives_hashmap_keys_automatically() { 32 | setup(); 33 | 34 | let mut hashmap = HashMap::new(); 35 | hashmap.insert("DB_HOST".to_string(), "db.example.com".to_string()); 36 | hashmap.insert("DB_PORT".to_string(), "5050".to_string()); 37 | 38 | let config = Config::init_from_hashmap(&hashmap).unwrap(); 39 | assert_eq!(config.db_host, "db.example.com"); 40 | assert_eq!(config.db_port, Some(5050)); 41 | } 42 | 43 | #[test] 44 | fn test_var_is_missing() { 45 | setup(); 46 | 47 | let err = Config::init_from_env().err().unwrap(); 48 | let expected_err = Error::EnvVarMissing { name: "DB_HOST" }; 49 | assert_eq!(err, expected_err); 50 | } 51 | 52 | #[test] 53 | fn test_key_is_missing() { 54 | setup(); 55 | 56 | let err = Config::init_from_hashmap(&HashMap::default()) 57 | .err() 58 | .unwrap(); 59 | let expected_err = Error::EnvVarMissing { name: "DB_HOST" }; 60 | assert_eq!(err, expected_err); 61 | } 62 | -------------------------------------------------------------------------------- /test_suite/tests/nested.rs: -------------------------------------------------------------------------------- 1 | extern crate envconfig; 2 | 3 | use envconfig::{Envconfig, Error}; 4 | use std::collections::HashMap; 5 | use std::env; 6 | 7 | #[derive(Envconfig)] 8 | pub struct DBConfig { 9 | #[envconfig(from = "DB_HOST")] 10 | pub host: String, 11 | #[envconfig(from = "DB_PORT")] 12 | pub port: u16, 13 | } 14 | 15 | #[derive(Envconfig)] 16 | pub struct Config { 17 | #[envconfig(nested)] 18 | pub db: DBConfig, 19 | } 20 | 21 | #[derive(Envconfig)] 22 | pub struct ConfigDouble { 23 | #[envconfig(nested)] 24 | pub db1: DBConfig, 25 | 26 | #[envconfig(nested)] 27 | pub db2: DBConfig, 28 | } 29 | 30 | fn setup() { 31 | env::remove_var("DB_HOST"); 32 | env::remove_var("DB_PORT"); 33 | } 34 | 35 | #[test] 36 | fn test_nesting_env() { 37 | setup(); 38 | 39 | env::set_var("DB_HOST", "localhost"); 40 | env::set_var("DB_PORT", "5432"); 41 | 42 | let config = Config::init_from_env().unwrap(); 43 | assert_eq!(config.db.host, "localhost"); 44 | assert_eq!(config.db.port, 5432u16); 45 | } 46 | 47 | #[test] 48 | fn test_nesting_hashmap() { 49 | setup(); 50 | 51 | let mut hashmap = HashMap::new(); 52 | hashmap.insert("DB_HOST".to_string(), "localhost".to_string()); 53 | hashmap.insert("DB_PORT".to_string(), "5432".to_string()); 54 | 55 | let config = Config::init_from_hashmap(&hashmap).unwrap(); 56 | assert_eq!(config.db.host, "localhost"); 57 | assert_eq!(config.db.port, 5432u16); 58 | } 59 | 60 | #[test] 61 | fn test_nesting_env_error() { 62 | setup(); 63 | 64 | env::set_var("DB_HOST", "localhost"); 65 | 66 | let err = Config::init_from_env().err().unwrap(); 67 | let expected_err = Error::EnvVarMissing { name: "DB_PORT" }; 68 | assert_eq!(err, expected_err); 69 | } 70 | 71 | #[test] 72 | fn test_nesting_hashmap_error() { 73 | setup(); 74 | 75 | let mut hashmap = HashMap::new(); 76 | hashmap.insert("DB_HOST".to_string(), "localhost".to_string()); 77 | 78 | let err = Config::init_from_hashmap(&hashmap).err().unwrap(); 79 | let expected_err = Error::EnvVarMissing { name: "DB_PORT" }; 80 | assert_eq!(err, expected_err); 81 | } 82 | 83 | #[test] 84 | fn test_duplicated_are_allowed_in_env() { 85 | setup(); 86 | 87 | env::set_var("DB_HOST", "localhost"); 88 | env::set_var("DB_PORT", "5432"); 89 | 90 | let config = ConfigDouble::init_from_env().unwrap(); 91 | assert_eq!(config.db1.host, "localhost"); 92 | assert_eq!(config.db1.port, 5432u16); 93 | assert_eq!(config.db2.host, "localhost"); 94 | assert_eq!(config.db2.port, 5432u16); 95 | } 96 | 97 | #[test] 98 | fn test_duplicated_are_allowed_in_hashmap() { 99 | setup(); 100 | 101 | let mut hashmap = HashMap::new(); 102 | hashmap.insert("DB_HOST".to_string(), "localhost".to_string()); 103 | hashmap.insert("DB_PORT".to_string(), "5432".to_string()); 104 | 105 | let config = ConfigDouble::init_from_hashmap(&hashmap).unwrap(); 106 | assert_eq!(config.db1.host, "localhost"); 107 | assert_eq!(config.db1.port, 5432u16); 108 | assert_eq!(config.db2.host, "localhost"); 109 | assert_eq!(config.db2.port, 5432u16); 110 | } 111 | -------------------------------------------------------------------------------- /test_suite/tests/option.rs: -------------------------------------------------------------------------------- 1 | extern crate envconfig; 2 | 3 | use envconfig::{Envconfig, Error}; 4 | use std::collections::HashMap; 5 | use std::env; 6 | 7 | #[derive(Envconfig)] 8 | pub struct Config { 9 | #[envconfig(from = "PORT")] 10 | pub port: Option, 11 | } 12 | 13 | fn setup() { 14 | env::remove_var("PORT"); 15 | } 16 | 17 | #[test] 18 | fn test_env_var_is_missing() { 19 | setup(); 20 | 21 | let config = Config::init_from_env().unwrap(); 22 | assert_eq!(config.port, None); 23 | } 24 | 25 | #[test] 26 | fn test_hashmap_key_is_missing() { 27 | setup(); 28 | 29 | let config = Config::init_from_hashmap(&HashMap::new()).unwrap(); 30 | assert_eq!(config.port, None); 31 | } 32 | 33 | #[test] 34 | fn test_env_var_is_present() { 35 | setup(); 36 | 37 | env::set_var("PORT", "3030"); 38 | let config = Config::init_from_env().unwrap(); 39 | assert_eq!(config.port, Some(3030)); 40 | } 41 | 42 | #[test] 43 | fn test_hashmap_key_is_present() { 44 | setup(); 45 | 46 | let mut hashmap = HashMap::new(); 47 | hashmap.insert("PORT".to_string(), "3030".to_string()); 48 | 49 | let config = Config::init_from_hashmap(&hashmap).unwrap(); 50 | assert_eq!(config.port, Some(3030)); 51 | } 52 | 53 | #[test] 54 | fn test_env_var_is_invalid() { 55 | setup(); 56 | 57 | env::set_var("PORT", "xyz"); 58 | let err = Config::init_from_env().err().unwrap(); 59 | let expected_err = Error::ParseError { name: "PORT" }; 60 | assert_eq!(err, expected_err); 61 | } 62 | 63 | #[test] 64 | fn test_hashmap_key_is_invalid() { 65 | setup(); 66 | 67 | let mut hashmap = HashMap::new(); 68 | hashmap.insert("PORT".to_string(), "xyz".to_string()); 69 | 70 | let err = Config::init_from_hashmap(&hashmap).err().unwrap(); 71 | let expected_err = Error::ParseError { name: "PORT" }; 72 | assert_eq!(err, expected_err); 73 | } 74 | --------------------------------------------------------------------------------