tier 0.1.17

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

use serde_json::Value;

use crate::error::ConfigError;
use crate::path::{get_value_at_path, join_path};

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

pub(super) fn validate_indexed_array_paths(base: &Value, layer: &Layer) -> Result<(), ConfigError> {
    for path in &layer.indexed_array_paths {
        let base_len = if let Some(base_len) = layer.indexed_array_base_lengths.get(path) {
            *base_len
        } else if layer.direct_array_paths.contains(path) {
            continue;
        } else {
            match get_value_at_path(base, path) {
                Some(Value::Array(values)) => values.len(),
                _ => 0,
            }
        };

        let mut explicit_indices = layer
            .entries
            .keys()
            .filter_map(|entry_path| direct_child_array_index(path, entry_path))
            .collect::<BTreeSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();
        if explicit_indices.is_empty() {
            continue;
        }
        explicit_indices.retain(|index| *index >= base_len);
        if explicit_indices.is_empty() {
            continue;
        }

        for (offset, index) in explicit_indices.iter().enumerate() {
            let expected = base_len + offset;
            if *index != expected {
                return Err(sparse_indexed_array_error(layer, path, *index, expected));
            }
        }
    }

    Ok(())
}

fn sparse_indexed_array_error(
    layer: &Layer,
    container_path: &str,
    offending_index: usize,
    expected_index: usize,
) -> ConfigError {
    let offending_path = join_path(container_path, &offending_index.to_string());
    let source = layer
        .entries
        .iter()
        .filter(|(entry_path, _)| {
            direct_child_array_index(container_path, entry_path) == Some(offending_index)
        })
        .max_by_key(|(entry_path, trace)| {
            (
                !is_generic_layer_trace(trace),
                entry_path.split('.').count(),
                entry_path.len(),
            )
        })
        .or_else(|| {
            layer
                .entries
                .iter()
                .find(|(entry_path, _)| *entry_path == &offending_path)
        });
    let message = format!(
        "sparse array override at `{container_path}`: index {offending_index} requires index {expected_index} to be set first"
    );

    match source {
        Some((_, trace)) => match trace.kind {
            SourceKind::Environment => ConfigError::InvalidEnv {
                name: trace.name.clone(),
                path: offending_path,
                message,
            },
            SourceKind::Arguments => ConfigError::InvalidArg {
                arg: trace.name.clone(),
                message,
            },
            _ => ConfigError::MetadataInvalid {
                path: offending_path,
                message,
            },
        },
        None => ConfigError::MetadataInvalid {
            path: offending_path,
            message,
        },
    }
}

fn is_generic_layer_trace(trace: &SourceTrace) -> bool {
    matches!(
        (trace.kind, trace.name.as_str()),
        (SourceKind::Arguments, "arguments") | (SourceKind::Environment, "environment")
    )
}