tier 0.1.17

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

use crate::path::{concrete_paths_overlap, get_value_at_path};
use crate::{ConfigError, ConfigMetadata, EnvDecoder};

mod context;
mod insert;

use super::EnvSource;
use super::name::path_for_env_var;
use super::state::EnvLayerState;
use super::target::{
    canonicalize_runtime_env_target_path, validate_binding_names,
    validate_binding_override_conflicts, validate_binding_paths,
};
use crate::loader::{CustomEnvDecoder, Layer};

use self::context::EnvLayerContext;
use self::insert::insert_env_value;

impl EnvSource {
    pub(in crate::loader) fn into_layer(
        self,
        metadata: &ConfigMetadata,
        env_decoders: &BTreeMap<String, EnvDecoder>,
        custom_env_decoders: &BTreeMap<String, CustomEnvDecoder>,
        runtime_layers: &[Layer],
    ) -> Result<Option<Layer>, ConfigError> {
        let EnvSource {
            vars,
            var_conflicts,
            prefix,
            separator,
            lowercase_segments,
            bindings,
            binding_conflicts,
        } = self;
        if let Some(conflict) = var_conflicts.into_iter().next() {
            return Err(ConfigError::InvalidEnv {
                name: conflict.name,
                path: String::new(),
                message: "environment source contains duplicate variable names".to_owned(),
            });
        }
        validate_binding_names(&bindings)?;
        if let Some(conflict) = binding_conflicts.into_iter().next() {
            let path = conflict.second.path.clone();
            return Err(ConfigError::InvalidEnv {
                name: conflict.name,
                path,
                message: format!(
                    "conflicting explicit env bindings target `{}` and `{}`",
                    conflict.first.path, conflict.second.path
                ),
            });
        }
        let alias_overrides = metadata.alias_overrides()?;
        validate_binding_paths(&bindings, &alias_overrides)?;
        let env_overrides = metadata.env_overrides()?;
        let context = EnvLayerContext {
            metadata,
            alias_overrides: &alias_overrides,
            env_decoders,
            custom_env_decoders,
            runtime_layers,
        };
        validate_binding_override_conflicts(
            &bindings,
            &env_overrides,
            &alias_overrides,
            runtime_layers,
        )?;
        let mut state = EnvLayerState::new();
        let mut fallback_vars = Vec::new();

        for (name, raw_value) in vars {
            if let Some(binding) = bindings.get(&name) {
                if binding.fallback {
                    fallback_vars.push((name, raw_value, binding.clone()));
                } else {
                    insert_env_value(
                        &context,
                        &mut state,
                        &name,
                        &raw_value,
                        &binding.path,
                        Some(binding.path.clone()),
                        binding.decoder,
                    )?;
                }
                continue;
            }

            if let Some(path) = env_overrides.get(&name) {
                insert_env_value(
                    &context,
                    &mut state,
                    &name,
                    &raw_value,
                    path,
                    Some(path.clone()),
                    None,
                )?;
                continue;
            }

            let Some(path) =
                path_for_env_var(&name, prefix.as_deref(), &separator, lowercase_segments)
                    .map_err(|error| ConfigError::InvalidEnv {
                        name: name.clone(),
                        path: error.path,
                        message: error.message,
                    })?
            else {
                continue;
            };
            insert_env_value(
                &context,
                &mut state,
                &name,
                &raw_value,
                &path,
                Some(path.clone()),
                None,
            )?;
        }

        let mut claimed_fallback_paths = BTreeMap::new();
        for (name, raw_value, binding) in fallback_vars {
            let normalized = canonicalize_runtime_env_target_path(
                &name,
                &binding.path,
                &alias_overrides,
                runtime_layers,
                &state.root,
            )?;
            if normalized.is_empty() {
                continue;
            }
            reject_conflicting_fallback(&name, &normalized, &claimed_fallback_paths)?;
            if get_value_at_path(&state.root, &normalized).is_some() {
                continue;
            }
            claimed_fallback_paths.insert(normalized.clone(), name.clone());
            insert_env_value(
                &context,
                &mut state,
                &name,
                &raw_value,
                &binding.path,
                Some(binding.path.clone()),
                binding.decoder,
            )?;
        }

        Ok(state.into_layer())
    }
}

fn reject_conflicting_fallback(
    name: &str,
    path: &str,
    claimed_fallback_paths: &BTreeMap<String, String>,
) -> Result<(), ConfigError> {
    for (existing_path, existing_name) in claimed_fallback_paths {
        if concrete_paths_overlap(existing_path, path) {
            return Err(ConfigError::InvalidEnv {
                name: name.to_owned(),
                path: path.to_owned(),
                message: format!(
                    "conflicting fallback environment variables `{existing_name}` and `{name}` target overlapping configuration paths `{existing_path}` and `{path}`"
                ),
            });
        }
    }

    Ok(())
}