1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
//! `env-inventory`: A Unified Environment Variable Management Crate
//!
//! This crate provides utilities for easily registering and managing environment variables
//! within your Rust applications. This ensures a centralized approach to handling environment
//! configurations, offering a consistent method of accessing parameters from the environment.
//!
//! Features:
//! - **Unified Access**: Access environment parameters uniformly from anywhere in the code.
//! - **TOML Integration**: Allows loading of parameters from TOML configuration files.
//! - **Precedence Handling**: Parameters loaded merge with environment variables, where the latter takes precedence.
//! - **Registration System**: Variables of interest are registered via the provided macros, ensuring that you only
//! focus on the ones you need.
//!
//! Usage involves registering variables using the provided macros, and then employing the
//! provided utilities to load and validate these variables either from the environment or TOML files.
//!
//! Note: `.dotenv` file support isn't available currently.
// ce-env/src/lib.rs
#![deny(missing_docs)]
extern crate inventory;
extern crate toml;
extern crate thiserror;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use toml::Value;
use thiserror::Error;
/// Registers one or more environment variables for tracking and validation.
///
/// This macro simplifies the process of registering environment variables that your application
/// depends on. Once registered, you can utilize `env-inventory`'s utilities to load, validate,
/// and manage these environment variables.
///
/// # Examples
///
/// ```rust (ignore)
/// # #[macro_use] extern crate env_inventory;
/// # fn main() {
/// register_env_vars!("DATABASE_URL", "REDIS_URL", "API_KEY");
/// # }
/// ```
///
/// The above registers three environment variables: `DATABASE_URL`, `REDIS_URL`, and `API_KEY`.
///
/// # Parameters
///
/// - `$($var:expr),*`: A comma-separated list of string literals, each representing an environment variable to register.
///
/// # Panics
///
/// This macro will panic at compile-time if any of the provided arguments are not string literals.
#[macro_export]
macro_rules! register_env_vars {
($($var:expr),* $(,)?) => {
$(
inventory::submit!(RequiredVar::new($var));
)*
};
}
/// Represents the potential errors that can be encountered by the `env-inventory` module.
///
/// This enum provides specific error variants to handle different failure scenarios when working
/// with environment variable loading and validation in the `env-inventory` module. It is designed
/// to give users of the module clear feedback on the nature of the error encountered.
///
/// # Variants
///
/// * `ReadFileError`: Occurs when there's an issue reading a settings file.
/// * `ParseFileError`: Occurs when parsing a settings file fails, possibly due to a malformed structure.
/// * `MissingEnvVars`: Occurs when one or more registered environment variables are not present
/// in either the environment or the settings files.
///
/// # Examples
///
/// ```rust (ignore)
/// # use std::fs;
/// # use env_inventory::EnvInventoryError;
/// fn read_settings(file_path: &str) -> Result<(), EnvInventoryError> {
/// if fs::read(file_path).is_err() {
/// return Err(EnvInventoryError::ReadFileError(file_path.to_string()));
/// }
/// // ... Additional logic ...
/// Ok(())
/// }
/// ```
#[derive(Error, Debug)]
pub enum EnvInventoryError {
/// Represents a failure to read a settings file.
///
/// Contains a string that provides the path to the file that failed to be read.
#[error("Failed to read the settings file at {0}")]
ReadFileError(String),
/// Represents a failure to parse a settings file.
///
/// Contains a string that provides the path to the file that failed to be parsed.
#[error("Failed to parse the settings file at {0}")]
ParseFileError(String),
/// Represents the absence of required environment variables.
///
/// Contains a vector of strings, each representing a missing environment variable.
#[error("Missing required environment variables: {0:?}")]
MissingEnvVars(Vec<String>),
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub struct RequiredVar {
pub var_name: &'static str,
// pub default: Option<&'static str>,
}
inventory::collect!(RequiredVar);
impl RequiredVar {
pub const fn new(var_name: &'static str) -> Self {
Self { var_name }
}
pub fn is_set(&self) -> bool {
env::var(self.var_name).is_ok()
}
}
/// Validates that all registered environment variables are set.
///
/// This function checks if the previously registered environment variables (via the `register_env_vars!` macro or
/// other means) are present either in the system's environment or the loaded configuration files.
///
/// If any of the registered variables are missing, an `EnvInventoryError::MissingEnvVars` error is returned,
/// containing a list of the missing variables.
///
/// # Parameters
///
/// * `config_paths`: A slice of file paths (as `&str`) pointing to the configuration files that might contain
/// the environment variables. These files are expected to be in TOML format with a dedicated `[env]` section.
/// * `section_name`: The name of the section in the TOML files that contains the environment variables. By default,
/// this is `"env"`.
///
/// # Returns
///
/// * `Ok(())`: If all registered environment variables are found.
/// * `Err(EnvInventoryError)`: If there's an error reading or parsing the config files or if any registered environment
/// variable is missing.
///
/// # Examples
///
/// ```rust (ignore)
/// # use env_inventory::validate_env_vars;
/// let result = validate_env_vars(&["/path/to/settings.conf"], "env");
/// if result.is_err() {
/// eprintln!("Failed to validate environment variables: {:?}", result);
/// }
/// ```
///
/// # Errors
///
/// This function can return the following errors:
/// * `ReadFileError`: If a provided config file cannot be read.
/// * `ParseFileError`: If a provided config file cannot be parsed as TOML or lacks the expected structure.
/// * `MissingEnvVars`: If one or more registered environment variables are missing.
pub fn validate_env_vars() -> Result<(), EnvInventoryError> {
let missing_vars: Vec<String> = inventory::iter::<RequiredVar>()
.filter_map(|var| {
if var.is_set() {
None
} else {
Some(var.var_name.to_string())
}
})
.collect();
if missing_vars.is_empty() {
Ok(())
} else {
type E = EnvInventoryError;
Err(E::MissingEnvVars(missing_vars))
}
}
/// Loads the settings from a TOML file and returns them as a `HashMap`.
pub(crate) fn load_toml_settings<P: AsRef<Path>>(path: P, section: &str) -> Result<HashMap<String, String>, EnvInventoryError> {
let content = fs::read_to_string(&path)
.map_err(|_| EnvInventoryError::ReadFileError(path.as_ref().display().to_string()))?;
let value = content.parse::<Value>()
.map_err(|_| EnvInventoryError::ParseFileError(path.as_ref().display().to_string()))?;
let env_section = match value.get(section) {
Some(env) => env.as_table(),
None => None,
};
let mut settings = HashMap::new();
if let Some(env_table) = env_section {
for (key, val) in env_table.iter() {
if let Some(val_str) = val.as_str() {
settings.insert(key.clone(), val_str.to_string());
}
}
}
Ok(settings)
}
/// Loads environment variables from specified configuration files and validates their presence.
///
/// This function goes through the provided list of configuration file paths, merges the environment settings
/// from each file, and ensures that all the registered environment variables are set. If an environment variable
/// is not already present in the system's environment, it will be set using the value from the merged settings.
///
/// Environment variables present in the system's environment take precedence over those in the configuration files.
///
/// # Parameters
///
/// * `config_paths`: A slice containing paths to the configuration files that should be loaded. The files
/// are expected to be in TOML format and have a dedicated section for environment variables.
/// * `section`: The name of the section in the TOML files that contains the environment variables.
///
/// # Returns
///
/// * `Ok(())`: If all registered environment variables are present either in the system's environment or in the merged settings.
/// * `Err(EnvInventoryError)`: If there's an error reading or parsing the config files or if any registered environment variable is missing.
///
/// # Behavior
///
/// The first file in the `config_paths` slice is mandatory and if it can't be read or parsed, an error is immediately returned.
/// Subsequent files are optional, and while they will generate a warning if they cannot be read or parsed, they won't
/// cause the function to return an error.
///
/// After merging the settings from all files and overlaying them on the system's environment variables, the function
/// checks for missing required environment variables and returns an error if any are found.
///
/// # Examples
///
/// ```rust (ignore)
/// # use env_inventory::load_and_validate_env_vars;
/// # use std::path::Path;
/// let paths = [Path::new("/path/to/shipped.conf"), Path::new("/path/to/system.conf")];
/// let result = load_and_validate_env_vars(&paths, "env");
/// if result.is_err() {
/// eprintln!("Failed to load and validate environment variables: {:?}", result);
/// }
/// ```
///
/// # Errors
///
/// This function can return the following errors:
/// * `ReadFileError`: If a provided config file cannot be read.
/// * `ParseFileError`: If a provided config file cannot be parsed as TOML or lacks the expected structure.
/// * `MissingEnvVars`: If one or more registered environment variables are missing.
pub fn load_and_validate_env_vars<P: AsRef<Path>>(config_paths: &[P], section: &str)
-> Result<(), EnvInventoryError>
{
let mut merged_settings = HashMap::new();
for (index, path) in config_paths.iter().enumerate() {
let settings = load_toml_settings(path.as_ref(), section);
match settings {
Ok(current_settings) => {
// Merge settings
for (key, value) in current_settings.iter() {
if !merged_settings.contains_key(key) {
merged_settings.insert(key.clone(), value.clone());
}
}
}
Err(e) => {
if index == 0 {
// The first file is mandatory
return Err(e);
} else {
// Subsequent files are optional, but let's warn for transparency
eprintln!("Warning: Could not load settings from {:?}. Reason: {}", path.as_ref(), e);
}
}
}
}
// Override the environment variables with our merged settings if they aren't already set
for (key, value) in merged_settings.iter() {
if env::var(key).is_err() {
env::set_var(key, value);
}
let value = env::var(key).unwrap();
tracing::info!("{} = {}", key, value);
}
let missing_vars: Vec<String> = inventory::iter::<RequiredVar>()
.filter_map(|var| if var.is_set() { None } else { Some(var.var_name.to_string()) })
.collect();
if missing_vars.is_empty() {
Ok(())
} else {
tracing::warn!("Missing required environment variables: {:?}", missing_vars);
Err(EnvInventoryError::MissingEnvVars(missing_vars))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use tempfile::tempdir;
register_env_vars!("TEST_ENV_VAR");
#[test]
fn test_load_single_toml() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("settings.conf");
fs::write(&file_path, "[env]\nTEST_ENV_VAR = \"test_value\"").unwrap();
load_and_validate_env_vars(&[file_path], "env").unwrap();
assert_eq!(env::var("TEST_ENV_VAR").unwrap(), "test_value");
}
#[test]
fn test_merge_priority() {
let dir = tempdir().unwrap();
let file_path1 = dir.path().join("settings1.conf");
let file_path2 = dir.path().join("settings2.conf");
fs::write(&file_path1, "[env]\nTEST_ENV_VAR = \"value1\"").unwrap();
fs::write(&file_path2, "[env]\nTEST_ENV_VAR = \"value2\"").unwrap();
load_and_validate_env_vars(&[file_path2, file_path1], "env").unwrap();
assert_eq!(env::var("TEST_ENV_VAR").unwrap(), "value2");
}
#[test]
fn test_missing_mandatory_config() {
let dir = tempdir().unwrap();
let file_path1 = dir.path().join("does_not_exist.conf");
let file_path2 = dir.path().join("settings.conf");
fs::write(&file_path2, "[env]\nTEST_ENV_VAR = \"test_value\"").unwrap();
assert!(load_and_validate_env_vars(&[file_path1, file_path2], "env").is_err());
}
#[test]
fn test_missing_env_vars() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("settings.conf");
// Write a file without MISSING_VAR
fs::write(&file_path, "[env]\nSOME_OTHER_VAR = \"some_value\"").unwrap();
// Ensure the environment variable isn't set before the test
env::remove_var("MISSING_VAR");
// Register MISSING_VAR as a required environment variable
register_env_vars!("MISSING_VAR");
// Since MISSING_VAR isn't in the environment and also isn't in the TOML files,
// the function should return an error.
assert!(load_and_validate_env_vars(&[file_path], "env").is_err());
}
#[test]
fn test_present_env_vars() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("settings.conf");
// Write a file with PRESENT_VAR
fs::write(&file_path, r#"
[env]
PRESENT_VAR = "present_value"
MISSING_VAR = "missing_value"
TEST_ENV_VAR = "test_value"
"#).unwrap();
// Ensure the environment variable isn't set before the test
env::remove_var("PRESENT_VAR");
// Register PRESENT_VAR as a required environment variable
register_env_vars!("PRESENT_VAR");
// Since PRESENT_VAR is in the TOML file, the function should run without errors
load_and_validate_env_vars(&[file_path], "env").unwrap();
}
}