├── .gitignore ├── .travis.yml ├── Cargo.toml ├── Justfile ├── LICENSE ├── README.md ├── images └── logo.png ├── run-travis-job.sh ├── rustfmt.toml ├── src ├── env.rs ├── hydro.rs ├── lib.rs ├── settings.rs ├── sources.rs └── utils.rs └── tests ├── data ├── .env └── config │ ├── .secrets.toml │ └── settings.toml ├── data2 ├── .env ├── .env.development └── config │ ├── .secrets.toml │ └── settings.toml ├── data3 ├── .env ├── .env.production ├── .secrets.toml └── settings.toml └── hydration.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | # Need to cache the whole `.cargo` directory to keep .crates.toml for 4 | # cargo-update to work 5 | cache: 6 | directories: 7 | - /home/travis/.cargo 8 | 9 | # But don't cache the cargo registry 10 | before_cache: 11 | - rm -rf /home/travis/.cargo/registry/{src,index} 12 | 13 | matrix: 14 | include: 15 | - rust: stable 16 | env: RUST_VERSION=stable COMMAND=test COV=yes 17 | 18 | - rust: beta 19 | env: RUST_VERSION=beta COMMAND=test 20 | 21 | - rust: nightly 22 | env: RUST_VERSION=nightly COMMAND=test 23 | 24 | script: 25 | - ./run-travis-job.sh 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hydroconf" 3 | version = "0.2.0" 4 | authors = ["Michele Lacchia "] 5 | license = "ISC" 6 | edition = "2018" 7 | 8 | description = "Effortless configuration management for Rust." 9 | homepage = "https://github.com/rubik/hydroconf" 10 | repository = "https://github.com/rubik/hydroconf" 11 | keywords = ["configuration", "12factorapp", "settings"] 12 | 13 | [dependencies] 14 | config = "0.10.1" 15 | dotenv-parser = ">=0.1.2" 16 | serde = "1.0" 17 | 18 | [dev-dependencies] 19 | serde = { version = "1.0", features = ["derive"] } 20 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test -- --test-threads 1 3 | 4 | f: 5 | rustfmt $(shell find src -name "*.rs" -type f) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, github.com/rubik 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Hydroconf logo 3 |
4 | 5 |
6 |

Hydroconf

7 |

Effortless configuration management for Rust. Keep your apps hydrated!

