tier 0.1.17

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

use serde_json::Value;

use crate::metadata::ConfigMetadata;
use crate::path::{
    collect_diff_paths, collect_paths, get_value_at_path, path_overlaps_pattern,
    path_starts_with_pattern, redact_value,
};
use crate::report::{ConfigReport, ConfigWarning, DeprecatedField, ResolutionStep};
use crate::value::values_equal;

use super::{Layer, SourceKind, SourceTrace};

pub(super) fn record_layer_steps(
    report: &mut ConfigReport,
    layer: &Layer,
    secret_paths: &BTreeSet<String>,
) {
    report.record_step(
        String::new(),
        ResolutionStep {
            source: layer.trace.clone(),
            value: redact_value(&layer.value, "", secret_paths),
            redacted: path_contains_secret(secret_paths, ""),
        },
    );

    for (path, trace) in &layer.entries {
        if let Some(value) = get_value_at_path(&layer.value, path) {
            let redacted = path_contains_secret(secret_paths, path);
            let rendered = redact_value(value, path, secret_paths);
            report.record_step(
                path.clone(),
                ResolutionStep {
                    source: trace.clone(),
                    value: rendered,
                    redacted,
                },
            );
        }
    }
}

pub(super) fn record_diff_steps(
    report: &mut ConfigReport,
    before: &Value,
    after: &Value,
    trace: &SourceTrace,
    secret_paths: &BTreeSet<String>,
) {
    if !values_equal(before, after) {
        report.record_step(
            String::new(),
            ResolutionStep {
                source: trace.clone(),
                value: redact_value(after, "", secret_paths),
                redacted: path_contains_secret(secret_paths, ""),
            },
        );
    }

    let mut paths = Vec::new();
    collect_diff_paths(before, after, "", &mut paths);
    paths.sort();
    paths.dedup();

    for path in paths {
        let after_value = get_value_at_path(after, &path).cloned();
        let removed = after_value.is_none() && get_value_at_path(before, &path).is_some();
        if !removed && after_value.is_none() {
            continue;
        }

        let redacted = path_contains_secret(secret_paths, &path);
        let rendered = match after_value {
            Some(value) => redact_value(&value, &path, secret_paths),
            None => Value::Null,
        };
        report.record_step(
            path,
            ResolutionStep {
                source: trace.clone(),
                value: rendered,
                redacted,
            },
        );
    }
}

pub(in crate::loader) fn record_layer_entry_traces(
    entries: &mut BTreeMap<String, SourceTrace>,
    kind: SourceKind,
    aggregate_name: &str,
    exact_name: &str,
    path: &str,
    segments: &[&str],
) {
    entries.insert(
        path.to_owned(),
        SourceTrace::new(kind, exact_name.to_owned()),
    );

    let mut prefix = String::new();
    for segment in segments {
        if !prefix.is_empty() {
            prefix.push('.');
        }
        prefix.push_str(segment);
        let entry = entries
            .entry(prefix.clone())
            .or_insert_with(|| SourceTrace::new(kind, exact_name.to_owned()));
        if prefix != path && entry.name != exact_name {
            *entry = SourceTrace::new(kind, aggregate_name.to_owned());
        }
    }
}

pub(super) fn record_deprecation_warnings(
    report: &mut ConfigReport,
    layer: &Layer,
    metadata: &ConfigMetadata,
) {
    if matches!(layer.trace.kind, SourceKind::Default) {
        return;
    }

    let mut used_paths = Vec::new();
    collect_paths(&layer.value, "", &mut used_paths);
    used_paths.sort();
    used_paths.dedup();

    let mut warned = BTreeSet::new();
    for path in used_paths {
        let Some(field) = metadata.effective_field_for(&path) else {
            continue;
        };
        let Some(note) = field.deprecated.clone() else {
            continue;
        };
        if warned.insert(path.clone()) {
            report.record_warning(ConfigWarning::DeprecatedField(
                DeprecatedField::new(path)
                    .with_source(Some(layer.trace.clone()))
                    .with_note(Some(note)),
            ));
        }
    }
}

pub(crate) fn is_secret_path(secret_paths: &BTreeSet<String>, path: &str) -> bool {
    secret_paths
        .iter()
        .any(|secret| path_starts_with_pattern(path, secret))
}

fn path_contains_secret(secret_paths: &BTreeSet<String>, path: &str) -> bool {
    secret_paths
        .iter()
        .any(|secret| path_overlaps_pattern(path, secret))
}