tier 0.1.17

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

use serde_json::{Map, Value};

use crate::path::{canonicalize_path_with_aliases, join_path};
use crate::{ConfigError, ConfigMetadata};

use super::super::de::insert_path;
use super::super::merge::ensure_root_object;
use super::super::path::ensure_path_safe_keys;

pub(in crate::loader) fn canonicalize_value_paths(
    value: &Value,
    metadata: &ConfigMetadata,
) -> Result<Value, ConfigError> {
    ensure_root_object(value)?;
    ensure_path_safe_keys(value, "")?;

    let aliases = metadata.alias_overrides()?;
    canonicalize_value_paths_with_aliases(value, &aliases)
}

pub(in crate::loader) fn canonicalize_value_paths_with_aliases(
    value: &Value,
    aliases: &BTreeMap<String, String>,
) -> Result<Value, ConfigError> {
    ensure_root_object(value)?;
    ensure_path_safe_keys(value, "")?;
    if aliases.is_empty() {
        return Ok(value.clone());
    }

    let mut canonical = Value::Object(Map::new());
    let mut nodes = Vec::new();
    collect_value_nodes(value, "", &mut nodes);
    let mut seen = BTreeMap::<String, String>::new();

    for (path, node) in nodes {
        let canonical_path = canonicalize_path_with_aliases(&path, aliases);
        if let Some(first_path) = seen.get(&canonical_path)
            && first_path != &path
        {
            return Err(ConfigError::PathConflict {
                first_path: first_path.clone(),
                second_path: path,
                canonical_path,
            });
        }
        seen.insert(canonical_path.clone(), path);
        let segments = canonical_path.split('.').collect::<Vec<_>>();
        insert_path(&mut canonical, &segments, node).map_err(|message| {
            ConfigError::InvalidArg {
                arg: canonical_path.clone(),
                message,
            }
        })?;
    }

    Ok(canonical)
}

fn collect_value_nodes(value: &Value, current: &str, nodes: &mut Vec<(String, Value)>) {
    match value {
        Value::Object(map) if map.is_empty() && !current.is_empty() => {
            nodes.push((current.to_owned(), Value::Object(Map::new())));
        }
        Value::Object(map) => {
            for (key, child) in map {
                let next = join_path(current, key);
                collect_value_nodes(child, &next, nodes);
            }
        }
        Value::Array(values) if values.is_empty() && !current.is_empty() => {
            nodes.push((current.to_owned(), Value::Array(Vec::new())));
        }
        Value::Array(values) => {
            for (index, child) in values.iter().enumerate() {
                let next = join_path(current, &index.to_string());
                collect_value_nodes(child, &next, nodes);
            }
        }
        _ if !current.is_empty() => nodes.push((current.to_owned(), value.clone())),
        _ => {}
    }
}