tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use serde_json::Value;

use crate::path::{join_path, normalize_path};

pub(super) fn collect_matching_values<'a>(
    value: &'a Value,
    path: &str,
) -> Vec<(String, &'a Value)> {
    let normalized = normalize_path(path);
    if normalized.is_empty() {
        return Vec::new();
    }

    let segments = normalized.split('.').collect::<Vec<_>>();
    let mut matches = Vec::new();
    collect_matching_values_recursive(value, "", &segments, 0, &mut matches);
    matches
}

pub(super) fn bind_required_paths(
    trigger_pattern: &str,
    matched_path: &str,
    requires: &[String],
) -> Option<Vec<String>> {
    let bindings = wildcard_bindings(trigger_pattern, matched_path)?;
    Some(
        requires
            .iter()
            .map(|path| apply_wildcard_bindings(path, &bindings))
            .collect(),
    )
}

pub(super) fn present_paths(value: &Value, paths: &[String]) -> Vec<String> {
    paths
        .iter()
        .filter(|path| path_is_present(value, path))
        .cloned()
        .collect()
}

pub(super) fn missing_paths(value: &Value, paths: &[String]) -> Vec<String> {
    paths
        .iter()
        .filter(|path| !path_is_present(value, path))
        .cloned()
        .collect()
}

pub(super) fn is_present_value(value: &Value) -> bool {
    !matches!(value, Value::Null)
}

fn wildcard_bindings(pattern: &str, matched_path: &str) -> Option<Vec<String>> {
    let pattern_segments = pattern
        .split('.')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    let path_segments = matched_path
        .split('.')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();

    if pattern_segments.len() != path_segments.len() {
        return None;
    }

    let mut bindings = Vec::new();
    for (expected, actual) in pattern_segments.iter().zip(path_segments.iter()) {
        if *expected == "*" {
            bindings.push((*actual).to_owned());
        } else if expected != actual {
            return None;
        }
    }

    Some(bindings)
}

fn apply_wildcard_bindings(pattern: &str, bindings: &[String]) -> String {
    let mut binding_index = 0;
    pattern
        .split('.')
        .filter(|segment| !segment.is_empty())
        .map(|segment| {
            if segment == "*" {
                let resolved = bindings
                    .get(binding_index)
                    .cloned()
                    .unwrap_or_else(|| "*".to_owned());
                binding_index += 1;
                resolved
            } else {
                segment.to_owned()
            }
        })
        .collect::<Vec<_>>()
        .join(".")
}

fn collect_matching_values_recursive<'a>(
    value: &'a Value,
    current: &str,
    segments: &[&str],
    index: usize,
    matches: &mut Vec<(String, &'a Value)>,
) {
    if index == segments.len() {
        matches.push((current.to_owned(), value));
        return;
    }

    let segment = segments[index];
    match (segment, value) {
        ("*", Value::Object(map)) => {
            for (key, child) in map {
                let next = join_path(current, key);
                collect_matching_values_recursive(child, &next, segments, index + 1, matches);
            }
        }
        ("*", Value::Array(values)) => {
            for (child_index, child) in values.iter().enumerate() {
                let next = join_path(current, &child_index.to_string());
                collect_matching_values_recursive(child, &next, segments, index + 1, matches);
            }
        }
        (_, Value::Object(map)) => {
            if let Some(child) = map.get(segment) {
                let next = join_path(current, segment);
                collect_matching_values_recursive(child, &next, segments, index + 1, matches);
            }
        }
        (_, Value::Array(values)) => {
            if let Ok(child_index) = segment.parse::<usize>()
                && let Some(child) = values.get(child_index)
            {
                let next = join_path(current, segment);
                collect_matching_values_recursive(child, &next, segments, index + 1, matches);
            }
        }
        _ => {}
    }
}

fn path_is_present(value: &Value, path: &str) -> bool {
    collect_matching_values(value, path)
        .iter()
        .any(|(_, value)| is_present_value(value))
}