8 | 9 | Build 10 | 11 | 12 | Code Coverage 13 | 14 | 15 | Downloads (all time) 16 | 17 | 18 | ISC License 19 | 20 |
21 |
22 |
23 | 24 | Hydroconf is a configuration management library for Rust, based on [config-rs] 25 | and heavily inspired by Python's [dynaconf]. 26 | 27 | # Features 28 | * Inspired by the [12-factor] configuration principles 29 | * Effective separation of sensitive information (secrets) 30 | * Layered system for multi environments (e.g. development, staging, production, 31 | etc.) 32 | * Sane defaults, with a 1-line configuration loading 33 | * Read from [JSON], [TOML], [YAML], [HJSON], [INI] files 34 | 35 | The [config-rs] library is a great building block, but it does not provide a 36 | default mechanism to load configuration and merge secrets, while keeping the 37 | different environments separated. Hydroconf fills this gap. 38 | 39 | [config-rs]: https://github.com/mehcode/config-rs 40 | [dynaconf]: https://github.com/rochacbruno/dynaconf 41 | [12-factor]: https://12factor.net/config 42 | [JSON]: https://github.com/serde-rs/json 43 | [TOML]: https://github.com/toml-lang/toml 44 | [YAML]: https://github.com/chyh1990/yaml-rust 45 | [HJSON]: https://github.com/hjson/hjson-rust 46 | [INI]: https://github.com/zonyitoo/rust-ini 47 | 48 | # Quickstart 49 | 50 | Suppose you have the following file structure: 51 | 52 | ``` 53 | ├── config 54 | │ ├── .secrets.toml 55 | │ └── settings.toml 56 | └── your-executable 57 | ``` 58 | 59 | `settings.toml`: 60 | 61 | ```toml 62 | [default] 63 | pg.port = 5432 64 | pg.host = 'localhost' 65 | 66 | [production] 67 | pg.host = 'db-0' 68 | ``` 69 | 70 | `.secrets.toml`: 71 | 72 | ```toml 73 | [default] 74 | pg.password = 'a password' 75 | 76 | [production] 77 | pg.password = 'a strong password' 78 | ``` 79 | 80 | Then, in your executable source (make sure to add `serde = { version = "1.0", 81 | features = ["derive"] }` to your dependencies): 82 | 83 | ```rust 84 | use serde::Deserialize; 85 | use hydroconf::Hydroconf; 86 | 87 | #[derive(Debug, Deserialize)] 88 | struct Config { 89 | pg: PostgresConfig, 90 | } 91 | 92 | #[derive(Debug, Deserialize)] 93 | struct PostgresConfig { 94 | host: String, 95 | port: u16, 96 | password: String, 97 | } 98 | 99 | fn main() { 100 | let conf: Config = match Hydroconf::default().hydrate() { 101 | Ok(c) => c, 102 | Err(e) => { 103 | println!("could not read configuration: {:#?}", e); 104 | std::process::exit(1); 105 | } 106 | }; 107 | 108 | println!("{:#?}", conf); 109 | } 110 | ``` 111 | 112 | If you compile and execute the program (making sure the executable is in the 113 | same directory where the `config` directory is), you will see the following: 114 | 115 | ```sh 116 | $ ./your-executable 117 | Config { 118 | pg: PostgresConfig { 119 | host: "localhost", 120 | port: 5432, 121 | password: "a password" 122 | } 123 | } 124 | ``` 125 | 126 | Hydroconf will select the settings in the `[default]` table by default. If you 127 | set `ENV_FOR_HYDRO` to `production`, Hydroconf will overwrite them with the 128 | production ones: 129 | 130 | ```sh 131 | $ ENV_FOR_HYDRO=production ./your-executable 132 | Config { 133 | pg: PostgresConfig { 134 | host: "db-0", 135 | port: 5432, 136 | password: "a strong password" 137 | } 138 | } 139 | ``` 140 | 141 | Settings can always be overridden with environment variables: 142 | 143 | ```bash 144 | $ HYDRO_PG__PASSWORD="an even stronger password" ./your-executable 145 | Config { 146 | pg: PostgresConfig { 147 | host: "localhost", 148 | port: 5432, 149 | password: "an even stronger password" 150 | } 151 | } 152 | ``` 153 | 154 | The description of all Hydroconf configuration options and how the program 155 | configuration is loaded can be found in the 156 | [documentation](https://docs.rs/hydroconf). 157 | 158 |
159 | 160 | Logo made by Freepik from www.flaticon.com 161 | 162 |
163 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubik/hydroconf/fac13fbe6268bee171b80c1812c7beba8f9c4718/images/logo.png -------------------------------------------------------------------------------- /run-travis-job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | make $COMMAND 6 | 7 | if [ "$COV" = "yes" ] 8 | then 9 | cargo install cargo-tarpaulin 10 | cargo tarpaulin -v --ignore-tests \ 11 | --ciserver travis-ci --coveralls "$TRAVIS_JOB_ID" -- --test-threads 1 12 | fi 13 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | max_width = 79 3 | # Unstable features 4 | #wrap_comments = true 5 | #format_code_in_doc_comments = true 6 | #imports_layout = "HorizontalVertical" 7 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub fn get_var<'a, T>(key: &'a str, suffix: &'a str) -> Option 4 | where 5 | T: FromVar, 6 | { 7 | let full_key = format!("{}{}", key, suffix); 8 | match std::env::var(full_key) { 9 | Err(_) => None, 10 | Ok(v) => FromVar::parse(v), 11 | } 12 | } 13 | 14 | pub fn get_var_default<'a, T>(key: &'a str, suffix: &'a str, default: T) -> T 15 | where 16 | T: FromVar, 17 | { 18 | get_var(key, suffix).unwrap_or(default) 19 | } 20 | 21 | pub trait FromVar { 22 | fn parse(var: String) -> Option 23 | where 24 | Self: Sized; 25 | } 26 | 27 | impl FromVar for PathBuf { 28 | fn parse(var: String) -> Option { 29 | Some(PathBuf::from(var)) 30 | } 31 | } 32 | 33 | impl FromVar for String { 34 | fn parse(var: String) -> Option { 35 | Some(var) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hydro.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | 4 | pub use config::{Config, ConfigError, Environment, File, Value}; 5 | use dotenv_parser::parse_dotenv; 6 | use serde::Deserialize; 7 | 8 | use crate::settings::HydroSettings; 9 | use crate::sources::FileSources; 10 | use crate::utils::path_to_string; 11 | 12 | type Table = HashMap; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Hydroconf { 16 | config: Config, 17 | orig_config: Config, 18 | hydro_settings: HydroSettings, 19 | sources: FileSources, 20 | } 21 | 22 | impl Default for Hydroconf { 23 | fn default() -> Self { 24 | Self::new(HydroSettings::default()) 25 | } 26 | } 27 | 28 | impl Hydroconf { 29 | pub fn new(hydro_settings: HydroSettings) -> Self { 30 | Self { 31 | config: Config::default(), 32 | orig_config: Config::default(), 33 | hydro_settings, 34 | sources: FileSources::default(), 35 | } 36 | } 37 | 38 | pub fn hydrate<'de, T: Deserialize<'de>>( 39 | mut self, 40 | ) -> Result { 41 | self.discover_sources(); 42 | self.load_settings()?; 43 | self.merge_settings()?; 44 | self.override_from_dotenv()?; 45 | self.override_from_env()?; 46 | self.try_into() 47 | } 48 | 49 | pub fn discover_sources(&mut self) { 50 | self.sources = self 51 | .root_path() 52 | .map(|p| { 53 | FileSources::from_root(p, self.hydro_settings.env.as_str()) 54 | }) 55 | .unwrap_or_else(|| FileSources::default()); 56 | } 57 | 58 | pub fn load_settings(&mut self) -> Result<&mut Self, ConfigError> { 59 | if let Some(ref settings_path) = self.sources.settings { 60 | self.orig_config.merge(File::from(settings_path.clone()))?; 61 | } 62 | if let Some(ref secrets_path) = self.sources.secrets { 63 | self.orig_config.merge(File::from(secrets_path.clone()))?; 64 | } 65 | 66 | Ok(self) 67 | } 68 | 69 | pub fn merge_settings(&mut self) -> Result<&mut Self, ConfigError> { 70 | for &name in &["default", self.hydro_settings.env.as_str()] { 71 | let table_value: Option = self.orig_config.get(name).ok(); 72 | if let Some(value) = table_value { 73 | let mut new_config = Config::default(); 74 | new_config.cache = value.into(); 75 | self.config.merge(new_config)?; 76 | } 77 | } 78 | 79 | Ok(self) 80 | } 81 | 82 | pub fn override_from_dotenv(&mut self) -> Result<&mut Self, ConfigError> { 83 | for dotenv_path in &self.sources.dotenv { 84 | let source = std::fs::read_to_string(dotenv_path.clone()) 85 | .map_err(|e| ConfigError::FileParse { 86 | uri: path_to_string(dotenv_path.clone()), 87 | cause: e.into(), 88 | })?; 89 | let map = 90 | parse_dotenv(&source).map_err(|e| ConfigError::FileParse { 91 | uri: path_to_string(dotenv_path.clone()), 92 | cause: e.into(), 93 | })?; 94 | 95 | for (key, val) in map.iter() { 96 | if val.is_empty() { 97 | continue; 98 | } 99 | let prefix = 100 | self.hydro_settings.envvar_prefix.to_lowercase() + "_"; 101 | let mut key = key.to_lowercase(); 102 | if !key.starts_with(&prefix) { 103 | continue; 104 | } else { 105 | key = key[prefix.len()..].to_string(); 106 | } 107 | let sep = self.hydro_settings.envvar_nested_sep.clone(); 108 | key = key.replace(&sep, "."); 109 | self.config.set::(&key, val.into())?; 110 | } 111 | } 112 | 113 | Ok(self) 114 | } 115 | 116 | pub fn override_from_env(&mut self) -> Result<&mut Self, ConfigError> { 117 | self.config.merge( 118 | Environment::with_prefix( 119 | self.hydro_settings.envvar_prefix.as_str(), 120 | ) 121 | .separator(self.hydro_settings.envvar_nested_sep.as_str()), 122 | )?; 123 | 124 | Ok(self) 125 | } 126 | 127 | pub fn root_path(&self) -> Option { 128 | self.hydro_settings 129 | .root_path 130 | .clone() 131 | .or_else(|| std::env::current_exe().ok()) 132 | } 133 | 134 | pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result { 135 | self.config.try_into() 136 | } 137 | 138 | //pub fn refresh(&mut self) -> Result<&mut Self, ConfigError> { 139 | //self.orig_config.refresh()?; 140 | //self.config.cache = Value::new(None, Table::new()); 141 | //self.merge()?; 142 | //self.override_from_env()?; 143 | //Ok(self) 144 | //} 145 | 146 | pub fn set_default( 147 | &mut self, 148 | key: &str, 149 | value: T, 150 | ) -> Result<&mut Self, ConfigError> 151 | where 152 | T: Into, 153 | { 154 | self.config.set_default(key, value)?; 155 | Ok(self) 156 | } 157 | 158 | pub fn set( 159 | &mut self, 160 | key: &str, 161 | value: T, 162 | ) -> Result<&mut Self, ConfigError> 163 | where 164 | T: Into, 165 | { 166 | self.config.set(key, value)?; 167 | Ok(self) 168 | } 169 | 170 | pub fn get<'de, T>(&self, key: &'de str) -> Result 171 | where 172 | T: Deserialize<'de>, 173 | { 174 | self.config.get(key) 175 | } 176 | 177 | pub fn get_str(&self, key: &str) -> Result { 178 | self.get(key).and_then(Value::into_str) 179 | } 180 | 181 | pub fn get_int(&self, key: &str) -> Result { 182 | self.get(key).and_then(Value::into_int) 183 | } 184 | 185 | pub fn get_float(&self, key: &str) -> Result { 186 | self.get(key).and_then(Value::into_float) 187 | } 188 | 189 | pub fn get_bool(&self, key: &str) -> Result { 190 | self.get(key).and_then(Value::into_bool) 191 | } 192 | 193 | pub fn get_table( 194 | &self, 195 | key: &str, 196 | ) -> Result, ConfigError> { 197 | self.get(key).and_then(Value::into_table) 198 | } 199 | 200 | pub fn get_array(&self, key: &str) -> Result, ConfigError> { 201 | self.get(key).and_then(Value::into_array) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Quickstart 2 | //! 3 | //! Suppose you have the following file structure: 4 | //! 5 | //! ```text 6 | //! ├── config 7 | //! │ ├── .secrets.toml 8 | //! │ └── settings.toml 9 | //! └── your-executable 10 | //! ``` 11 | //! 12 | //! `settings.toml`: 13 | //! 14 | //! ```toml 15 | //! [default] 16 | //! pg.port = 5432 17 | //! pg.host = 'localhost' 18 | //! 19 | //! [production] 20 | //! pg.host = 'db-0' 21 | //! ``` 22 | //! 23 | //! `.secrets.toml`: 24 | //! 25 | //! ```toml 26 | //! [default] 27 | //! pg.password = 'a password' 28 | //! 29 | //! [production] 30 | //! pg.password = 'a strong password' 31 | //! ``` 32 | //! 33 | //! Then, in your executable source (make sure to add `serde = { version = "1.0", 34 | //! features = ["derive"] }` to your dependencies): 35 | //! 36 | //! ```no_run 37 | //! use serde::Deserialize; 38 | //! use hydroconf::Hydroconf; 39 | //! 40 | //! #[derive(Debug, Deserialize)] 41 | //! struct Config { 42 | //! pg: PostgresConfig, 43 | //! } 44 | //! 45 | //! #[derive(Debug, Deserialize)] 46 | //! struct PostgresConfig { 47 | //! host: String, 48 | //! port: u16, 49 | //! password: String, 50 | //! } 51 | //! 52 | //! fn main() { 53 | //! let conf: Config = match Hydroconf::default().hydrate() { 54 | //! Ok(c) => c, 55 | //! Err(e) => { 56 | //! println!("could not read configuration: {:#?}", e); 57 | //! std::process::exit(1); 58 | //! } 59 | //! }; 60 | //! 61 | //! println!("{:#?}", conf); 62 | //! } 63 | //! ``` 64 | //! 65 | //! If you compile and execute the program (making sure the executable is in the 66 | //! same directory where the `config` directory is), you will see the following: 67 | //! 68 | //! ```sh 69 | //! $ ./your-executable 70 | //! Config { 71 | //! pg: PostgresConfig { 72 | //! host: "localhost", 73 | //! port: 5432, 74 | //! password: "a password" 75 | //! } 76 | //! } 77 | //! ``` 78 | //! 79 | //! Hydroconf will select the settings in the `[default]` table by default. If you 80 | //! set `ENV_FOR_HYDRO` to `production`, Hydroconf will overwrite them with the 81 | //! production ones: 82 | //! 83 | //! ```sh 84 | //! $ ENV_FOR_HYDRO=production ./your-executable 85 | //! Config { 86 | //! pg: PostgresConfig { 87 | //! host: "db-0", 88 | //! port: 5432, 89 | //! password: "a strong password" 90 | //! } 91 | //! } 92 | //! ``` 93 | //! 94 | //! Settings can always be overridden with environment variables: 95 | //! 96 | //! ```bash 97 | //! $ HYDRO_PG__PASSWORD="an even stronger password" ./your-executable 98 | //! Config { 99 | //! pg: PostgresConfig { 100 | //! host: "localhost", 101 | //! port: 5432, 102 | //! password: "an even stronger password" 103 | //! } 104 | //! } 105 | //! ``` 106 | //! # Environment variables 107 | //! There are two formats for the environment variables: 108 | //! 109 | //! 1. those that control how Hydroconf works have the form `*_FOR_HYDRO`; 110 | //! 2. those that override values in your configuration have the form `HYDRO_*`. 111 | //! 112 | //! For example, to specify where Hydroconf should look for the configuration 113 | //! files, you can set the variable `ROOT_PATH_FOR_HYDRO`. In that case, it's no 114 | //! longer necessary to place the binary in the same directory as the 115 | //! configuration. Hydroconf will search directly from the root path you specify. 116 | //! 117 | //! Here is a list of all the currently supported environment variables to 118 | //! configure how Hydroconf works: 119 | //! 120 | //! * `ROOT_PATH_FOR_HYDRO`: specifies the location from which Hydroconf should 121 | //! start searching configuration files. By default, Hydroconf will start from 122 | //! the directory that contains your executable; 123 | //! * `SETTINGS_FILE_FOR_HYDRO`: exact location of the main settings file; 124 | //! * `SECRETS_FILE_FOR_HYDRO`: exact location of the file containing secrets; 125 | //! * `ENV_FOR_HYDRO`: the environment to load after loading the `default` one 126 | //! (e.g. `development`, `testing`, `staging`, `production`, etc.). By default, 127 | //! Hydroconf will load the `development` environment, unless otherwise 128 | //! specified. 129 | //! * `ENVVAR_PREFIX_FOR_HYDRO`: the prefix of the environement variables holding 130 | //! your configuration -- see group number 2. above. By default it's `HYDRO` 131 | //! (note that you don't have to include the `_` separator, as that is added 132 | //! automatically); 133 | //! * `ENVVAR_NESTED_SEP_FOR_HYDRO`: the separator in the environment variables 134 | //! holding your configuration that signals a nesting point. By default it's `__` 135 | //! (double underscore), so if you set `HYDRO_REDIS__HOST=localhost`, Hydroconf 136 | //! will match it with the nested field `redis.host` in your configuration. 137 | //! 138 | //! # Hydroconf initialization 139 | //! You can create a new Hydroconf struct in two ways. 140 | //! 141 | //! The first one is to use the `Hydroconf::default()` method, which will use the 142 | //! default settings. The default constructor will attempt to load the settings 143 | //! from the environment variables (those in the form `*_FOR_HYDRO`), and if it 144 | //! doesn't find them it will use the default values. The alternative is to create 145 | //! a `HydroSettings` struct manually and pass it to `Hydroconf`: 146 | //! 147 | //! ```rust 148 | //! # use hydroconf::{Hydroconf, HydroSettings}; 149 | //! 150 | //! let hydro_settings = HydroSettings::default() 151 | //! .set_envvar_prefix("MYAPP".into()) 152 | //! .set_env("staging".into()); 153 | //! let hydro = Hydroconf::new(hydro_settings); 154 | //! ``` 155 | //! 156 | //! Note that `HydroSettings::default()` will still try to load the settings from 157 | //! the environment before you overwrite them. 158 | //! 159 | //! # The hydration process 160 | //! ## 1. Configuration loading 161 | //! When you call `Hydroconf::hydrate()`, Hydroconf starts looking for your 162 | //! configuration files and if it finds them, it loads them. The search starts from 163 | //! `HydroSettings.root_path`; if the root path is not defined, Hydroconf will use 164 | //! `std::env::current_exe()`. From this path, Hydroconf generates all the possible 165 | //! candidates by walking up the directory tree, also searching in the `config` 166 | //! subfolder at each level. For example, if the root path is 167 | //! `/home/user/www/api-server/dist`, Hydroconf will try the following paths, in 168 | //! this order: 169 | //! 170 | //! 1. `/home/user/www/api-server/dist/config` 171 | //! 2. `/home/user/www/api-server/dist` 172 | //! 3. `/home/user/www/api-server/config` 173 | //! 4. `/home/user/www/api-server` 174 | //! 5. `/home/user/www/config` 175 | //! 6. `/home/user/www` 176 | //! 7. `/home/user/config` 177 | //! 8. `/home/user` 178 | //! 9. `/home/config` 179 | //! 10. `/home` 180 | //! 11. `/config` 181 | //! 12. `/` 182 | //! 183 | //! In each directory, Hydroconf will search for the files 184 | //! `settings.{toml,json,yaml,ini,hjson}` and 185 | //! `.secrets.{toml,json,yaml,ini,hjson}`. As soon as one of those (or both) are 186 | //! found, the search stops and Hydroconf won't search the remaining upper levels. 187 | //! 188 | //! ## 2. Merging 189 | //! In this step, Hydroconf merges the values from the different environments 190 | //! from the configuration files discovered in the previous step. Hydroconf 191 | //! first checks if an environment called `default` exists: in that case those 192 | //! values are selected first. Then, it checks if the environment specified for 193 | //! Hydroconf (`ENV_FOR_HYDRO`, or "development" if not specified) exists and in 194 | //! that case it selects those values and merges them with the existing ones. 195 | //! 196 | //! ## 3. `.env` file overrides 197 | //! In this step Hydroconf starts from the root path (the same one from step 1), 198 | //! and walks the filesystem upward in search of an `.env` file. If it finds 199 | //! one, it parses it and merges those values with the existing ones. 200 | //! 201 | //! ## 4. Environment variables overrides 202 | //! In this step Hydroconf merges the values from all environment variables that 203 | //! you defined with the Hydro prefix (`HYDRO_` by default, as explained in the 204 | //! [previous section](#environment-variables)). 205 | //! 206 | //! ## 5. Deserialization 207 | //! Finally, Hydroconf tries to deserialize the configuration into the return 208 | //! type you specify, which should be your configuration struct. 209 | //! 210 | //! # Best practices 211 | //! In order to keep your configuration simple, secure and effective, Hydroconf 212 | //! makes it easy for you to follow these best practices: 213 | //! 1. keep the non-secret values inside `config/settings.{toml,yaml,json,...}` 214 | //! separated by environment; in particular, define a "default" environment 215 | //! which contains the base settings, and specialize it in all the additional 216 | //! environments that you need (e.g. "development", "testing", "staging", 217 | //! "production", etc.); 218 | //! 2. keep the secret values inside `config/.secrets.{toml,yaml,json,...}` 219 | //! separated by environment and exclude this file from version control; 220 | //! 3. define the environment variable `ENV_FOR_DYNACONF` to specify which 221 | //! environment should be loaded (besides the "default" one, which is always 222 | //! loaded first); 223 | //! 4. if you want to override some values, or specify some secret values which 224 | //! are not in the secret file, define the environment variables `HYDRO_*` 225 | //! (or use a custom prefix and define `ENVVAR_PREFIX_FOR_HYDRO`). 226 | 227 | mod env; 228 | mod hydro; 229 | mod settings; 230 | mod sources; 231 | mod utils; 232 | 233 | pub use hydro::{Config, ConfigError, Environment, File, Hydroconf}; 234 | pub use settings::HydroSettings; 235 | pub use sources::FileSources; 236 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::env; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct HydroSettings { 7 | pub root_path: Option, 8 | pub settings_file: Option, 9 | pub secrets_file: Option, 10 | pub env: String, 11 | pub envvar_prefix: String, 12 | pub encoding: String, 13 | pub envvar_nested_sep: String, 14 | } 15 | 16 | impl Default for HydroSettings { 17 | fn default() -> Self { 18 | let hydro_suffix = "_FOR_HYDRO"; 19 | Self { 20 | root_path: env::get_var("ROOT_PATH", hydro_suffix), 21 | settings_file: env::get_var("SETTINGS_FILE", hydro_suffix), 22 | secrets_file: env::get_var("SECRETS_FILE", hydro_suffix), 23 | env: env::get_var_default( 24 | "ENV", 25 | hydro_suffix, 26 | "development".into(), 27 | ), 28 | envvar_prefix: env::get_var_default( 29 | "ENVVAR_PREFIX", 30 | hydro_suffix, 31 | "HYDRO".into(), 32 | ), 33 | encoding: env::get_var_default( 34 | "ENCODING", 35 | hydro_suffix, 36 | "utf-8".into(), 37 | ), 38 | envvar_nested_sep: env::get_var_default( 39 | "ENVVAR_NESTED_SEP", 40 | hydro_suffix, 41 | "__".into(), 42 | ), 43 | } 44 | } 45 | } 46 | 47 | impl HydroSettings { 48 | pub fn set_root_path(mut self, p: PathBuf) -> Self { 49 | self.root_path = Some(p); 50 | self 51 | } 52 | 53 | pub fn set_settings_file(mut self, p: PathBuf) -> Self { 54 | self.settings_file = Some(p); 55 | self 56 | } 57 | 58 | pub fn set_secrets_file(mut self, p: PathBuf) -> Self { 59 | self.secrets_file = Some(p); 60 | self 61 | } 62 | 63 | pub fn set_env(mut self, e: String) -> Self { 64 | self.env = e; 65 | self 66 | } 67 | 68 | pub fn set_envvar_prefix(mut self, p: String) -> Self { 69 | self.envvar_prefix = p; 70 | self 71 | } 72 | 73 | pub fn set_encoding(mut self, e: String) -> Self { 74 | self.encoding = e; 75 | self 76 | } 77 | 78 | pub fn set_envvar_nested_sep(mut self, s: String) -> Self { 79 | self.envvar_nested_sep = s; 80 | self 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | use std::env::{remove_var, set_var}; 88 | 89 | #[test] 90 | fn test_default() { 91 | assert_eq!( 92 | HydroSettings::default(), 93 | HydroSettings { 94 | root_path: None, 95 | settings_file: None, 96 | secrets_file: None, 97 | env: "development".into(), 98 | envvar_prefix: "HYDRO".into(), 99 | encoding: "utf-8".into(), 100 | envvar_nested_sep: "__".into(), 101 | }, 102 | ); 103 | } 104 | 105 | #[test] 106 | fn test_default_with_env() { 107 | set_var("ENCODING_FOR_HYDRO", "latin-1"); 108 | set_var("ROOT_PATH_FOR_HYDRO", "/an/absolute/path"); 109 | assert_eq!( 110 | HydroSettings::default(), 111 | HydroSettings { 112 | root_path: Some("/an/absolute/path".into()), 113 | settings_file: None, 114 | secrets_file: None, 115 | env: "development".into(), 116 | envvar_prefix: "HYDRO".into(), 117 | encoding: "latin-1".into(), 118 | envvar_nested_sep: "__".into(), 119 | }, 120 | ); 121 | remove_var("ENCODING_FOR_HYDRO"); 122 | remove_var("ROOT_PATH_FOR_HYDRO"); 123 | } 124 | 125 | #[test] 126 | fn test_one_builder_method() { 127 | assert_eq!( 128 | HydroSettings::default() 129 | .set_root_path(PathBuf::from("~/test/dir")), 130 | HydroSettings { 131 | root_path: Some(PathBuf::from("~/test/dir")), 132 | settings_file: None, 133 | secrets_file: None, 134 | env: "development".into(), 135 | envvar_prefix: "HYDRO".into(), 136 | encoding: "utf-8".into(), 137 | envvar_nested_sep: "__".into(), 138 | }, 139 | ); 140 | } 141 | 142 | #[test] 143 | fn test_all_builder_methods() { 144 | assert_eq!( 145 | HydroSettings::default() 146 | .set_envvar_prefix("HY_".into()) 147 | .set_encoding("latin-1".into()) 148 | .set_secrets_file(PathBuf::from(".secrets.toml")) 149 | .set_env("production".into()) 150 | .set_envvar_nested_sep("-".into()) 151 | .set_root_path(PathBuf::from("~/test/dir")) 152 | .set_settings_file(PathBuf::from("settings.toml")), 153 | HydroSettings { 154 | root_path: Some(PathBuf::from("~/test/dir")), 155 | settings_file: Some(PathBuf::from("settings.toml")), 156 | secrets_file: Some(PathBuf::from(".secrets.toml")), 157 | env: "production".into(), 158 | envvar_prefix: "HY_".into(), 159 | encoding: "latin-1".into(), 160 | envvar_nested_sep: "-".into(), 161 | }, 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/sources.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | const SETTINGS_FILE_EXTENSIONS: &[&str] = 4 | &["toml", "json", "yaml", "ini", "hjson"]; 5 | const SETTINGS_DIRS: &[&str] = &["", "config"]; 6 | 7 | #[derive(Clone, Debug, Default, PartialEq)] 8 | pub struct FileSources { 9 | pub settings: Option, 10 | pub secrets: Option, 11 | pub dotenv: Vec, 12 | } 13 | 14 | impl FileSources { 15 | pub fn from_root(root_path: PathBuf, env: &str) -> Self { 16 | let mut sources = Self { 17 | settings: None, 18 | secrets: None, 19 | dotenv: Vec::new(), 20 | }; 21 | let mut settings_found = false; 22 | let candidates = walk_to_root(root_path); 23 | 24 | for cand in candidates { 25 | let dotenv_cand = cand.join(".env"); 26 | if dotenv_cand.exists() { 27 | sources.dotenv.push(dotenv_cand); 28 | } 29 | let dotenv_cand = cand.join(format!(".env.{}", env)); 30 | if dotenv_cand.exists() { 31 | sources.dotenv.push(dotenv_cand); 32 | } 33 | 'outer: for &settings_dir in SETTINGS_DIRS { 34 | let dir = cand.join(settings_dir); 35 | for &ext in SETTINGS_FILE_EXTENSIONS { 36 | let settings_cand = dir.join(format!("settings.{}", ext)); 37 | if settings_cand.exists() { 38 | sources.settings = Some(settings_cand); 39 | settings_found = true; 40 | } 41 | let secrets_cand = dir.join(format!(".secrets.{}", ext)); 42 | if secrets_cand.exists() { 43 | sources.secrets = Some(secrets_cand); 44 | settings_found = true; 45 | } 46 | if settings_found { 47 | break 'outer; 48 | } 49 | } 50 | } 51 | 52 | if sources.any() { 53 | break; 54 | } 55 | } 56 | 57 | sources 58 | } 59 | 60 | fn any(&self) -> bool { 61 | self.settings.is_some() 62 | || self.secrets.is_some() 63 | || !self.dotenv.is_empty() 64 | } 65 | } 66 | 67 | pub fn walk_to_root(mut path: PathBuf) -> Vec { 68 | let mut candidates = Vec::new(); 69 | if path.is_file() { 70 | path = path.parent().unwrap_or_else(|| Path::new("/")).into(); 71 | } 72 | for ancestor in path.ancestors() { 73 | candidates.push(ancestor.to_path_buf()); 74 | } 75 | candidates 76 | } 77 | 78 | #[cfg(test)] 79 | mod test { 80 | use super::*; 81 | use std::env; 82 | 83 | fn get_data_path(suffix: &str) -> PathBuf { 84 | let mut target_dir = PathBuf::from( 85 | env::current_exe() 86 | .expect("exe path") 87 | .parent() 88 | .expect("exe parent"), 89 | ); 90 | while target_dir.file_name() != Some(std::ffi::OsStr::new("target")) { 91 | if !target_dir.pop() { 92 | panic!("Cannot find target directory"); 93 | } 94 | } 95 | target_dir.pop(); 96 | target_dir.join(format!("tests/data{}", suffix)) 97 | } 98 | 99 | #[test] 100 | fn test_walk_to_root_dir() { 101 | assert_eq!( 102 | walk_to_root(PathBuf::from("/a/dir/located/somewhere")), 103 | vec![ 104 | PathBuf::from("/a/dir/located/somewhere"), 105 | PathBuf::from("/a/dir/located"), 106 | PathBuf::from("/a/dir"), 107 | PathBuf::from("/a"), 108 | PathBuf::from("/"), 109 | ], 110 | ); 111 | } 112 | 113 | #[test] 114 | fn test_walk_to_root_root() { 115 | assert_eq!(walk_to_root(PathBuf::from("/")), vec![PathBuf::from("/")],); 116 | } 117 | 118 | #[test] 119 | fn test_sources() { 120 | let data_path = get_data_path(""); 121 | assert_eq!( 122 | FileSources::from_root(data_path.clone(), "development"), 123 | FileSources { 124 | settings: Some(data_path.clone().join("config/settings.toml")), 125 | secrets: Some(data_path.join("config/.secrets.toml")), 126 | dotenv: vec![data_path.join(".env")], 127 | }, 128 | ); 129 | 130 | let data_path = get_data_path("2"); 131 | assert_eq!( 132 | FileSources::from_root(data_path.clone(), "development"), 133 | FileSources { 134 | settings: Some(data_path.clone().join("config/settings.toml")), 135 | secrets: Some(data_path.join("config/.secrets.toml")), 136 | dotenv: vec![ 137 | data_path.join(".env"), 138 | data_path.join(".env.development") 139 | ], 140 | }, 141 | ); 142 | 143 | let data_path = get_data_path("2"); 144 | assert_eq!( 145 | FileSources::from_root(data_path.clone(), "production"), 146 | FileSources { 147 | settings: Some(data_path.clone().join("config/settings.toml")), 148 | secrets: Some(data_path.join("config/.secrets.toml")), 149 | dotenv: vec![data_path.join(".env")], 150 | }, 151 | ); 152 | 153 | let data_path = get_data_path("3"); 154 | assert_eq!( 155 | FileSources::from_root(data_path.clone(), "development"), 156 | FileSources { 157 | settings: Some(data_path.clone().join("settings.toml")), 158 | secrets: Some(data_path.join(".secrets.toml")), 159 | dotenv: vec![data_path.join(".env")], 160 | }, 161 | ); 162 | 163 | let data_path = get_data_path("3"); 164 | assert_eq!( 165 | FileSources::from_root(data_path.clone(), "production"), 166 | FileSources { 167 | settings: Some(data_path.clone().join("settings.toml")), 168 | secrets: Some(data_path.join(".secrets.toml")), 169 | dotenv: vec![ 170 | data_path.join(".env"), 171 | data_path.join(".env.production") 172 | ], 173 | }, 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | pub fn path_to_string(path: PathBuf) -> Option { 4 | path.into_os_string().into_string().ok() 5 | } 6 | -------------------------------------------------------------------------------- /tests/data/.env: -------------------------------------------------------------------------------- 1 | TEST_VAR=dotenv 2 | PG__PORT=12329 3 | -------------------------------------------------------------------------------- /tests/data/config/.secrets.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | pg.password = 'a password' 3 | 4 | [production] 5 | pg.password = 'a strong password' 6 | -------------------------------------------------------------------------------- /tests/data/config/settings.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | pg.port = 5432 3 | pg.host = 'localhost' 4 | 5 | [production] 6 | pg.host = 'db-0' 7 | -------------------------------------------------------------------------------- /tests/data2/.env: -------------------------------------------------------------------------------- 1 | TEST_VAR=dotenv 2 | HYDRO_PG__PORT=12329 3 | -------------------------------------------------------------------------------- /tests/data2/.env.development: -------------------------------------------------------------------------------- 1 | TEST_VAR=dotenv-dev 2 | HYDRO_PG__PORT=15330 3 | -------------------------------------------------------------------------------- /tests/data2/config/.secrets.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | pg.password = 'a password' 3 | 4 | [production] 5 | pg.password = 'a strong password' 6 | -------------------------------------------------------------------------------- /tests/data2/config/settings.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | pg.port = 5432 3 | pg.host = 'localhost' 4 | 5 | [production] 6 | pg.host = 'db-0' 7 | -------------------------------------------------------------------------------- /tests/data3/.env: -------------------------------------------------------------------------------- 1 | TEST_VAR=dotenv 2 | HYDRO_PG__PORT=12329 3 | -------------------------------------------------------------------------------- /tests/data3/.env.production: -------------------------------------------------------------------------------- 1 | TEST_VAR=dotenv-prod 2 | HYDRO_PG__PORT=9999 3 | -------------------------------------------------------------------------------- /tests/data3/.secrets.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | pg.password = 'a password' 3 | 4 | [production] 5 | pg.password = 'a strong password' 6 | -------------------------------------------------------------------------------- /tests/data3/settings.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | pg.port = 5432 3 | pg.host = 'localhost' 4 | 5 | [production] 6 | pg.host = 'db-0' 7 | -------------------------------------------------------------------------------- /tests/hydration.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use serde::Deserialize; 4 | use hydroconf::{ConfigError, Hydroconf, HydroSettings}; 5 | 6 | #[derive(Debug, PartialEq, Deserialize)] 7 | struct Config { 8 | pg: PostgresConfig, 9 | } 10 | 11 | #[derive(Debug, PartialEq, Deserialize)] 12 | struct PostgresConfig { 13 | host: String, 14 | port: u16, 15 | password: String, 16 | } 17 | 18 | fn get_data_path(suffix: &str) -> PathBuf { 19 | let mut target_dir = PathBuf::from( 20 | env::current_exe() 21 | .expect("exe path") 22 | .parent() 23 | .expect("exe parent"), 24 | ); 25 | while target_dir.file_name() != Some(std::ffi::OsStr::new("target")) { 26 | if !target_dir.pop() { 27 | panic!("Cannot find target directory"); 28 | } 29 | } 30 | target_dir.pop(); 31 | target_dir.join(format!("tests/data{}", suffix)) 32 | } 33 | 34 | #[test] 35 | fn test_default_hydration() { 36 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("").into_os_string().into_string().unwrap()); 37 | let conf: Result = Hydroconf::default().hydrate(); 38 | assert_eq!(conf.unwrap(), Config { 39 | pg: PostgresConfig { 40 | host: "localhost".into(), 41 | port: 5432, 42 | password: "a password".into(), 43 | }, 44 | } 45 | ); 46 | env::remove_var("ROOT_PATH_FOR_HYDRO"); 47 | } 48 | 49 | #[test] 50 | fn test_default_hydration_with_env() { 51 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("").into_os_string().into_string().unwrap()); 52 | env::set_var("ENV_FOR_HYDRO", "production"); 53 | let conf: Result = Hydroconf::default().hydrate(); 54 | assert_eq!(conf.unwrap(), Config { 55 | pg: PostgresConfig { 56 | host: "db-0".into(), 57 | port: 5432, 58 | password: "a strong password".into(), 59 | }, 60 | } 61 | ); 62 | env::remove_var("ROOT_PATH_FOR_HYDRO"); 63 | env::remove_var("ENV_FOR_HYDRO"); 64 | } 65 | 66 | #[test] 67 | fn test_default_hydration_with_override() { 68 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("").into_os_string().into_string().unwrap()); 69 | env::set_var("HYDRO_PG__PORT", "1234"); 70 | let conf: Result = Hydroconf::default().hydrate(); 71 | assert_eq!(conf.unwrap(), Config { 72 | pg: PostgresConfig { 73 | host: "localhost".into(), 74 | port: 1234, 75 | password: "a password".into(), 76 | }, 77 | } 78 | ); 79 | env::remove_var("ROOT_PATH_FOR_HYDRO"); 80 | env::remove_var("HYDRO_PG__PORT"); 81 | } 82 | 83 | #[test] 84 | fn test_default_hydration_with_env_and_override() { 85 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("").into_os_string().into_string().unwrap()); 86 | env::set_var("ENV_FOR_HYDRO", "production"); 87 | env::set_var("HYDRO_PG__PORT", "1234"); 88 | let conf: Result = Hydroconf::default().hydrate(); 89 | assert_eq!(conf.unwrap(), Config { 90 | pg: PostgresConfig { 91 | host: "db-0".into(), 92 | port: 1234, 93 | password: "a strong password".into(), 94 | }, 95 | } 96 | ); 97 | env::remove_var("ROOT_PATH_FOR_HYDRO"); 98 | env::remove_var("ENV_FOR_HYDRO"); 99 | env::remove_var("HYDRO_PG__PORT"); 100 | } 101 | 102 | #[test] 103 | fn test_default_hydration_with_env_vars_only() { 104 | env::set_var("ENV_FOR_HYDRO", "production"); 105 | env::set_var("HYDRO_PG__HOST", "staging-db-23"); 106 | env::set_var("HYDRO_PG__PORT", "29378"); 107 | env::set_var("HYDRO_PG__PASSWORD", "a super strong password"); 108 | let conf: Result = Hydroconf::default().hydrate(); 109 | assert_eq!(conf.unwrap(), Config { 110 | pg: PostgresConfig { 111 | host: "staging-db-23".into(), 112 | port: 29378, 113 | password: "a super strong password".into(), 114 | }, 115 | } 116 | ); 117 | env::remove_var("ENV_FOR_HYDRO"); 118 | env::remove_var("HYDRO_PG__PORT"); 119 | env::remove_var("HYDRO_PG__HOST"); 120 | env::remove_var("HYDRO_PG__PASSWORD"); 121 | } 122 | 123 | #[test] 124 | fn test_custom_hydration() { 125 | env::set_var("HYDRO_PG__PORT", "2378"); 126 | env::set_var("MYAPP_PG___PORT", "29378"); 127 | let settings = HydroSettings::default() 128 | .set_root_path(get_data_path("")) 129 | .set_env("production".into()) 130 | .set_envvar_prefix("MYAPP".into()) 131 | .set_envvar_nested_sep("___".into()); 132 | let conf: Result = Hydroconf::new(settings).hydrate(); 133 | assert_eq!(conf.unwrap(), Config { 134 | pg: PostgresConfig { 135 | host: "db-0".into(), 136 | port: 29378, 137 | password: "a strong password".into(), 138 | }, 139 | } 140 | ); 141 | env::remove_var("HYDRO_PG__PORT"); 142 | env::remove_var("MYAPP_PG___PORT"); 143 | } 144 | 145 | #[test] 146 | fn test_multiple_dotenvs() { 147 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("2").into_os_string().into_string().unwrap()); 148 | env::set_var("ENV_FOR_HYDRO", "development"); 149 | 150 | let conf: Result = Hydroconf::default().hydrate(); 151 | assert_eq!(conf.unwrap(), Config { 152 | pg: PostgresConfig { 153 | host: "localhost".into(), 154 | port: 15330, 155 | password: "a password".into(), 156 | }, 157 | }); 158 | 159 | env::set_var("ENV_FOR_HYDRO", "production"); 160 | let conf: Result = Hydroconf::default().hydrate(); 161 | assert_eq!(conf.unwrap(), Config { 162 | pg: PostgresConfig { 163 | host: "db-0".into(), 164 | port: 12329, 165 | password: "a strong password".into(), 166 | }, 167 | }); 168 | 169 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("3").into_os_string().into_string().unwrap()); 170 | env::set_var("ENV_FOR_HYDRO", "development"); 171 | 172 | let conf: Result = Hydroconf::default().hydrate(); 173 | assert_eq!(conf.unwrap(), Config { 174 | pg: PostgresConfig { 175 | host: "localhost".into(), 176 | port: 12329, 177 | password: "a password".into(), 178 | }, 179 | }); 180 | 181 | env::set_var("ENV_FOR_HYDRO", "production"); 182 | let conf: Result = Hydroconf::default().hydrate(); 183 | assert_eq!(conf.unwrap(), Config { 184 | pg: PostgresConfig { 185 | host: "db-0".into(), 186 | port: 9999, 187 | password: "a strong password".into(), 188 | }, 189 | }); 190 | 191 | env::set_var("ROOT_PATH_FOR_HYDRO", get_data_path("3").into_os_string().into_string().unwrap()); 192 | env::set_var("ENV_FOR_HYDRO", "development"); 193 | env::set_var("ENVVAR_PREFIX_FOR_HYDRO", "APP_"); 194 | 195 | let conf: Result = Hydroconf::default().hydrate(); 196 | assert_eq!(conf.unwrap(), Config { 197 | pg: PostgresConfig { 198 | host: "localhost".into(), 199 | port: 5432, 200 | password: "a password".into(), 201 | }, 202 | }); 203 | 204 | env::set_var("ENV_FOR_HYDRO", "production"); 205 | let conf: Result = Hydroconf::default().hydrate(); 206 | assert_eq!(conf.unwrap(), Config { 207 | pg: PostgresConfig { 208 | host: "db-0".into(), 209 | port: 5432, 210 | password: "a strong password".into(), 211 | }, 212 | }); 213 | } 214 | --------------------------------------------------------------------------------