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::ValidationErrors;
use crate::metadata::{ConfigMetadata, ValidationCheck};
use crate::value::values_equal;

use super::error::group_validation_error;
use super::matching::{
    bind_required_paths, collect_matching_values, is_present_value, missing_paths, present_paths,
};

pub(super) fn validate_declared_checks(
    value: &Value,
    metadata: &ConfigMetadata,
    secret_paths: &BTreeSet<String>,
) -> ValidationErrors {
    let mut errors = ValidationErrors::new();

    for check in metadata.checks() {
        match check {
            ValidationCheck::AtLeastOneOf { paths } => {
                let present = present_paths(value, paths);
                if present.is_empty() {
                    errors.push(group_validation_error(
                        check,
                        paths,
                        secret_paths,
                        &format!("at least one of {} must be configured", paths.join(", ")),
                        Some(serde_json::json!({ "min_present": 1, "paths": paths })),
                        Some(serde_json::json!({ "present": present })),
                    ));
                }
            }
            ValidationCheck::ExactlyOneOf { paths } => {
                let present = present_paths(value, paths);
                if present.len() != 1 {
                    errors.push(group_validation_error(
                        check,
                        paths,
                        secret_paths,
                        &format!("exactly one of {} must be configured", paths.join(", ")),
                        Some(serde_json::json!({ "exactly_one_of": paths })),
                        Some(serde_json::json!({ "present": present })),
                    ));
                }
            }
            ValidationCheck::MutuallyExclusive { paths } => {
                let present = present_paths(value, paths);
                if present.len() > 1 {
                    errors.push(group_validation_error(
                        check,
                        paths,
                        secret_paths,
                        &format!("{} are mutually exclusive", paths.join(", ")),
                        Some(serde_json::json!({ "max_present": 1, "paths": paths })),
                        Some(serde_json::json!({ "present": present })),
                    ));
                }
            }
            ValidationCheck::RequiredWith { path, requires } => {
                for (matched_path, _) in collect_matching_values(value, path)
                    .into_iter()
                    .filter(|(_, matched)| is_present_value(matched))
                {
                    let bound_requires = bind_required_paths(path, &matched_path, requires)
                        .unwrap_or_else(|| requires.clone());
                    let missing = missing_paths(value, &bound_requires);
                    if !missing.is_empty() {
                        errors.push(group_validation_error(
                            check,
                            std::iter::once(matched_path.clone()).chain(missing.iter().cloned()),
                            secret_paths,
                            &format!("{matched_path} requires {}", missing.join(", ")),
                            Some(serde_json::json!({
                                "trigger": matched_path,
                                "requires": bound_requires
                            })),
                            Some(serde_json::json!({ "missing": missing })),
                        ));
                    }
                }
            }
            ValidationCheck::RequiredIf {
                path,
                equals,
                requires,
            } => {
                for (matched_path, _actual) in collect_matching_values(value, path)
                    .into_iter()
                    .filter(|(_, matched)| values_equal(matched, &equals.0))
                {
                    let bound_requires = bind_required_paths(path, &matched_path, requires)
                        .unwrap_or_else(|| requires.clone());
                    let missing = missing_paths(value, &bound_requires);
                    if !missing.is_empty() {
                        errors.push(group_validation_error(
                            check,
                            std::iter::once(matched_path.clone()).chain(missing.iter().cloned()),
                            secret_paths,
                            &format!(
                                "{matched_path} == {} requires {}",
                                equals,
                                missing.join(", ")
                            ),
                            Some(serde_json::json!({
                                "trigger": matched_path,
                                "equals": equals,
                                "requires": bound_requires
                            })),
                            Some(serde_json::json!({ "missing": missing })),
                        ));
                    }
                }
            }
        }
    }

    errors
}