tier 0.1.17

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

use crate::ConfigError;
use crate::metadata::paths::{
    alias_mapping_is_lossless, alias_overlap_sample_path, alias_patterns_are_ambiguous,
    validate_metadata_path,
};

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

impl ConfigMetadata {
    /// Returns explicit path aliases keyed by alias path.
    pub fn alias_overrides(&self) -> Result<BTreeMap<String, String>, ConfigError> {
        let mut aliases = BTreeMap::<String, String>::new();
        let canonical_paths = self
            .fields
            .iter()
            .map(|field| field.path.clone())
            .collect::<BTreeSet<_>>();

        for field in &self.fields {
            validate_metadata_path(&field.path)?;
            for alias in &field.aliases {
                validate_metadata_path(alias)?;
                validate_alias(field, alias, &canonical_paths)?;
                if let Some(first_path) = aliases.get(alias)
                    && first_path != &field.path
                {
                    return Err(ConfigError::MetadataConflict {
                        kind: "alias",
                        name: alias.clone(),
                        first_path: first_path.clone(),
                        second_path: field.path.clone(),
                    });
                }
                if let Some((other_alias, sample_path)) =
                    ambiguous_alias_overlap(&aliases, alias, &field.path)
                {
                    return Err(ConfigError::MetadataInvalid {
                        path: alias.clone(),
                        message: format!(
                            "alias `{alias}` overlaps ambiguously with `{other_alias}` for concrete path `{sample_path}`"
                        ),
                    });
                }
                aliases.insert(alias.clone(), field.path.clone());
            }
        }
        Ok(aliases)
    }
}

fn validate_alias(
    field: &FieldMetadata,
    alias: &str,
    canonical_paths: &BTreeSet<String>,
) -> Result<(), ConfigError> {
    if alias.is_empty() {
        return Err(ConfigError::MetadataInvalid {
            path: alias.to_owned(),
            message: "aliases cannot target the root path".to_owned(),
        });
    }
    if field.path.is_empty() {
        return Err(ConfigError::MetadataInvalid {
            path: alias.to_owned(),
            message: "aliases cannot rewrite the root path".to_owned(),
        });
    }
    if !alias_mapping_is_lossless(alias, &field.path) {
        return Err(ConfigError::MetadataInvalid {
            path: alias.to_owned(),
            message: format!(
                "alias `{alias}` must preserve wildcard positions and cannot be deeper than canonical path `{}`",
                field.path
            ),
        });
    }
    if canonical_paths.contains(alias) && alias != field.path {
        return Err(ConfigError::MetadataConflict {
            kind: "alias",
            name: alias.to_owned(),
            first_path: alias.to_owned(),
            second_path: field.path.clone(),
        });
    }
    Ok(())
}

fn ambiguous_alias_overlap(
    aliases: &BTreeMap<String, String>,
    alias: &str,
    canonical: &str,
) -> Option<(String, String)> {
    aliases.iter().find_map(|(other_alias, other_canonical)| {
        alias_patterns_are_ambiguous(alias, canonical, other_alias, other_canonical).then(|| {
            (
                other_alias.clone(),
                alias_overlap_sample_path(alias, other_alias),
            )
        })
    })
}