tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use std::collections::BTreeMap;

use crate::ConfigError;
use crate::metadata::paths::validate_metadata_path;
use crate::path::canonicalize_path_with_aliases;

use super::super::{ConfigMetadata, EnvDecoder};

impl ConfigMetadata {
    pub(crate) fn canonicalize_env_decoder_paths(&mut self) -> Result<(), ConfigError> {
        let alias_source_fields = self
            .fields
            .iter()
            .filter(|field| !field.is_env_decoder_only())
            .cloned()
            .collect::<Vec<_>>();
        let aliases = ConfigMetadata {
            fields: alias_source_fields,
            checks: Vec::new(),
        }
        .alias_overrides()?;

        let mut seen = BTreeMap::<String, (String, EnvDecoder)>::new();
        for field in &mut self.fields {
            if !field.is_env_decoder_only() {
                continue;
            }

            let original_path = field.path.clone();
            let canonical = canonicalize_path_with_aliases(&original_path, &aliases);
            let Some(decoder) = field.env_decode else {
                return Err(ConfigError::MetadataInvalid {
                    path: original_path,
                    message: "environment decoder metadata is missing a decoder".to_owned(),
                });
            };
            if let Some((first_path, first_decoder)) = seen.get(&canonical)
                && (first_path != &original_path || *first_decoder != decoder)
            {
                return Err(ConfigError::MetadataConflict {
                    kind: "environment decoder",
                    name: canonical,
                    first_path: first_path.clone(),
                    second_path: original_path,
                });
            }

            seen.insert(canonical.clone(), (original_path, decoder));
            field.path = canonical;
        }

        self.normalize();
        Ok(())
    }

    /// Returns explicit environment variable name overrides keyed by env name.
    pub fn env_overrides(&self) -> Result<BTreeMap<String, String>, ConfigError> {
        let aliases = self.alias_overrides()?;
        let mut envs = BTreeMap::new();
        let mut canonical_targets = BTreeMap::<String, String>::new();
        for field in &self.fields {
            let Some(env) = &field.env else {
                continue;
            };
            if env.is_empty() {
                return Err(ConfigError::MetadataInvalid {
                    path: field.path.clone(),
                    message: "explicit environment variable names cannot be empty".to_owned(),
                });
            }
            validate_metadata_path(&field.path)?;
            if field.path.is_empty() {
                return Err(ConfigError::MetadataInvalid {
                    path: field.path.clone(),
                    message: "explicit environment variable names cannot target the root path"
                        .to_owned(),
                });
            }
            if field.path.split('.').any(|segment| segment == "*") {
                return Err(ConfigError::MetadataInvalid {
                    path: field.path.clone(),
                    message: "explicit environment variable names cannot target wildcard paths"
                        .to_owned(),
                });
            }
            let canonical = canonicalize_path_with_aliases(&field.path, &aliases);
            if let Some(first_env) = canonical_targets.insert(canonical.clone(), env.clone())
                && first_env != *env
            {
                return Err(ConfigError::MetadataConflict {
                    kind: "environment override target",
                    name: canonical,
                    first_path: first_env,
                    second_path: env.clone(),
                });
            }
            if let Some(first_path) = envs.insert(env.clone(), field.path.clone())
                && first_path != field.path
            {
                return Err(ConfigError::MetadataConflict {
                    kind: "environment variable",
                    name: env.clone(),
                    first_path,
                    second_path: field.path.clone(),
                });
            }
        }
        Ok(envs)
    }
}