tier 0.1.17

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

use serde_json::Value;

use crate::ConfigError;
use crate::path::{canonicalize_path_with_aliases, concrete_paths_overlap};

use super::super::Layer;
use super::super::canonical::{canonicalize_runtime_path, canonicalize_runtime_path_across_layers};
use super::super::path::{invalid_concrete_path_segment, try_normalize_external_path};
use super::binding::EnvBinding;

pub(super) fn validate_binding_names(
    bindings: &BTreeMap<String, EnvBinding>,
) -> Result<(), ConfigError> {
    for (name, binding) in bindings {
        if name.is_empty() {
            return Err(ConfigError::InvalidEnv {
                name: name.clone(),
                path: binding.path.clone(),
                message: "environment variable names cannot be empty".to_owned(),
            });
        }
    }

    Ok(())
}

pub(super) fn validate_binding_paths(
    bindings: &BTreeMap<String, EnvBinding>,
    alias_overrides: &BTreeMap<String, String>,
) -> Result<(), ConfigError> {
    for (name, binding) in bindings {
        canonical_env_target_path(name, &binding.path, alias_overrides)?;
    }
    Ok(())
}

pub(super) fn validate_binding_override_conflicts(
    bindings: &BTreeMap<String, EnvBinding>,
    env_overrides: &BTreeMap<String, String>,
    alias_overrides: &BTreeMap<String, String>,
    runtime_layers: &[Layer],
) -> Result<(), ConfigError> {
    for (name, binding) in bindings {
        let Some(metadata_path) = env_overrides.get(name) else {
            continue;
        };

        let binding_path = canonical_env_target_path(name, &binding.path, alias_overrides)?;
        let binding_path = canonicalize_runtime_path_across_layers(&binding_path, runtime_layers);
        let metadata_path = canonicalize_runtime_path_across_layers(metadata_path, runtime_layers);
        if binding_path != metadata_path {
            return Err(ConfigError::InvalidEnv {
                name: name.clone(),
                path: binding.path.clone(),
                message: format!(
                    "conflicting environment bindings target `{}` via EnvSource and `{}` via metadata",
                    binding_path, metadata_path
                ),
            });
        }
    }

    Ok(())
}

pub(super) fn canonicalize_runtime_env_target_path(
    name: &str,
    path: &str,
    alias_overrides: &BTreeMap<String, String>,
    runtime_layers: &[Layer],
    current_root: &Value,
) -> Result<String, ConfigError> {
    let path = canonical_env_target_path(name, path, alias_overrides)?;
    let path = canonicalize_runtime_path_across_layers(&path, runtime_layers);
    Ok(canonicalize_runtime_path(current_root, &path))
}

pub(super) fn claim_env_path(
    name: &str,
    path: &str,
    claimed_paths: &mut BTreeMap<String, String>,
) -> Result<(), ConfigError> {
    for (existing_path, existing_name) in claimed_paths.iter() {
        if existing_name == name {
            continue;
        }

        if existing_path == path {
            return Err(ConfigError::InvalidEnv {
                name: name.to_owned(),
                path: path.to_owned(),
                message: format!(
                    "conflicting environment variables `{existing_name}` and `{name}` both target `{path}`"
                ),
            });
        }

        if concrete_paths_overlap(existing_path, path) {
            return Err(ConfigError::InvalidEnv {
                name: name.to_owned(),
                path: path.to_owned(),
                message: format!(
                    "conflicting environment variables `{existing_name}` and `{name}` target overlapping configuration paths `{existing_path}` and `{path}`"
                ),
            });
        }
    }

    claimed_paths.insert(path.to_owned(), name.to_owned());
    Ok(())
}

fn canonical_env_target_path(
    name: &str,
    path: &str,
    alias_overrides: &BTreeMap<String, String>,
) -> Result<String, ConfigError> {
    let normalized =
        try_normalize_external_path(path).map_err(|message| ConfigError::InvalidEnv {
            name: name.to_owned(),
            path: path.to_owned(),
            message,
        })?;
    if normalized.is_empty() {
        return Err(ConfigError::InvalidEnv {
            name: name.to_owned(),
            path: path.to_owned(),
            message: "environment binding path cannot be empty".to_owned(),
        });
    }
    if let Some((segment, message)) = invalid_concrete_path_segment(&normalized) {
        return Err(ConfigError::InvalidEnv {
            name: name.to_owned(),
            path: path.to_owned(),
            message: format!("environment binding path segment `{segment}` is invalid: {message}"),
        });
    }
    Ok(canonicalize_path_with_aliases(&normalized, alias_overrides))
}