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::ValidationError;
use crate::metadata::ValidationRule;

use super::super::super::error::validation_error;

pub(in crate::loader::validation::rules) fn validate_min_length(
    path: &str,
    actual: &Value,
    rule: &ValidationRule,
    secret_paths: &BTreeSet<String>,
    min: usize,
) -> Option<ValidationError> {
    validate_count_bound(
        Bound::Min,
        CountValidation {
            path,
            actual,
            rule,
            secret_paths,
            observed: validation_length(actual),
            limit: min,
            label: "length",
            type_error: "must be a string, array, or object to apply length validation",
        },
    )
}

pub(in crate::loader::validation::rules) fn validate_max_length(
    path: &str,
    actual: &Value,
    rule: &ValidationRule,
    secret_paths: &BTreeSet<String>,
    max: usize,
) -> Option<ValidationError> {
    validate_count_bound(
        Bound::Max,
        CountValidation {
            path,
            actual,
            rule,
            secret_paths,
            observed: validation_length(actual),
            limit: max,
            label: "length",
            type_error: "must be a string, array, or object to apply length validation",
        },
    )
}

pub(in crate::loader::validation::rules) fn validate_min_items(
    path: &str,
    actual: &Value,
    rule: &ValidationRule,
    secret_paths: &BTreeSet<String>,
    min: usize,
) -> Option<ValidationError> {
    validate_count_bound(
        Bound::Min,
        CountValidation {
            path,
            actual,
            rule,
            secret_paths,
            observed: array_len(actual),
            limit: min,
            label: "item count",
            type_error: "must be an array to apply item-count validation",
        },
    )
}

pub(in crate::loader::validation::rules) fn validate_max_items(
    path: &str,
    actual: &Value,
    rule: &ValidationRule,
    secret_paths: &BTreeSet<String>,
    max: usize,
) -> Option<ValidationError> {
    validate_count_bound(
        Bound::Max,
        CountValidation {
            path,
            actual,
            rule,
            secret_paths,
            observed: array_len(actual),
            limit: max,
            label: "item count",
            type_error: "must be an array to apply item-count validation",
        },
    )
}

pub(in crate::loader::validation::rules) fn validate_min_properties(
    path: &str,
    actual: &Value,
    rule: &ValidationRule,
    secret_paths: &BTreeSet<String>,
    min: usize,
) -> Option<ValidationError> {
    validate_count_bound(
        Bound::Min,
        CountValidation {
            path,
            actual,
            rule,
            secret_paths,
            observed: object_len(actual),
            limit: min,
            label: "property count",
            type_error: "must be an object to apply property-count validation",
        },
    )
}

pub(in crate::loader::validation::rules) fn validate_max_properties(
    path: &str,
    actual: &Value,
    rule: &ValidationRule,
    secret_paths: &BTreeSet<String>,
    max: usize,
) -> Option<ValidationError> {
    validate_count_bound(
        Bound::Max,
        CountValidation {
            path,
            actual,
            rule,
            secret_paths,
            observed: object_len(actual),
            limit: max,
            label: "property count",
            type_error: "must be an object to apply property-count validation",
        },
    )
}

#[derive(Clone, Copy)]
enum Bound {
    Min,
    Max,
}

impl Bound {
    fn accepts(self, observed: usize, limit: usize) -> bool {
        match self {
            Self::Min => observed >= limit,
            Self::Max => observed <= limit,
        }
    }

    fn operator(self) -> &'static str {
        match self {
            Self::Min => ">=",
            Self::Max => "<=",
        }
    }
}

struct CountValidation<'a> {
    path: &'a str,
    actual: &'a Value,
    rule: &'a ValidationRule,
    secret_paths: &'a BTreeSet<String>,
    observed: Option<usize>,
    limit: usize,
    label: &'static str,
    type_error: &'static str,
}

fn validate_count_bound(bound: Bound, validation: CountValidation<'_>) -> Option<ValidationError> {
    let expected = || {
        Some(Value::Number(
            u64::try_from(validation.limit).unwrap_or(u64::MAX).into(),
        ))
    };
    match validation.observed {
        Some(observed) if bound.accepts(observed, validation.limit) => None,
        Some(_) => Some(validation_error(
            validation.path,
            validation.actual,
            validation.rule,
            validation.secret_paths,
            &format!(
                "{} must be {} {}",
                validation.label,
                bound.operator(),
                validation.limit
            ),
            expected(),
        )),
        None => Some(validation_error(
            validation.path,
            validation.actual,
            validation.rule,
            validation.secret_paths,
            validation.type_error,
            expected(),
        )),
    }
}

fn validation_length(value: &Value) -> Option<usize> {
    match value {
        Value::String(inner) => Some(inner.chars().count()),
        Value::Array(values) => Some(values.len()),
        Value::Object(values) => Some(values.len()),
        _ => None,
    }
}

fn array_len(value: &Value) -> Option<usize> {
    match value {
        Value::Array(values) => Some(values.len()),
        _ => None,
    }
}

fn object_len(value: &Value) -> Option<usize> {
    match value {
        Value::Object(values) => Some(values.len()),
        _ => None,
    }
}