tier 0.1.17

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

use regex::Regex;
use serde_json::Value;

use super::example_matches_schema;
use crate::schema::count::{keyword_u64, len_at_least, len_at_most};

pub(super) fn object_matches_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
    root: &Value,
    visited_refs: &mut BTreeSet<String>,
) -> bool {
    object_size_matches_schema(map, object)
        && property_names_match_schema(map, object, root, visited_refs)
        && required_properties_match_schema(map, object)
        && fixed_properties_match_schema(map, object, root, visited_refs)
        && dynamic_properties_match_schema(map, object, root, visited_refs)
}

fn object_size_matches_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
) -> bool {
    keyword_u64(object, "minProperties")
        .is_none_or(|min_properties| len_at_least(map.len(), min_properties))
        && keyword_u64(object, "maxProperties")
            .is_none_or(|max_properties| len_at_most(map.len(), max_properties))
}

fn property_names_match_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
    root: &Value,
    visited_refs: &BTreeSet<String>,
) -> bool {
    object.get("propertyNames").is_none_or(|property_names| {
        map.keys().all(|key| {
            example_matches_schema(
                &Value::String(key.clone()),
                property_names,
                root,
                &mut visited_refs.clone(),
            )
        })
    })
}

fn required_properties_match_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
) -> bool {
    object
        .get("required")
        .and_then(Value::as_array)
        .is_none_or(|required| {
            required
                .iter()
                .filter_map(Value::as_str)
                .all(|key| map.contains_key(key))
        })
}

fn fixed_properties_match_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
    root: &Value,
    visited_refs: &mut BTreeSet<String>,
) -> bool {
    let Some(properties) = object.get("properties").and_then(Value::as_object) else {
        return true;
    };

    properties.iter().all(|(key, child_schema)| {
        map.get(key).is_none_or(|child_value| {
            example_matches_schema(child_value, child_schema, root, visited_refs)
        })
    })
}

fn dynamic_properties_match_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
    root: &Value,
    visited_refs: &mut BTreeSet<String>,
) -> bool {
    let fixed_properties = object
        .get("properties")
        .and_then(Value::as_object)
        .map_or_else(BTreeSet::new, |properties| {
            properties.keys().cloned().collect::<BTreeSet<_>>()
        });
    let Some(pattern_matched_keys) =
        pattern_properties_match_schema(map, object, root, visited_refs)
    else {
        return false;
    };

    object.get("additionalProperties").is_none_or(|additional| {
        map.iter().all(|(key, child_value)| {
            fixed_properties.contains(key)
                || pattern_matched_keys.contains(key)
                || example_matches_schema(child_value, additional, root, visited_refs)
        })
    })
}

fn pattern_properties_match_schema(
    map: &serde_json::Map<String, Value>,
    object: &serde_json::Map<String, Value>,
    root: &Value,
    visited_refs: &mut BTreeSet<String>,
) -> Option<BTreeSet<String>> {
    let mut matched_keys = BTreeSet::new();
    let Some(pattern_properties) = object.get("patternProperties").and_then(Value::as_object)
    else {
        return Some(matched_keys);
    };

    for (key, child_value) in map {
        for (pattern, child_schema) in pattern_properties {
            if Regex::new(pattern).is_ok_and(|regex| regex.is_match(key)) {
                matched_keys.insert(key.clone());
                if !example_matches_schema(child_value, child_schema, root, visited_refs) {
                    return None;
                }
            }
        }
    }

    Some(matched_keys)
}