env_inventory/lib.rs
1//! `env-inventory`: A Unified Environment Variable Management Crate
2//!
3//! This crate provides utilities for easily registering and managing
4//! environment variables within your Rust applications. This ensures a
5//! centralized approach to handling environment configurations, offering a
6//! consistent method of accessing parameters from the environment.
7//!
8//! Features:
9//! - **Unified Access**: Access environment parameters uniformly from anywhere
10//! in the code.
11//! - **TOML Integration**: Allows loading of parameters from TOML configuration
12//! files.
13//! - **Precedence Handling**: Parameters loaded merge with environment
14//! variables, where the latter takes precedence.
15//! - **Registration System**: Variables of interest are registered via the
16//! provided macros, ensuring that you only focus on the ones you need.
17//!
18//! Usage involves registering variables using the provided macros, and then
19//! employing the provided utilities to load and validate these variables either
20//! from the environment or TOML files.
21//!
22//! Note: `dotenv` file support isn't available currently.
23//! Note: This crate is still in its early stages and is subject to change.
24//! Note: `shell-expansions` (probably using
25//! [https://docs.rs/shellexpand/latest/shellexpand/fn.tilde.html](shellexpand))
26//! coming soon.
27
28// ce-env/src/lib.rs
29
30#![deny(missing_docs)]
31pub extern crate inventory;
32extern crate thiserror;
33extern crate toml;
34
35use std::collections::HashMap;
36use std::collections::HashSet;
37use std::env;
38use std::fs;
39use std::path::Path;
40use thiserror::Error;
41use toml::Value;
42
43/// Registers one or more environment variables for tracking and validation.
44///
45/// This macro simplifies the process of registering environment variables that
46/// your application depends on. Once registered, you can utilize
47/// `env-inventory`'s utilities to load, validate, and manage these environment
48/// variables.
49///
50/// # Examples
51///
52/// ```rust (ignore)
53/// # #[macro_use] extern crate env_inventory;
54/// # fn main() {
55/// register!("DATABASE_URL", "REDIS_URL", "API_KEY");
56/// register!("LOG_LEVEL" => "debug", "CACHE_SIZE" => 1024);
57/// # }
58/// ```
59///
60/// The first call registers three environment variables: `DATABASE_URL`,
61/// `REDIS_URL`, and `API_KEY`. The second call registers two environment variables
62/// with default values: `LOG_LEVEL` with a default of `"debug"`, and `CACHE_SIZE`
63/// with a default of `1024`.
64///
65/// # Parameters
66///
67/// - `$($var:expr),*`: A comma-separated list of string literals, each
68/// representing an environment variable to register.
69/// - `$($var:expr => $default:expr),*`: A comma-separated list of pairs, where
70/// each pair consists of a string literal representing an environment variable
71/// and its default value.
72///
73/// # Panics
74///
75/// This macro will panic at compile-time if any of the provided arguments are
76/// not string literals or if the pairs don't have the appropriate structure.
77#[macro_export]
78macro_rules! register {
79 ($var:ident) => {
80 const _: () = {
81 use $crate::RequiredVar;
82 $crate::inventory::submit!(RequiredVar::new(stringify!($var)));
83 };
84 };
85
86 ($var:ident | $err:expr ) => {
87 const _: () = {
88 use $crate::RequiredVar;
89 $crate::inventory::submit!(RequiredVar {
90 name: stringify!($var),
91 default: None,
92 error: Some($err),
93 source: file!(),
94 priority: $crate::Priority::Library,
95 });
96 };
97 };
98
99 ($($var:ident),* $(,)?) => {
100 const _: () = {
101 use $crate::RequiredVar;
102 $(
103 $crate::register!($var);
104 )*
105 };
106 };
107
108 ($($var:ident | $err:expr),* $(,)?) => {
109 const _: () => {
110 use $crate::RequiredVar;
111 $(
112 $crate::register!($var | $err);
113 )*
114 };
115 };
116
117 ($var:ident = $default:expr) => {
118 const _: () = {
119 use $crate::RequiredVar;
120 $crate::inventory::submit!(RequiredVar {
121 name: stringify!($var),
122 default: Some($default),
123 error: None,
124 source: file!(),
125 priority: $crate::Priority::Library,
126 });
127 };
128 };
129
130 ($($var:ident = $default:expr),* $(,)?) => {
131 const _: () = {
132 use $crate::RequiredVar;
133 $(
134 $crate::register!($var = $default);
135 )*
136 };
137 };
138
139 ($var:ident = $default:expr; $priority:ident) => {
140 const _: () = {
141 use $crate::RequiredVar;
142 $crate::inventory::submit!(RequiredVar {
143 name: stringify!($var),
144 default: Some($default),
145 error: None,
146 source: file!(),
147 priority: $crate::Priority::$priority,
148 });
149 };
150 };
151
152
153 ($($var:ident = $default:expr),* $(,)?; $priority:ident) => {
154 const _: () = {
155 use $crate::RequiredVar;
156 $(
157 $crate::register!($var = $default; $priority);
158 )*
159 };
160 };
161
162}
163
164/// Represents the potential errors that can be encountered by the
165/// `env-inventory` module.
166///
167/// This enum provides specific error variants to handle different failure
168/// scenarios when working with environment variable loading and validation in
169/// the `env-inventory` module. It is designed to give users of the module clear
170/// feedback on the nature of the error encountered.
171///
172/// # Variants
173///
174/// * `ReadFileError`: Occurs when there's an issue reading a settings file.
175/// * `ParseFileError`: Occurs when parsing a settings file fails, possibly due
176/// to a malformed structure.
177/// * `MissingEnvVars`: Occurs when one or more registered environment variables
178/// are not present in either the environment or the settings files.
179///
180/// # Examples
181///
182/// ```rust (ignore)
183/// # use std::fs;
184/// # use env_inventory::EnvInventoryError;
185/// fn read_settings(file_path: &str) -> Result<(), EnvInventoryError> {
186/// if fs::read(file_path).is_err() {
187/// return Err(EnvInventoryError::ReadFileError(file_path.to_string()));
188/// }
189/// // ... Additional logic ...
190/// Ok(())
191/// }
192/// ```
193#[derive(Error, Debug)]
194pub enum EnvInventoryError {
195 /// Represents a failure to read a settings file.
196 ///
197 /// Contains a string that provides the path to the file that failed to be
198 /// read.
199 #[error("Failed to read the settings file at {0}")]
200 ReadFileError(String),
201
202 /// Represents a failure to parse a settings file.
203 ///
204 /// Contains a string that provides the path to the file that failed to be
205 /// parsed.
206 #[error("Failed to parse the settings file at {0}")]
207 ParseFileError(String),
208
209 /// Represents the absence of required environment variables.
210 ///
211 /// Contains a vector of strings, each representing a missing environment
212 /// variable.
213 #[error("Missing required environment variables: {0:?}")]
214 MissingEnvVars(Vec<String>),
215
216 /// Represents the absence of required environment variables.
217 /// variable.
218 #[error("Missing required environment variables: {0:?}")]
219 MissingEnvVar(String),
220}
221
222#[doc(hidden)]
223#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
224pub enum Priority {
225 /// The default value is from a library or unknown.
226 Unknown,
227 Library,
228 Binary,
229}
230
231#[doc(hidden)]
232#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
233pub struct RequiredVar {
234 pub name: &'static str,
235 pub default: Option<&'static str>,
236 pub error: Option<&'static str>,
237 pub source: &'static str,
238 pub priority: Priority,
239}
240impl std::fmt::Debug for RequiredVar {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 write!(
243 f,
244 "RequiredVar {{ name: {}, default: {:?}, error: {:?}, source: {}, priority: {:?} }}",
245 self.name, self.default, self.error, self.source, self.priority
246 )
247 }
248}
249
250impl std::fmt::Display for RequiredVar {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 write!(
253 f,
254 "RequiredVar {{ name: {}, default: {:?}, error: {:?}, source: {}, priority: {:?} }}",
255 self.name, self.default, self.error, self.source, self.priority
256 )
257 }
258}
259
260// This macro is used to register the `RequiredVar` struct with the inventory
261// system. It allows the `RequiredVar` instances to be collected and managed
262// by the inventory crate.
263inventory::collect!(RequiredVar);
264impl RequiredVar {
265 /// Creates a new `RequiredVar` instance at compile time.
266 pub const fn new(name: &'static str) -> Self {
267 Self {
268 name,
269 default: None,
270 error: None,
271 source: "<none>",
272 priority: Priority::Library,
273 }
274 }
275
276 /// Checks if the variable is set in the environment or has a default value.
277 pub fn is_set(&self) -> bool {
278 // If the variable is set in the environment, or
279 // we have a default value, we're good
280 env::var(self.name).is_ok() || self.default.is_some()
281 }
282
283 /// Gets the value of the variable from the environment or the default.
284 pub fn get(&self) -> Option<String> {
285 match env::var(self.name) {
286 Ok(value) => Some(value),
287 Err(_) => match self.default {
288 Some(value) => Some(value.to_string()),
289 None => None,
290 },
291 }
292 }
293}
294
295/// Validates that all registered environment variables are set.
296///
297/// This function checks if the previously registered environment variables (via
298/// the `register!` macro or other means) are present either in the system's
299/// environment or the loaded configuration files.
300///
301/// If any of the registered variables are missing, an
302/// `EnvInventoryError::MissingEnvVars` error is returned, containing a list of
303/// the missing variables.
304///
305/// # Parameters
306///
307/// * `config_paths`: A slice of file paths (as `&str`) pointing to the
308/// configuration files that might contain the environment variables. These
309/// files are expected to be in TOML format with a dedicated `[env]` section.
310/// * `section_name`: The name of the section in the TOML files that contains
311/// the environment variables. By default, this is `"env"`.
312///
313/// # Returns
314///
315/// * `Ok(())`: If all registered environment variables are found.
316/// * `Err(EnvInventoryError)`: If there's an error reading or parsing the
317/// config files or if any registered environment variable is missing.
318///
319/// # Examples
320///
321/// ```rust (ignore)
322/// # use env_inventory::validate_env_vars;
323/// let result = validate_env_vars(&["/path/to/settings.conf"], "env");
324/// if result.is_err() {
325/// eprintln!("Failed to validate environment variables: {:?}", result);
326/// }
327/// ```
328///
329/// # Errors
330///
331/// This function can return the following errors:
332/// * `ReadFileError`: If a provided config file cannot be read.
333/// * `ParseFileError`: If a provided config file cannot be parsed as TOML or
334/// lacks the expected structure.
335/// * `MissingEnvVars`: If one or more registered environment variables are
336/// missing.
337
338pub fn validate_env_vars() -> Result<(), EnvInventoryError> {
339 let missing_vars: HashSet<String> = inventory::iter::<RequiredVar>()
340 .filter_map(|var| {
341 if var.is_set() {
342 None
343 } else {
344 let msg = if let Some(err) = var.error {
345 format!("{}={}", var.name, err)
346 } else {
347 format!("{}={}", var.name, "(missing)")
348 };
349 Some(msg)
350 }
351 })
352 .collect();
353 let mut missing_vars = missing_vars.into_iter().collect::<Vec<String>>();
354 missing_vars.sort();
355
356
357 if missing_vars.is_empty() {
358 Ok(())
359 } else {
360 // tracing::warn!("Missing required environment variables: {:?}", missing_vars);
361 Err(EnvInventoryError::MissingEnvVars(missing_vars))
362 }
363}
364
365/// List all the registered environment variables.
366/// that are expected from different parts of the application.
367pub fn list_all_vars() -> Vec<String> {
368 let mut v: Vec<String> = inventory::iter::<RequiredVar>()
369 .map(|v| format!("{:#?}", v))
370 .collect();
371 v.sort();
372 v
373}
374
375/// Dump all the registered environment variables.
376
377pub fn dump_all_vars() {
378 let mut v: Vec<String> = inventory::iter::<RequiredVar>()
379 .map(|v| format!("{:#?}", v))
380 .collect();
381 v.sort();
382 dbg!(v);
383}
384
385#[doc(hidden)]
386pub fn map() -> HashMap<&'static str, String> {
387 let mut seen_vars: HashMap<&'static str, String> = HashMap::new();
388
389 for var in inventory::iter::<RequiredVar>() {
390 if !seen_vars.contains_key(var.name) {
391 if let Some(value) = var.get() {
392 seen_vars.insert(var.name, value);
393 }
394 }
395 }
396 seen_vars
397}
398
399/// Expand all the registered environment variables.
400/// that are expected from different parts of the application.
401/// So for instance if you have a variable like this:
402/// ```rust (ignore)
403/// // somewhere
404/// register!(TEST_ENV_VAR = "~/test");
405/// // elsewhere you do this:
406/// register!(LIBDIR = "${TEST_ENV_VAR}/lib");
407/// // then you can do this:
408///
409/// ```
410/// then expanded_map will update the env and expand the env.
411/// TODO:
412/// 1. This should be done in the register macro.
413pub fn expanded_map() -> Result<HashMap<String, String>, EnvInventoryError> {
414 let mut seen_vars: HashMap<String, String> = HashMap::new();
415
416 for var in inventory::iter::<RequiredVar>() {
417 if !seen_vars.contains_key(var.name) {
418 if let Some(value) = var.get() {
419 let value = shellexpand::full(&value)
420 .map_err(|e| EnvInventoryError::MissingEnvVar(e.to_string()))?
421 .to_string();
422
423 std::env::set_var(var.name, &value);
424 seen_vars.insert(var.name.to_string(), value);
425 }
426 }
427 }
428 for (key, value) in seen_vars.iter() {
429 let value = shellexpand::full(&value)
430 .map_err(|e| EnvInventoryError::MissingEnvVar(e.to_string()))?
431 .to_string();
432
433 std::env::set_var(key, &value);
434 }
435 Ok(seen_vars)
436}
437
438/// Loads the settings from a TOML file and returns them as a `HashMap`.
439pub(crate) fn load_toml_settings<P: AsRef<Path>>(
440 path: P,
441 section: &str,
442) -> Result<HashMap<String, String>, EnvInventoryError> {
443 let content = fs::read_to_string(&path)
444 .map_err(|_| EnvInventoryError::ReadFileError(path.as_ref().display().to_string()))?;
445
446 let value = content.parse::<Value>().map_err(|e| {
447 eprintln!("Error parsing TOML: {}", e);
448 EnvInventoryError::ParseFileError(path.as_ref().display().to_string())
449 })?;
450
451 let env_section = match value.get(section) {
452 Some(env) => env.as_table(),
453 None => None,
454 };
455
456 let mut settings = HashMap::new();
457
458 if let Some(env_table) = env_section {
459 for (key, val) in env_table.iter() {
460 if let Some(val_str) = val.as_str() {
461 settings.insert(key.clone(), val_str.to_string());
462 }
463 }
464 }
465 Ok(settings)
466}
467
468/// Loads environment variables from specified configuration files and validates
469/// their presence.
470///
471/// This function goes through the provided list of configuration file paths,
472/// merges the environment settings from each file, and ensures that all the
473/// registered environment variables are set. If an environment variable
474/// is not already present in the system's environment, it will be set using the
475/// value from the merged settings.
476///
477/// Environment variables present in the system's environment take precedence
478/// over those in the configuration files.
479///
480/// # Parameters
481///
482/// * `config_paths`: A slice containing paths to the configuration files that
483/// should be loaded. The files are expected to be in TOML format and have a
484/// dedicated section for environment variables.
485/// * `section`: The name of the section in the TOML files that contains the
486/// environment variables.
487///
488/// # Returns
489///
490/// * `Ok(())`: If all registered environment variables are present either in
491/// the system's environment or in the merged settings.
492/// * `Err(EnvInventoryError)`: If there's an error reading or parsing the
493/// config files or if any registered environment variable is missing.
494///
495/// # Behavior
496///
497/// The first file in the `config_paths` slice is mandatory and if it can't be
498/// read or parsed, an error is immediately returned. Subsequent files are
499/// optional, and while they will generate a warning if they cannot be read or
500/// parsed, they won't cause the function to return an error.
501///
502/// After merging the settings from all files and overlaying them on the
503/// system's environment variables, the function checks for missing required
504/// environment variables and returns an error if any are found.
505///
506/// # Examples
507///
508/// ```rust (ignore)
509/// # use env_inventory::load_and_validate_env_vars;
510/// # use std::path::Path;
511/// let paths = [Path::new("/path/to/shipped.conf"), Path::new("/path/to/system.conf")];
512/// let result = load_and_validate_env_vars(&paths, "env");
513/// if result.is_err() {
514/// eprintln!("Failed to load and validate environment variables: {:?}", result);
515/// }
516/// ```
517///
518/// # Errors
519///
520/// This function can return the following errors:
521/// * `ReadFileError`: If a provided config file cannot be read.
522/// * `ParseFileError`: If a provided config file cannot be parsed as TOML or
523/// lacks the expected structure.
524/// * `MissingEnvVars`: If one or more registered environment variables are
525/// missing.
526
527pub fn load_and_validate_env_vars<P: AsRef<Path>>(
528 config_paths: &[P],
529 section: &str,
530) -> Result<(), EnvInventoryError> {
531 let mut merged_settings = HashMap::new();
532
533 for (index, path) in config_paths.iter().enumerate() {
534 let settings = load_toml_settings(path.as_ref(), section);
535
536 match settings {
537 Ok(current_settings) => {
538 // Merge settings with nth file being most significant
539 for (key, value) in current_settings.iter() {
540 merged_settings.insert(key.clone(), value.clone());
541 }
542 }
543 Err(e) => {
544 if index == 0 {
545 // The first file is mandatory
546 eprintln!(
547 "Error: Could not load settings from {:?}. Reason: {}",
548 path.as_ref(),
549 e
550 );
551 return Err(e);
552 } else {
553 // Subsequent files are optional, but let's warn for transparency
554 eprintln!(
555 "Warning: Could not load settings from {:?}. Reason: {}",
556 path.as_ref(),
557 e
558 );
559 }
560 }
561 }
562 }
563
564 // let mut missing_vars = Vec::new();
565
566 for var in inventory::iter::<RequiredVar>() {
567 // 1) Check if set in env
568 if env::var(var.name).is_ok() {
569 continue;
570 }
571
572 // 2) Check if set in config files
573 if let Some(value) = merged_settings.get(var.name) {
574 env::set_var(var.name, value);
575 continue;
576 }
577
578 // 3) Check if set by binary
579 let binary_default = inventory::iter::<RequiredVar>()
580 .filter(|v| v.name == var.name && v.priority == Priority::Binary)
581 .last() // Get the most significant binary default
582 .and_then(|v| v.default);
583
584 if let Some(default_value) = binary_default {
585 env::set_var(var.name, default_value);
586 continue;
587 }
588
589 // 4) Check if set by library (with nth library being the most significant)
590 let library_default = inventory::iter::<RequiredVar>()
591 .filter(|v| v.name == var.name && v.priority == Priority::Library)
592 .last() // Get the most significant library default
593 .and_then(|v| v.default);
594
595 if let Some(default_value) = library_default {
596 env::set_var(var.name, default_value);
597 continue;
598 }
599
600 // 5) If not set, add to missing
601 // missing_vars.push(var.name.to_string());
602 }
603
604 expanded_map()?;
605 validate_env_vars()
606
607 // if missing_vars.is_empty() {
608 // Ok(())
609 // } else {
610 // tracing::warn!("Missing required environment variables: {:?}", missing_vars);
611 // Err(EnvInventoryError::MissingEnvVars(missing_vars))
612 // }
613}
614
615#[doc(hidden)]
616pub fn __old_load_and_validate_env_vars<P: AsRef<Path>>(
617 config_paths: &[P],
618 section: &str,
619) -> Result<(), EnvInventoryError> {
620 let mut merged_settings = HashMap::new();
621
622 for (index, path) in config_paths.iter().enumerate() {
623 let settings = load_toml_settings(path.as_ref(), section);
624
625 match settings {
626 Ok(current_settings) => {
627 // Merge settings
628 for (key, value) in current_settings.iter() {
629 if !merged_settings.contains_key(key) {
630 merged_settings.insert(key.clone(), value.clone());
631 }
632 }
633 }
634 Err(e) => {
635 if index == 0 {
636 // The first file is mandatory
637 return Err(e);
638 } else {
639 // Subsequent files are optional, but let's warn for transparency
640 eprintln!(
641 "Warning: Could not load settings from {:?}. Reason: {}",
642 path.as_ref(),
643 e
644 );
645 }
646 }
647 }
648 }
649
650 // Override the environment variables with our merged settings if they aren't
651 // already set
652 for (key, value) in merged_settings.iter() {
653 if env::var(key).is_err() {
654 env::set_var(key, value);
655 }
656 let value = env::var(key).unwrap();
657 tracing::info!("{} = {}", key, value);
658 }
659 return validate_env_vars();
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use std::env;
666 use std::fs;
667 use tempfile::tempdir;
668
669 register!(TEST_ENV_VAR = "foo"; Binary);
670
671 #[test]
672 fn test_load_single_toml() {
673 let dir = tempdir().unwrap();
674 let file_path = dir.path().join("settings.conf");
675
676 fs::write(&file_path, "[env]\nTEST_ENV_VAR = \"test_value\"").unwrap();
677
678 load_and_validate_env_vars(&[file_path], "env").unwrap();
679 assert_eq!(env::var("TEST_ENV_VAR").unwrap(), "test_value");
680 }
681
682 #[test]
683 fn test_merge_priority() {
684 let dir = tempdir().unwrap();
685 let file_path1 = dir.path().join("settings1.conf");
686 let file_path2 = dir.path().join("settings2.conf");
687 fs::write(&file_path1, "[env]\nTEST_ENV_VAR = \"value1\"").unwrap();
688 fs::write(&file_path2, "[env]\nTEST_ENV_VAR = \"value2\"").unwrap();
689
690 eprintln!("files are in {} directory", dir.path().display());
691 load_and_validate_env_vars(&[file_path1, file_path2], "env").unwrap();
692 assert_eq!(env::var("TEST_ENV_VAR").unwrap(), "value2");
693 }
694
695 #[test]
696 fn test_missing_mandatory_config() {
697 let dir = tempdir().unwrap();
698 let file_path1 = dir.path().join("does_not_exist.conf");
699 let file_path2 = dir.path().join("settings.conf");
700 fs::write(&file_path2, "[env]\nTEST_ENV_VAR = \"test_value\"").unwrap();
701
702 assert!(load_and_validate_env_vars(&[file_path1, file_path2], "env").is_err());
703 }
704
705 #[test]
706 fn test_missing_env_vars() {
707 let dir = tempdir().unwrap();
708 let file_path = dir.path().join("settings.conf");
709
710 // Write a file without MISSING_VAR
711 fs::write(&file_path, "[env]\nSOME_OTHER_VAR = \"some_value\"").unwrap();
712
713 // Ensure the environment variable isn't set before the test
714 env::remove_var("MISSING_VAR");
715
716 // Register MISSING_VAR as a required environment variable
717 register!(MISSING_VAR);
718
719 // Since MISSING_VAR isn't in the environment and also isn't in the TOML files,
720 // the function should return an error.
721 assert!(load_and_validate_env_vars(&[file_path], "env").is_err());
722 }
723
724 #[test]
725 fn test_present_env_vars() {
726 let dir = tempdir().unwrap();
727 let file_path = dir.path().join("settings.conf");
728
729 // Write a file with PRESENT_VAR
730 fs::write(
731 &file_path,
732 r#"
733 [env]
734 PRESENT_VAR = "present_value"
735 MISSING_VAR = "missing_value"
736 TEST_ENV_VAR = "test_value"
737 "#,
738 )
739 .unwrap();
740
741 // Ensure the environment variable isn't set before the test
742 env::remove_var("PRESENT_VAR");
743
744 // Register PRESENT_VAR as a required environment variable
745 register!(PRESENT_VAR = "default_value"; Binary);
746
747 // Since PRESENT_VAR is in the TOML file, the function should run without errors
748 load_and_validate_env_vars(&[file_path], "env").unwrap();
749 }
750}