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}