toml_env/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2#![deny(missing_docs)]
3
4// NOTE: This crate intentionally uses a single module in order to put pressure on keeping the
5// feature list small.
6
7use std::{
8    borrow::Cow,
9    collections::{BTreeMap, HashMap},
10    path::{Path, PathBuf},
11    str::FromStr,
12};
13
14use serde::{de::DeserializeOwned, Serialize};
15use thiserror::Error;
16use toml::Value;
17
18/// Convenience type shorthand for `Result<T, Error>`.
19pub type Result<T> = std::result::Result<T, Error>;
20
21/// Default name for attempting to load the configuration (and environment variables) from a file.
22pub const DEFAULT_DOTENV_PATH: &str = ".env.toml";
23
24/// Default environment variable name to use for loading configuration from. Also the same name
25/// used for the table of the configuration within the `.env.toml`.
26pub const DEFAULT_CONFIG_VARIABLE_NAME: &str = "CONFIG";
27
28/// The default divider between different levels of parent.child in environment variable names.
29/// This will be replaced with a `.` for the [`TomlKeyPath`].
30pub const DEFAULT_MAP_ENV_DIVIDER: &str = "__";
31
32/// A source of configuration.
33#[derive(Debug, Clone)]
34pub enum ConfigSource {
35    /// From two configuration sources merged together.
36    Merged {
37        /// Merged from.
38        from: Box<Self>,
39        /// Merged into.
40        into: Box<Self>,
41    },
42    /// From a `.toml.env` file (Path may be different if user has specified something different).
43    DotEnv(PathBuf),
44    /// From a configuration file.
45    File(PathBuf),
46    /// From environment variables.
47    Environment {
48        /// The names of the environment variables.
49        variable_names: Vec<String>,
50    },
51}
52
53impl std::fmt::Display for ConfigSource {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            ConfigSource::Merged { from, into } => write!(f, "({from}) merged into ({into})"),
57            ConfigSource::DotEnv(path) => write!(f, "dotenv TOML file {path:?}"),
58            ConfigSource::File(path) => write!(f, "config TOML file {path:?}"),
59            ConfigSource::Environment { variable_names } => {
60                let variable_names = variable_names.join(", ");
61                write!(f, "environment variables {variable_names}")
62            }
63        }
64    }
65}
66
67/// An error that occurs while initializing configuration.
68#[derive(Debug, Error)]
69#[error(transparent)]
70pub struct Error(#[from] InnerError);
71
72/// An error that occurs while initializing configuration.
73#[derive(Debug, Error)]
74#[non_exhaustive]
75enum InnerError {
76    /// Error reading environment variable.
77    #[error("Error reading {name} environment variable")]
78    ErrorReadingEnvironmentVariable {
79        /// Name of the environment variable.
80        name: String,
81        /// Source of the error.
82        #[source]
83        error: std::env::VarError,
84    },
85    /// Error reading TOML file.
86    #[error("Error reading TOML file {path:?}")]
87    ErrorReadingFile {
88        /// Path to the file.
89        path: PathBuf,
90        /// Source of the error.
91        #[source]
92        error: std::io::Error,
93    },
94    /// Error parsing TOML file.
95    #[error("Error parsing TOML file {path:?}")]
96    ErrorParsingTomlFile {
97        /// Path to the file.
98        path: PathBuf,
99        /// Source of the error.
100        #[source]
101        error: Box<toml::de::Error>,
102    },
103    /// Cannot parse a table in the `.toml.env` file.
104    #[error("Cannot parse {key} as environment variable in {path:?}. Advice: {advice}")]
105    CannotParseTomlDotEnvFile {
106        /// Key in the TOML file.
107        key: String,
108        /// Path to the file.
109        path: PathBuf,
110        /// Advice
111        advice: String,
112    },
113    /// Error parsing envirnment variable
114    #[error("Error parsing config key ({name}) in TOML config file {path:?}")]
115    ErrorParsingTomlDotEnvFileKey {
116        /// Name of the key variable.
117        name: String,
118        /// Path to the file.
119        path: PathBuf,
120        /// Source of the error.
121        #[source]
122        error: Box<toml::de::Error>,
123    },
124    /// Either there was an error parsing the environment variable as the config, or if the value
125    /// is a filename, it does not exist.
126    #[error(
127        "Error parsing config environment variable ({name}={value:?}) as the config or if it is a filename, the file does not exist."
128    )]
129    ErrorParsingEnvironmentVariableAsConfigOrFile {
130        /// Name of the environment variable.
131        name: String,
132        /// Value of the environment variable.
133        value: String,
134        /// Source of the error.
135        #[source]
136        error: Box<toml::de::Error>,
137    },
138    /// Error parsing file as `.env.toml` format.
139    #[error(
140        "Error parsing the {path:?} as `.env.toml` format file:\n{value:#?}\nTop level should be a table."
141    )]
142    UnexpectedTomlDotEnvFileFormat {
143        /// Path to file.
144        path: PathBuf,
145        /// Value that was unable to be parsed as `.env.toml` format
146        value: Value,
147    },
148    /// Error parsing merged configuration.
149    #[error("Error parsing merged configuration from {source}")]
150    ErrorParsingMergedToml {
151        /// Source(s) of the configuration.
152        source: ConfigSource,
153        /// Source of the error.
154        #[source]
155        error: Box<toml::de::Error>,
156    },
157    /// Error merging configurations.
158    #[error("Error merging configuration {from} into {into}: {error}")]
159    ErrorMerging {
160        /// Error merging from this source.
161        from: ConfigSource,
162        /// Error merging into this source.
163        into: ConfigSource,
164        /// Source of the error.
165        error: serde_toml_merge::Error,
166    },
167    #[error("Error inserting toml value")]
168    InsertTomlValueError(#[from] InsertTomlValueError),
169}
170
171/// What method of logging for this library to use.
172#[derive(Default, Clone, Copy)]
173pub enum Logging {
174    /// Don't perform any logging
175    #[default]
176    None,
177    /// Use STDOUT for logging. This may be attractive if you are relying on the output of this
178    /// library to configure your logging system and still want to see what's going on here before
179    /// the system is configured.
180    StdOut,
181    /// Use the [`log`] crate for logging.
182    #[cfg(feature = "log")]
183    Log,
184}
185
186type InnerResult<T> = std::result::Result<T, InnerError>;
187
188/// A path to a key into a [`toml::Value`]. In the format of `key.0.key` (`0` for indexing into an
189/// array) when parsed using [`FromStr`].
190///
191/// See [`TomlKeyPath::resolve()`] for an example.
192#[derive(Debug, Clone, Default)]
193pub struct TomlKeyPath(Vec<PathElement>);
194
195#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
196enum PathElement {
197    TableProperty(String),
198    ArrayIndex(usize),
199}
200
201impl std::fmt::Display for PathElement {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match self {
204            PathElement::TableProperty(p) => p.fmt(f),
205            PathElement::ArrayIndex(i) => i.fmt(f),
206        }
207    }
208}
209
210impl TomlKeyPath {
211    /// Resolve a value contained within a [`toml::Value`] using this [`TomlKeyPath`].
212    ///
213    /// # Example
214    ///
215    /// ```rust
216    /// use toml_env::TomlKeyPath;
217    /// use toml;
218    ///
219    /// let toml_value = toml::from_str(r#"
220    /// key="value1"
221    /// array=["hello", "world"]
222    /// [child]
223    /// key="value2"
224    /// "#).unwrap();
225    ///
226    /// let key1: TomlKeyPath = "key".parse().unwrap();
227    /// let key1_value = key1.resolve(&toml_value)
228    ///     .expect("Expected to resolve")
229    ///     .as_str()
230    ///     .expect("Expected to be a string");
231    /// assert_eq!(key1_value, "value1");
232    ///
233    /// let key2: TomlKeyPath = "child.key".parse().unwrap();
234    /// let key2_value = key2.resolve(&toml_value)
235    ///     .expect("Expected to resolve")
236    ///     .as_str()
237    ///     .expect("Expected to be a string");
238    /// assert_eq!(key2_value, "value2");
239    ///
240    /// let hello: TomlKeyPath = "array.0".parse().unwrap();
241    /// let hello_value = hello.resolve(&toml_value)
242    ///     .expect("Expected to resolve")
243    ///     .as_str()
244    ///     .expect("Expected to be a string");
245    /// assert_eq!(hello_value, "hello");
246    /// ```
247    pub fn resolve<'a>(&self, value: &'a toml::Value) -> Option<&'a toml::Value> {
248        Self::resolve_impl(&mut self.clone(), value)
249    }
250
251    fn resolve_impl<'a>(key: &mut Self, value: &'a toml::Value) -> Option<&'a toml::Value> {
252        if key.0.is_empty() {
253            return Some(value);
254        }
255
256        let current_key = key.0.remove(0);
257
258        match value {
259            Value::Table(table) => match current_key {
260                PathElement::TableProperty(p) => {
261                    let value = table.get(&p)?;
262                    Self::resolve_impl(key, value)
263                }
264                PathElement::ArrayIndex(_) => None,
265            },
266            Value::Array(array) => match current_key {
267                PathElement::ArrayIndex(i) => {
268                    let value = array.get(i)?;
269                    Self::resolve_impl(key, value)
270                }
271                PathElement::TableProperty(_) => None,
272            },
273            _ => None,
274        }
275    }
276}
277
278impl std::fmt::Display for TomlKeyPath {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        f.write_str(
281            &self
282                .0
283                .iter()
284                .map(|e| e.to_string())
285                .collect::<Vec<_>>()
286                .join("."),
287        )
288    }
289}
290
291impl FromStr for TomlKeyPath {
292    type Err = ();
293
294    /// Parse a string into a [`TomlKeyPath`].
295    ///
296    /// # Example
297    ///
298    /// ```rust
299    /// use toml_env::TomlKeyPath;
300    ///
301    /// "key".parse::<TomlKeyPath>().unwrap();
302    /// "key.0".parse::<TomlKeyPath>().unwrap();
303    /// "key.key".parse::<TomlKeyPath>().unwrap();
304    /// "key.0.key".parse::<TomlKeyPath>().unwrap();
305    /// "key.key.key".parse::<TomlKeyPath>().unwrap();
306    /// "".parse::<TomlKeyPath>().unwrap();
307    /// ".".parse::<TomlKeyPath>().unwrap();
308    /// ".key".parse::<TomlKeyPath>().unwrap();
309    /// "key.".parse::<TomlKeyPath>().unwrap();
310    /// ```
311
312    fn from_str(s: &str) -> std::result::Result<Self, ()> {
313        if s.is_empty() {
314            return Ok(Self::default());
315        }
316
317        let v: Vec<PathElement> = s
318            .split('.')
319            .filter_map(|k| {
320                if k.is_empty() {
321                    None
322                } else {
323                    if let Ok(i) = usize::from_str(k) {
324                        Some(PathElement::ArrayIndex(i))
325                    } else {
326                        Some(PathElement::TableProperty(k.to_owned()))
327                    }
328                }
329            })
330            .collect();
331
332        Ok(Self(v))
333    }
334}
335
336/// Automatically map environment variables into config.
337pub struct AutoMapEnvArgs<'a> {
338    /// The divider that separates different levels of the parent.child relationship for the
339    /// mapping. This will get replaced with `.` when converting the name of a variable to a [`TomlKeyPath`]. The default value is [`DEFAULT_DOTENV_PATH`].
340    pub divider: &'a str,
341    /// Prefix for environment variables to be mapped. By default this will be [`DEFAULT_CONFIG_VARIABLE_NAME`].
342    pub prefix: Option<&'a str>,
343    /// A transform operation to perform on the environment variable before parsing it. By default
344    /// this transforms it to lowercase.
345    pub transform: Box<dyn Fn(&str) -> String>,
346}
347
348impl Default for AutoMapEnvArgs<'_> {
349    fn default() -> Self {
350        Self {
351            divider: DEFAULT_MAP_ENV_DIVIDER,
352            prefix: None,
353            transform: Box::new(|name| name.to_lowercase()),
354        }
355    }
356}
357
358/// Args as input to [`initialize()`].
359pub struct Args<'a> {
360    /// Path to `.env.toml` format file. The value is [`DEFAULT_DOTENV_PATH`] by default.
361    pub dotenv_path: &'a Path,
362    /// Path to a config file to load.
363    pub config_path: Option<&'a Path>,
364    /// Name of the environment variable to use that stores the config. The value is [`DEFAULT_CONFIG_VARIABLE_NAME`] by default.
365    pub config_variable_name: &'a str,
366    /// What method of logging to use (if any). [`Logging::None`] by default.
367    pub logging: Logging,
368    /// Map the specified environment variables into config keys.
369    pub map_env: HashMap<&'a str, TomlKeyPath>,
370    /// See [`AutoMapEnvArgs`].
371    pub auto_map_env: Option<AutoMapEnvArgs<'a>>,
372}
373
374impl Default for Args<'static> {
375    fn default() -> Self {
376        Self {
377            dotenv_path: Path::new(DEFAULT_DOTENV_PATH),
378            config_path: None,
379            config_variable_name: DEFAULT_CONFIG_VARIABLE_NAME,
380            logging: Logging::default(),
381            map_env: HashMap::default(),
382            auto_map_env: None,
383        }
384    }
385}
386
387fn log_info(logging: Logging, args: std::fmt::Arguments<'_>) {
388    match logging {
389        Logging::None => {}
390        Logging::StdOut => println!("INFO {}: {}", module_path!(), std::fmt::format(args)),
391        #[cfg(feature = "log")]
392        Logging::Log => log::info!("{}", std::fmt::format(args)),
393    }
394}
395
396/// Reads and parses the .env.toml file (or whatever is specified in `dotenv_path`). Returns
397/// `Some(C)` if the file contains a table with the name matching `config_variable_name`.
398fn initialize_dotenv_toml<'a, C: DeserializeOwned + Serialize>(
399    dotenv_path: &'a Path,
400    config_variable_name: &'a str,
401    logging: Logging,
402) -> InnerResult<Option<C>> {
403    let path = Path::new(dotenv_path);
404    if !path.exists() {
405        return Ok(None);
406    }
407
408    log_info(
409        logging,
410        format_args!("Loading config and environment variables from dotenv {path:?}"),
411    );
412
413    let env_str = std::fs::read_to_string(path).map_err(|error| InnerError::ErrorReadingFile {
414        path: path.to_owned(),
415        error,
416    })?;
417    let env: Value =
418        toml::from_str(&env_str).map_err(|error| InnerError::ErrorParsingTomlFile {
419            path: path.to_owned(),
420            error: error.into(),
421        })?;
422    let table: toml::value::Table = match env {
423        Value::Table(table) => table,
424        unexpected => {
425            return Err(InnerError::UnexpectedTomlDotEnvFileFormat {
426                path: path.to_owned(),
427                value: unexpected,
428            });
429        }
430    };
431
432    if table.is_empty() {
433        return Ok(None);
434    }
435
436    let mut config: Option<C> = None;
437    let mut set_keys: String = String::new();
438    for (key, value) in table {
439        let value_string = match value {
440            Value::Table(_) => {
441                if key.as_str() != config_variable_name {
442                    return Err(InnerError::CannotParseTomlDotEnvFile {
443                        key,
444                        path: path.to_owned(),
445                        advice: format!("Only a table with {config_variable_name} is allowed in a .toml.env format file."),
446                    });
447                }
448                match C::deserialize(value.clone()) {
449                    Ok(c) => config = Some(c),
450                    Err(error) => {
451                        return Err(InnerError::ErrorParsingTomlDotEnvFileKey {
452                            name: key,
453                            path: path.to_owned(),
454                            error: error.into(),
455                        })
456                    }
457                }
458                None
459            }
460            Value::String(value) => Some(value),
461            Value::Integer(value) => Some(value.to_string()),
462            Value::Float(value) => Some(value.to_string()),
463            Value::Boolean(value) => Some(value.to_string()),
464            Value::Datetime(value) => Some(value.to_string()),
465            Value::Array(value) => {
466                return Err(InnerError::CannotParseTomlDotEnvFile {
467                    key,
468                    path: path.to_owned(),
469                    advice: format!("Array values are not supported: {value:?}"),
470                })
471            }
472        };
473
474        if let Some(value_string) = value_string {
475            set_keys.push('\n');
476            set_keys.push_str(key.as_str());
477            std::env::set_var(key.as_str(), value_string)
478        }
479    }
480
481    log_info(
482        logging,
483        format_args!(
484            "Set environment variables specified in {dotenv_path:?}:\x1b[34m{set_keys}\x1b[0m"
485        ),
486    );
487    Ok(config)
488}
489
490#[derive(Debug, thiserror::Error)]
491enum InsertTomlValueError {
492    #[error("Table property {property:?} can only be used to index into a table. Cannot index into {value:?}")]
493    TablePropertyCannotIndex {
494        property: String,
495        value: toml::Value,
496    },
497    #[error(
498        "Array index {index} can only be used to index into an array. Cannot index into {value:?}"
499    )]
500    ArrayIndexCannotIndex { index: usize, value: toml::Value },
501    #[error("Array index {index} cannot be greater than the length of {array:?}")]
502    ArrayOutOfBounds {
503        index: usize,
504        array: Vec<toml::Value>,
505    },
506}
507
508/// Insert a `new_value` into a `value` at the location specified by `path`, creating any required
509/// tables or arrays if they are missing. If the `path` is empty, it will replace the value
510/// entirely.
511fn insert_toml_value(
512    value: &mut toml::Value,
513    mut path: TomlKeyPath,
514    new_value: Value,
515) -> std::result::Result<(), InsertTomlValueError> {
516    if path.0.is_empty() {
517        *value = new_value;
518        return Ok(());
519    }
520
521    let current_key = path.0.remove(0);
522    let next_key = path.0.get(0);
523
524    match (current_key, value) {
525        (PathElement::TableProperty(property), Value::Table(table)) => {
526            let next_value = table.get_mut(&property);
527            match (next_value, next_key) {
528                (None, None) => {
529                    table.insert(property, new_value);
530                    return Ok(());
531                }
532                (None, Some(PathElement::ArrayIndex(_))) => {
533                    table.insert(property.clone(), toml::Value::Array(Vec::with_capacity(1)));
534                    return insert_toml_value(
535                        table
536                            .get_mut(&property)
537                            .expect("Expect inserted property to be present"),
538                        path,
539                        new_value,
540                    );
541                }
542                (None, Some(PathElement::TableProperty(_))) => {
543                    table.insert(
544                        property.clone(),
545                        toml::Value::Table(toml::Table::with_capacity(1)),
546                    );
547                    return insert_toml_value(
548                        table
549                            .get_mut(&property)
550                            .expect("Expect inserted property to be present"),
551                        path,
552                        new_value,
553                    );
554                }
555                (Some(next_value), None) => {
556                    *next_value = new_value;
557                    return Ok(());
558                }
559                (Some(next_value), Some(_)) => {
560                    return insert_toml_value(next_value, path, new_value)
561                }
562            }
563        }
564        (PathElement::TableProperty(property), value) => {
565            return Err(InsertTomlValueError::TablePropertyCannotIndex {
566                property,
567                value: value.clone(),
568            })
569        }
570        (PathElement::ArrayIndex(index), Value::Array(array)) => {
571            if index > array.len() {
572                return Err(InsertTomlValueError::ArrayOutOfBounds {
573                    index,
574                    array: array.clone(),
575                });
576            }
577            let next_value = array.get_mut(index);
578            match (next_value, next_key) {
579                (None, None) => {
580                    array.insert(index, new_value);
581                    return Ok(());
582                }
583                (None, Some(PathElement::ArrayIndex(_))) => {
584                    array.insert(index, toml::Value::Array(Vec::with_capacity(1)));
585                    return insert_toml_value(
586                        array
587                            .get_mut(index)
588                            .expect("Expect inserted element to be present"),
589                        path,
590                        new_value,
591                    );
592                }
593                (None, Some(PathElement::TableProperty(_))) => {
594                    array.insert(index, toml::Value::Table(toml::Table::with_capacity(1)));
595                    return insert_toml_value(
596                        array
597                            .get_mut(index)
598                            .expect("Expect inserted element to be present"),
599                        path,
600                        new_value,
601                    );
602                }
603                (Some(next_value), None) => {
604                    *next_value = new_value;
605                    return Ok(());
606                }
607                (Some(next_value), Some(_)) => {
608                    return insert_toml_value(next_value, path, new_value)
609                }
610            }
611        }
612        (PathElement::ArrayIndex(index), value) => {
613            Err(InsertTomlValueError::ArrayIndexCannotIndex {
614                index,
615                value: value.clone(),
616            })
617        }
618    }
619}
620
621/// Initialize from environment variables.
622fn initialize_env(
623    logging: Logging,
624    map_env: HashMap<&'_ str, TomlKeyPath>,
625    auto_args: Option<AutoMapEnvArgs<'_>>,
626    config_variable_name: &'_ str,
627) -> InnerResult<Option<Value>> {
628    fn parse_toml_value(value: String) -> Value {
629        if let Ok(value) = bool::from_str(&value) {
630            return Value::Boolean(value);
631        }
632        if let Ok(value) = f64::from_str(&value) {
633            return Value::Float(value);
634        }
635        if let Ok(value) = i64::from_str(&value) {
636            return Value::Integer(value);
637        }
638        if let Ok(value) = toml::value::Datetime::from_str(&value) {
639            return Value::Datetime(value);
640        }
641
642        Value::String(value)
643    }
644
645    // Using a BTreeMap to ensure values are sorted by environment variable, so that array indices
646    // are in the correct order of insertion to avoid an out of bounds.
647    let mut map_env: BTreeMap<Cow<'_, str>, TomlKeyPath> = map_env
648        .into_iter()
649        .map(|(key, value)| (Cow::Borrowed(key), value))
650        .collect();
651
652    if let Some(auto_args) = auto_args {
653        let mut prefix = auto_args.prefix.unwrap_or(config_variable_name).to_owned();
654        prefix.push_str(auto_args.divider);
655        for (key, _) in std::env::vars_os() {
656            let key = if let Some(key) = key.to_str() {
657                key.to_owned()
658            } else {
659                continue;
660            };
661
662            let key_without_prefix: &str = if let Some(0) = key.find(&prefix) {
663                key.split_at(prefix.len()).1
664            } else {
665                continue;
666            };
667
668            let key_transformed = (auto_args.transform)(key_without_prefix);
669            let toml_key: TomlKeyPath =
670                if let Ok(key) = key_transformed.replace(auto_args.divider, ".").parse() {
671                    key
672                } else {
673                    continue;
674                };
675
676            map_env.entry(key.into()).or_insert(toml_key);
677        }
678    }
679
680    if map_env.is_empty() {
681        return Ok(None);
682    }
683
684    if !matches!(logging, Logging::None) {
685        let mut buffer = String::new();
686        buffer.push_str("\x1b[34m");
687        for (k, v) in &map_env {
688            if std::env::var(k.as_ref()).is_ok() {
689                buffer.push_str(&format!("\n{k} => {v}"));
690            }
691        }
692        buffer.push_str("\x1b[0m");
693        log_info(
694            logging,
695            format_args!("Loading config from current environment variables: {buffer}"),
696        );
697    }
698
699    log_info(logging, format_args!("Loading config from environment"));
700
701    let mut config = toml::Value::Table(toml::Table::new());
702    for (variable_name, toml_key) in map_env {
703        let value = match std::env::var(variable_name.as_ref()) {
704            Ok(value) => value,
705            Err(std::env::VarError::NotPresent) => continue,
706            Err(error) => {
707                return Err(InnerError::ErrorReadingEnvironmentVariable {
708                    name: (*variable_name.into_owned()).to_owned(),
709                    error,
710                })
711            }
712        };
713        let value = parse_toml_value(value);
714        insert_toml_value(&mut config, toml_key.clone(), value)?;
715    }
716
717    Ok(Some(config.into()))
718}
719
720/// Initialize configuration from available sources specified in [`Args`].
721///
722/// If no configuration was found, will return `None`.
723///
724/// See [`toml-env`](crate).
725pub fn initialize<C>(args: Args<'_>) -> Result<Option<C>>
726where
727    C: DeserializeOwned + Serialize,
728{
729    let config_variable_name = args.config_variable_name;
730    let logging = args.logging;
731    let dotenv_path = args.dotenv_path;
732
733    let config_env_config: Option<(Value, ConfigSource)> = match std::env::var(config_variable_name) {
734        Ok(variable_value) => match toml::from_str(&variable_value) {
735            Ok(config) => {
736                log_info(
737                    logging,
738                    format_args!(
739                        "Options loaded from `{config_variable_name}` environment variable"
740                    ),
741                );
742                Ok(Some(config))
743            }
744            Err(error) => {
745                let path = Path::new(&variable_value);
746                if path.is_file() {
747                    log_info(
748                        args.logging,
749                        format_args!("Loading environment variables from {path:?}"),
750                    );
751
752                    let config_str =
753                        std::fs::read_to_string(path).map_err(|error| InnerError::ErrorReadingFile {
754                            path: path.to_owned(),
755                            error,
756                        })?;
757                    let config: Value = toml::from_str(&config_str).map_err(|error| {
758                        InnerError::ErrorParsingTomlFile {
759                            path: path.to_owned(),
760                            error: error.into(),
761                        }
762                    })?;
763                    log_info(logging, format_args!("Options loaded from file specified in `{config_variable_name}` environment variable: {path:?}"));
764                    Ok(Some(config))
765                } else {
766                    Err(InnerError::ErrorParsingEnvironmentVariableAsConfigOrFile {
767                        name: config_variable_name.to_owned(),
768                        value: variable_value,
769                        error: error.into(),
770                    })
771                }
772            }
773        },
774        Err(std::env::VarError::NotPresent) => {
775            log_info(
776                logging,
777                format_args!(
778                    "No environment variable with the name {config_variable_name} found, using default options."
779                ),
780            );
781            Ok(None)
782        }
783        Err(error) => Err(InnerError::ErrorReadingEnvironmentVariable {
784            name: config_variable_name.to_owned(),
785            error,
786        }),
787    }?.map(|config| {
788        let source = ConfigSource::DotEnv(args.dotenv_path.to_owned());
789        (config, source)
790    });
791
792    let dotenv_config =
793        initialize_dotenv_toml(dotenv_path, config_variable_name, logging)?.map(|config| {
794            (
795                config,
796                ConfigSource::Environment {
797                    variable_names: vec![args.config_variable_name.to_owned()],
798                },
799            )
800        });
801
802    let config: Option<(Value, ConfigSource)> = match (dotenv_config, config_env_config) {
803        (None, None) => None,
804        (None, Some(config)) => Some(config),
805        (Some(config), None) => Some(config),
806        (Some(from), Some(into)) => {
807            let config = serde_toml_merge::merge(into.0, from.0).map_err(|error| {
808                InnerError::ErrorMerging {
809                    from: from.1.clone(),
810                    into: into.1.clone(),
811                    error,
812                }
813            })?;
814
815            let source = ConfigSource::Merged {
816                from: from.1.into(),
817                into: into.1.into(),
818            };
819
820            Some((config, source))
821        }
822    };
823
824    let env_config = initialize_env(
825        args.logging,
826        args.map_env.clone(),
827        args.auto_map_env,
828        config_variable_name,
829    )?
830    .map(|value| {
831        (
832            value,
833            ConfigSource::Environment {
834                variable_names: args.map_env.keys().map(|key| (*key).to_owned()).collect(),
835            },
836        )
837    });
838
839    let config = match (config, env_config) {
840        (None, None) => None,
841        (None, Some(config)) => Some(config),
842        (Some(config), None) => Some(config),
843        (Some(from), Some(into)) => {
844            let config = serde_toml_merge::merge(into.0, from.0).map_err(|error| {
845                InnerError::ErrorMerging {
846                    from: from.1.clone(),
847                    into: into.1.clone(),
848                    error,
849                }
850            })?;
851
852            let source = ConfigSource::Merged {
853                from: from.1.into(),
854                into: into.1.into(),
855            };
856            Some((config, source))
857        }
858    };
859
860    let file_config: Option<(Value, ConfigSource)> =
861        Option::transpose(args.config_path.map(|path| {
862            if path.is_file() {
863                let file_string = std::fs::read_to_string(path).map_err(|error| {
864                    InnerError::ErrorReadingFile {
865                        path: path.to_owned(),
866                        error,
867                    }
868                })?;
869                return Result::Ok(Some((
870                    toml::from_str(&file_string).map_err(|error| {
871                        InnerError::ErrorParsingTomlFile {
872                            path: path.to_owned(),
873                            error: error.into(),
874                        }
875                    })?,
876                    ConfigSource::File(path.to_owned()),
877                )));
878            }
879            Ok(None)
880        }))?
881        .flatten();
882
883    let config = match (config, file_config) {
884        (None, None) => None,
885        (None, Some(config)) => Some(config),
886        (Some(config), None) => Some(config),
887        (Some(from), Some(into)) => {
888            let config = serde_toml_merge::merge(into.0, from.0).map_err(|error| {
889                InnerError::ErrorMerging {
890                    from: from.1.clone(),
891                    into: into.1.clone(),
892                    error,
893                }
894            })?;
895
896            let source = ConfigSource::Merged {
897                from: from.1.into(),
898                into: into.1.into(),
899            };
900            Some((config, source))
901        }
902    };
903
904    let config = Option::transpose(config.map(|(config, source)| {
905        C::deserialize(config).map_err(|error| InnerError::ErrorParsingMergedToml {
906            source,
907            error: error.into(),
908        })
909    }))?;
910
911    match (logging, config.as_ref()) {
912        (_, Some(config)) => {
913            let config_string = toml::to_string_pretty(&config)
914                .expect("Expected to be able to re-serialize config toml");
915            log_info(
916                logging,
917                format_args!("Parsed configuration:\n\x1b[34m{config_string}\x1b[0m"),
918            );
919        }
920        (Logging::None, _) | (_, None) => {}
921    }
922
923    Ok(config)
924}
925
926#[cfg(test)]
927mod test {
928    use crate::InsertTomlValueError;
929
930    use super::insert_toml_value;
931    #[test]
932    fn insert_toml_value_empty_path() {
933        let mut value = toml::Value::String("Hello".to_owned());
934        insert_toml_value(
935            &mut value,
936            "".parse().unwrap(),
937            toml::Value::String("World".to_owned()),
938        )
939        .unwrap();
940        assert_eq!(value.as_str().unwrap(), "World");
941    }
942
943    #[test]
944    fn insert_toml_value_table_property() {
945        let mut value = toml::Value::Table(toml::Table::new());
946        insert_toml_value(
947            &mut value,
948            "child".parse().unwrap(),
949            toml::Value::String("Hello Child".to_owned()),
950        )
951        .unwrap();
952        assert_eq!(value.get("child").unwrap().as_str().unwrap(), "Hello Child");
953    }
954
955    #[test]
956    fn insert_toml_value_table_property_property() {
957        let mut value = toml::Value::Table(toml::Table::new());
958        insert_toml_value(
959            &mut value,
960            "child.value".parse().unwrap(),
961            toml::Value::String("Hello Child Value".to_owned()),
962        )
963        .unwrap();
964        assert_eq!(
965            value
966                .get("child")
967                .unwrap()
968                .get("value")
969                .unwrap()
970                .as_str()
971                .unwrap(),
972            "Hello Child Value"
973        );
974    }
975
976    #[test]
977    fn insert_toml_value_create_array() {
978        let mut value = toml::Value::Array(Vec::new());
979        insert_toml_value(
980            &mut value,
981            "0".parse().unwrap(),
982            toml::Value::String("Hello Element".to_owned()),
983        )
984        .unwrap();
985        assert_eq!(value.get(0).unwrap().as_str().unwrap(), "Hello Element");
986    }
987
988    #[test]
989    fn insert_toml_value_array_out_of_bounds_error() {
990        let mut value = toml::Value::Array(Vec::new());
991        let error = insert_toml_value(
992            &mut value,
993            "1".parse().unwrap(),
994            toml::Value::String("Hello Element".to_owned()),
995        )
996        .unwrap_err();
997
998        assert!(matches!(
999            error,
1000            InsertTomlValueError::ArrayOutOfBounds { .. }
1001        ))
1002    }
1003
1004    #[test]
1005    fn insert_toml_value_table_property_index_error() {
1006        let mut value = toml::Value::Array(Vec::new());
1007        let error = insert_toml_value(
1008            &mut value,
1009            "key".parse().unwrap(),
1010            toml::Value::String("Hello Element".to_owned()),
1011        )
1012        .unwrap_err();
1013
1014        assert!(matches!(
1015            error,
1016            InsertTomlValueError::TablePropertyCannotIndex { .. }
1017        ))
1018    }
1019
1020    #[test]
1021    fn insert_toml_value_array_index_cannot_index_error() {
1022        let mut value = toml::Value::Table(toml::Table::new());
1023        let error = insert_toml_value(
1024            &mut value,
1025            "0".parse().unwrap(),
1026            toml::Value::String("Hello Element".to_owned()),
1027        )
1028        .unwrap_err();
1029
1030        assert!(matches!(
1031            error,
1032            InsertTomlValueError::ArrayIndexCannotIndex { .. }
1033        ))
1034    }
1035
1036    #[test]
1037    fn insert_toml_value_table_child_create_array() {
1038        let mut value = toml::Value::Table(toml::Table::new());
1039        insert_toml_value(
1040            &mut value,
1041            "child.0".parse().unwrap(),
1042            toml::Value::String("Hello Element".to_owned()),
1043        )
1044        .unwrap();
1045        assert_eq!(
1046            value
1047                .get("child")
1048                .unwrap()
1049                .get(0)
1050                .unwrap()
1051                .as_str()
1052                .unwrap(),
1053            "Hello Element"
1054        );
1055    }
1056}