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 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
//! `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.
//! Note: This crate is still in its early stages and is subject to change.
//! Note: `shell-expansions` (probably using
//! [https://docs.rs/shellexpand/latest/shellexpand/fn.tilde.html](shellexpand))
//! coming soon.
// ce-env/src/lib.rs
#![deny(missing_docs)]
pub extern crate inventory;
extern crate thiserror;
extern crate toml;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use thiserror::Error;
use toml::Value;
/// 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!("DATABASE_URL", "REDIS_URL", "API_KEY");
/// register!("LOG_LEVEL" => "debug", "CACHE_SIZE" => 1024);
/// # }
/// ```
///
/// The first call registers three environment variables: `DATABASE_URL`,
/// `REDIS_URL`, and `API_KEY`. The second call registers two environment variables
/// with default values: `LOG_LEVEL` with a default of `"debug"`, and `CACHE_SIZE`
/// with a default of `1024`.
///
/// # Parameters
///
/// - `$($var:expr),*`: A comma-separated list of string literals, each
/// representing an environment variable to register.
/// - `$($var:expr => $default:expr),*`: A comma-separated list of pairs, where
/// each pair consists of a string literal representing an environment variable
/// and its default value.
///
/// # Panics
///
/// This macro will panic at compile-time if any of the provided arguments are
/// not string literals or if the pairs don't have the appropriate structure.
#[macro_export]
macro_rules! register {
($var:ident) => {
const _: () = {
use $crate::RequiredVar;
$crate::inventory::submit!(RequiredVar::new(stringify!($var)));
};
};
($var:ident = $default:expr) => {
const _: () = {
use $crate::RequiredVar;
$crate::inventory::submit!(RequiredVar {
var_name: stringify!($var),
default: Some(ToString::to_string($default)),
});
};
};
($($var:ident),* $(,)?) => {
const _: () = {
use $crate::RequiredVar;
$(
$crate::inventory::submit!(RequiredVar::new(stringify!($var)));
)*
};
};
($($var:ident = $default:expr),* $(,)?) => {
const _: () = {
use $crate::RequiredVar;
$(
$crate::inventory::submit!(RequiredVar {
var_name: stringify!($var),
default: Some(ToString::to_string($default)),
});
)*
};
};
($var:expr) => {
const _: () = {
use $crate::RequiredVar;
$crate::inventory::submit!(RequiredVar::new($var));
};
};
($var:expr => $default:expr) => {
const _: () = {
use $crate::RequiredVar;
$crate::inventory::submit!(RequiredVar {
var_name: $var,
default: Some(ToString::to_string($default)),
});
};
};
($($var:expr),* $(,)?) => {
const _: () = {
use $crate::RequiredVar;
$(
$crate::inventory::submit!(RequiredVar::new($var));
)*
};
};
($($var:expr => $default:expr),* $(,)?) => {
const _: () = {
use $crate::RequiredVar;
$(
$crate::inventory::submit!(RequiredVar {
var_name: $var,
default: Some(ToString::to_string($default)),
});
)*
};
};
}
/// 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,
default: None,
}
}
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!` 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))
}
}
/// List all the registered environment variables.
/// that are expected from different parts of the application.
pub fn list_all_vars() -> Vec<String> {
let mut v: Vec<String> = inventory::iter::<RequiredVar>()
.map(|var| var.var_name.to_string())
.collect();
v.sort();
v
}
/// 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!("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!("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!("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();
}
}