use crate::SchemaNode;
use crate::subset::{SubschemaCheckContext, is_subschema_of_with_productive_context};
use json_schema_ast::{ContainsConstraint, CountRange};
use serde_json::Value;
use super::{
finite_schema_value_superset, scalar::check_enum_inclusion,
schema_definitely_rejects_all_values, schema_may_under_accept_values,
schemas_definitely_disjoint_by_shape,
};
pub(super) struct ArrayConstraints<'a> {
pub(super) prefix_items: &'a [SchemaNode],
pub(super) items: &'a SchemaNode,
pub(super) item_count: CountRange<u64>,
pub(super) contains: Option<&'a ContainsConstraint<SchemaNode>>,
pub(super) unique_items: bool,
pub(super) enumeration: Option<&'a [Value]>,
}
pub(super) fn array_constraints_subsumed(
sub: ArrayConstraints<'_>,
sup: ArrayConstraints<'_>,
context: &mut SubschemaCheckContext,
) -> bool {
if array_constraints_definitely_uninhabited(&sub) {
return true;
}
let Some(sub_item_count) = effective_item_count_for_unique_finite_domain(
sub.prefix_items,
sub.items,
sub.item_count,
sub.unique_items,
) else {
return true;
};
if let Some(contains) = sub.contains
&& (contains_requirement_definitely_impossible(contains, sub_item_count, sub.unique_items)
|| contains_requirement_impossible_for_unique_finite_items(
sub.prefix_items,
sub.items,
sub_item_count,
contains,
sub.unique_items,
))
{
return true;
}
let sub_inherently_unique = sup.unique_items
&& !sub.unique_items
&& array_positions_guaranteed_unique(
sub.prefix_items,
sub.items,
sub_item_count.max(),
context,
);
sup.item_count.contains_range(sub_item_count)
&& (!sup.unique_items
|| sub.unique_items
|| sub_item_count.max().is_some_and(|max_items| max_items <= 1)
|| sub_inherently_unique)
&& array_item_constraints_subsumed(
sub.prefix_items,
sub.items,
sub_item_count.max(),
sup.prefix_items,
sup.items,
context,
)
&& array_contains_constraints_subsumed(
sub.prefix_items,
sub.items,
sub_item_count,
sub.contains,
sub.unique_items,
sup.contains,
context,
)
&& check_enum_inclusion(sub.enumeration, sup.enumeration)
}
fn array_positions_guaranteed_unique(
prefix_items: &[SchemaNode],
items: &SchemaNode,
max_items: Option<u64>,
context: &mut SubschemaCheckContext,
) -> bool {
let Some(max_items) = max_items else {
return false;
};
if max_items <= 1 {
return true;
}
let prefix_len = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
if max_items > prefix_len {
let tail_slots = max_items.saturating_sub(prefix_len);
if tail_slots > 1 && !schema_definitely_rejects_all_values(items) {
return false;
}
}
let checked_prefix = prefix_items
.len()
.min(usize::try_from(max_items).unwrap_or(usize::MAX));
if checked_prefix > 64 {
return false;
}
for i in 0..checked_prefix {
for j in (i + 1)..checked_prefix {
if !schemas_definitely_disjoint(&prefix_items[i], &prefix_items[j], context) {
return false;
}
}
}
if max_items > prefix_len && !schema_definitely_rejects_all_values(items) {
for prefix_item in &prefix_items[..checked_prefix] {
if !schemas_definitely_disjoint(prefix_item, items, context) {
return false;
}
}
}
true
}
fn schemas_definitely_disjoint(
left: &SchemaNode,
right: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
if schema_definitely_rejects_all_values(left) || schema_definitely_rejects_all_values(right) {
return true;
}
if schemas_definitely_disjoint_by_shape(left, right) {
return true;
}
if let Some(values) = finite_schema_value_superset(left)
&& values
.iter()
.all(|value| context.schema_definitely_rejects_value(right, value))
{
return true;
}
if let Some(values) = finite_schema_value_superset(right)
&& values
.iter()
.all(|value| context.schema_definitely_rejects_value(left, value))
{
return true;
}
false
}
pub(super) fn array_constraints_definitely_uninhabited(sub: &ArrayConstraints<'_>) -> bool {
if let Some(contains) = sub.contains
&& contains_requirement_definitely_impossible(contains, sub.item_count, sub.unique_items)
{
return true;
}
if unique_required_positions_exceed_finite_union(
sub.prefix_items,
sub.items,
sub.item_count,
sub.unique_items,
) {
return true;
}
let min_items = sub.item_count.min();
if min_items == 0 {
return false;
}
let required_prefix_len = sub
.prefix_items
.len()
.min(usize::try_from(min_items).unwrap_or(usize::MAX));
if sub.prefix_items[..required_prefix_len]
.iter()
.any(schema_definitely_rejects_all_values)
{
return true;
}
min_items > u64::try_from(sub.prefix_items.len()).unwrap_or(u64::MAX)
&& schema_definitely_rejects_all_values(sub.items)
}
fn array_item_constraints_subsumed(
sub_prefix_items: &[SchemaNode],
sub_items: &SchemaNode,
sub_max_items: Option<u64>,
sup_prefix_items: &[SchemaNode],
sup_items: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
let checked_prefix_len = sub_prefix_items.len().max(sup_prefix_items.len());
for index in 0..checked_prefix_len {
if !array_index_can_exist(sub_max_items, index) {
return true;
}
let sub_item = sub_prefix_items.get(index).unwrap_or(sub_items);
let sup_item = sup_prefix_items.get(index).unwrap_or(sup_items);
if !is_subschema_of_with_productive_context(sub_item, sup_item, context) {
return false;
}
}
!array_index_can_exist(sub_max_items, checked_prefix_len)
|| is_subschema_of_with_productive_context(sub_items, sup_items, context)
}
pub(super) fn unique_finite_domain_guarantees_contains_at_least(
prefix_items: &[SchemaNode],
items: &SchemaNode,
min_items: u64,
unique_items: bool,
target: &SchemaNode,
required_matches: u64,
context: &mut SubschemaCheckContext,
) -> bool {
if required_matches == 0 {
return true;
}
if !unique_items {
return false;
}
let Some(values) = finite_domain_values(items) else {
return false;
};
let guaranteed_prefix_len = prefix_items
.len()
.min(usize::try_from(min_items).unwrap_or(usize::MAX));
let mut guaranteed_prefix_matches = 0_u64;
let mut consumed_tail_nonmatches: Vec<Value> = Vec::new();
for prefix_item in &prefix_items[..guaranteed_prefix_len] {
if is_subschema_of_with_productive_context(prefix_item, target, context) {
guaranteed_prefix_matches = guaranteed_prefix_matches.saturating_add(1);
continue;
}
if let Some(prefix_values) = finite_domain_values(prefix_item)
&& prefix_values.len() == 1
{
let value = &prefix_values[0];
if context.schema_definitely_rejects_value(prefix_item, value) {
return true;
}
if context.superset_contains_value(target, value) {
guaranteed_prefix_matches = guaranteed_prefix_matches.saturating_add(1);
continue;
}
if context.schema_definitely_rejects_value(target, value) {
push_distinct_owned(&mut consumed_tail_nonmatches, value);
}
}
}
if guaranteed_prefix_matches >= required_matches {
return true;
}
let prefix_capacity = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
let min_tail_items = min_items.saturating_sub(prefix_capacity);
if min_tail_items == 0 {
return false;
}
let domain_size = u64::try_from(values.len()).unwrap_or(u64::MAX);
if min_tail_items > domain_size {
return true;
}
let definitely_matching = values
.iter()
.filter(|value| context.superset_contains_value(target, value))
.count();
let definitely_matching = u64::try_from(definitely_matching).unwrap_or(u64::MAX);
let consumed_nonmatching = consumed_tail_nonmatches
.iter()
.filter(|consumed| {
values.iter().any(|value| {
json_schema_ast::json_values_equal(value, consumed)
&& context.schema_definitely_rejects_value(target, value)
})
})
.count();
let consumed_nonmatching = u64::try_from(consumed_nonmatching).unwrap_or(u64::MAX);
let maybe_nonmatching = domain_size
.saturating_sub(definitely_matching)
.saturating_sub(consumed_nonmatching);
let forced_tail_matches = min_tail_items.saturating_sub(maybe_nonmatching);
guaranteed_prefix_matches.saturating_add(forced_tail_matches) >= required_matches
}
pub(super) fn contains_requirement_definitely_impossible(
contains: &ContainsConstraint<SchemaNode>,
item_count: CountRange<u64>,
unique_items: bool,
) -> bool {
let required_matches = contains.count().min();
required_matches > 0
&& (schema_definitely_rejects_all_values(&contains.schema)
|| item_count
.max()
.is_some_and(|max_items| required_matches > max_items)
|| (unique_items
&& finite_match_domain_size(&contains.schema)
.is_some_and(|domain_size| required_matches > domain_size)))
}
pub(super) fn contains_requirement_impossible_for_unique_finite_items(
prefix_items: &[SchemaNode],
items: &SchemaNode,
item_count: CountRange<u64>,
contains: &ContainsConstraint<SchemaNode>,
unique_items: bool,
) -> bool {
let required_matches = contains.count().min();
if required_matches == 0 || !unique_items {
return false;
}
let Some(values) = finite_domain_values(items) else {
return false;
};
let max_prefix_matches = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
let (max_prefix_matches, max_tail_slots) = match item_count.max() {
Some(max_items) => {
let prefix = max_prefix_matches.min(max_items);
let prefix_len = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
(prefix, max_items.saturating_sub(prefix_len))
}
None => (max_prefix_matches, u64::MAX),
};
let may_under_accept = schema_may_under_accept_values(&contains.schema);
let matching_tail_values = values
.iter()
.filter(|value| may_under_accept || contains.schema.accepts_value(value))
.count();
let matching_tail_values = u64::try_from(matching_tail_values).unwrap_or(u64::MAX);
let max_tail_matches = matching_tail_values.min(max_tail_slots);
required_matches > max_prefix_matches.saturating_add(max_tail_matches)
}
fn array_contains_constraints_subsumed(
sub_prefix_items: &[SchemaNode],
sub_items: &SchemaNode,
sub_item_count: CountRange<u64>,
sub_contains: Option<&ContainsConstraint<SchemaNode>>,
sub_unique_items: bool,
sup_contains: Option<&ContainsConstraint<SchemaNode>>,
context: &mut SubschemaCheckContext,
) -> bool {
let Some(sup_contains) = sup_contains else {
return true;
};
let sup_contains_count = sup_contains.count();
let sub_max_items = sub_item_count.max();
let lower_bound_ok = sup_contains_count.min() == 0
|| sub_contains.is_some_and(|sub_contains| {
sub_contains.count().min() >= sup_contains_count.min()
&& is_subschema_of_with_productive_context(
&sub_contains.schema,
&sup_contains.schema,
context,
)
})
|| guaranteed_array_item_matches_at_least(
sub_prefix_items,
sub_items,
sub_item_count.min(),
&sup_contains.schema,
sup_contains_count.min(),
context,
)
|| unique_finite_domain_guarantees_contains_at_least(
sub_prefix_items,
sub_items,
sub_item_count.min(),
sub_unique_items,
&sup_contains.schema,
sup_contains_count.min(),
context,
);
if !lower_bound_ok {
return false;
}
let Some(sup_max_contains) = sup_contains_count.max() else {
return true;
};
sub_contains
.filter(|sub_contains| {
sub_contains
.count()
.max()
.is_some_and(|sub_max_contains| sub_max_contains <= sup_max_contains)
&& is_subschema_of_with_productive_context(
&sup_contains.schema,
&sub_contains.schema,
context,
)
})
.is_some()
|| sub_max_items.is_some_and(|sub_max_items| sub_max_items <= sup_max_contains)
|| (sub_unique_items
&& finite_match_domain_size(&sup_contains.schema)
.is_some_and(|domain_size| domain_size <= sup_max_contains))
|| array_items_match_at_most(
sub_prefix_items,
sub_items,
sub_item_count.max(),
sub_unique_items,
&sup_contains.schema,
context,
)
.is_some_and(|max_matches| max_matches <= sup_max_contains)
}
pub(super) fn effective_item_count_for_unique_finite_domain(
prefix_items: &[SchemaNode],
items: &SchemaNode,
item_count: CountRange<u64>,
unique_items: bool,
) -> Option<CountRange<u64>> {
let mut max_items = item_count.max();
for (index, prefix_item) in prefix_items.iter().enumerate() {
if schema_definitely_rejects_all_values(prefix_item) {
let ceiling = u64::try_from(index).unwrap_or(u64::MAX);
max_items = Some(max_items.map_or(ceiling, |max| max.min(ceiling)));
break;
}
}
if schema_definitely_rejects_all_values(items) {
let ceiling = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
max_items = Some(max_items.map_or(ceiling, |max| max.min(ceiling)));
}
if unique_items && let Some(domain_size) = finite_match_domain_size(items) {
let prefix_capacity = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
let inferred_max = prefix_capacity.saturating_add(domain_size);
max_items = Some(max_items.map_or(inferred_max, |max| max.min(inferred_max)));
}
if unique_items && let Some(capacity) = finite_unique_position_capacity(prefix_items, items) {
max_items = Some(max_items.map_or(capacity, |max| max.min(capacity)));
}
CountRange::new(item_count.min(), max_items)
}
pub(super) fn finite_match_domain_size(schema: &SchemaNode) -> Option<u64> {
finite_domain_values(schema).and_then(|values| u64::try_from(values.len()).ok())
}
fn finite_domain_values(schema: &SchemaNode) -> Option<Vec<Value>> {
finite_schema_value_superset(schema)
}
pub(super) fn array_items_match_at_most(
prefix_items: &[SchemaNode],
items: &SchemaNode,
max_items: Option<u64>,
unique_items: bool,
target: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> Option<u64> {
let possible_prefix_len = match max_items {
Some(max_items) => prefix_items
.len()
.min(usize::try_from(max_items).unwrap_or(usize::MAX)),
None => prefix_items.len(),
};
let mut bound = 0_u64;
for prefix_item in &prefix_items[..possible_prefix_len] {
if schemas_definitely_disjoint_by_shape(prefix_item, target)
|| finite_domain_values(prefix_item).is_some_and(|values| {
values
.iter()
.all(|value| context.schema_definitely_rejects_value(target, value))
})
{
continue;
}
bound = bound.saturating_add(1);
}
let prefix_len_u64 = u64::try_from(prefix_items.len()).unwrap_or(u64::MAX);
let tail_slots = max_items.map(|max_items| max_items.saturating_sub(prefix_len_u64));
if tail_slots == Some(0) {
return Some(bound);
}
if schemas_definitely_disjoint_by_shape(items, target) {
return Some(bound);
}
let Some(tail_values) = finite_domain_values(items) else {
return tail_slots.map(|slots| bound.saturating_add(slots));
};
let maybe_matching_tail_values = tail_values
.iter()
.filter(|value| !context.schema_definitely_rejects_value(target, value))
.count();
let maybe_matching_tail_values = u64::try_from(maybe_matching_tail_values).unwrap_or(u64::MAX);
if maybe_matching_tail_values == 0 {
return Some(bound);
}
if !unique_items {
return tail_slots.map(|slots| bound.saturating_add(slots));
}
let prefix_only_bound = bound;
let mut consumed_matching_tail_values: Vec<Value> = Vec::new();
let tail_can_exist = tail_slots.is_none_or(|slots| slots > 0);
if tail_can_exist {
for prefix_item in prefix_items {
let Some(prefix_values) = finite_domain_values(prefix_item) else {
continue;
};
if prefix_values.len() != 1 {
continue;
}
let prefix_value = &prefix_values[0];
if context.schema_definitely_rejects_value(target, prefix_value) {
continue;
}
if tail_values.iter().any(|tail_value| {
json_schema_ast::json_values_equal(tail_value, prefix_value)
&& !context.schema_definitely_rejects_value(target, tail_value)
}) {
push_distinct_owned(&mut consumed_matching_tail_values, prefix_value);
}
}
}
let consumed_matching_tail_values =
u64::try_from(consumed_matching_tail_values.len()).unwrap_or(u64::MAX);
let available_matching_tail_values =
maybe_matching_tail_values.saturating_sub(consumed_matching_tail_values);
let with_tail_bound = match tail_slots {
Some(slots) => bound.saturating_add(available_matching_tail_values.min(slots)),
None => bound.saturating_add(available_matching_tail_values),
};
Some(prefix_only_bound.max(with_tail_bound))
}
fn push_distinct_owned(distinct: &mut Vec<Value>, value: &Value) {
if !distinct
.iter()
.any(|seen| json_schema_ast::json_values_equal(seen, value))
{
distinct.push(value.clone());
}
}
fn finite_unique_position_capacity(prefix_items: &[SchemaNode], items: &SchemaNode) -> Option<u64> {
let tail_values = finite_domain_values(items)?;
let mut union = Vec::new();
for value in tail_values {
push_distinct_owned(&mut union, &value);
}
let mut unknown_prefix_positions = 0_u64;
for prefix_item in prefix_items {
if let Some(values) = finite_domain_values(prefix_item) {
for value in values {
push_distinct_owned(&mut union, &value);
}
} else {
unknown_prefix_positions = unknown_prefix_positions.saturating_add(1);
}
}
Some(
u64::try_from(union.len())
.unwrap_or(u64::MAX)
.saturating_add(unknown_prefix_positions),
)
}
fn unique_required_positions_exceed_finite_union(
prefix_items: &[SchemaNode],
items: &SchemaNode,
item_count: CountRange<u64>,
unique_items: bool,
) -> bool {
if !unique_items || item_count.min() <= 1 {
return false;
}
let required = item_count.min();
let required_prefix = prefix_items
.len()
.min(usize::try_from(required).unwrap_or(usize::MAX));
let required_tail = required.saturating_sub(u64::try_from(required_prefix).unwrap_or(u64::MAX));
let mut position_domains: Vec<Vec<Value>> = Vec::new();
let mut global_union: Vec<Value> = Vec::new();
for prefix_item in &prefix_items[..required_prefix] {
let Some(values) = finite_domain_values(prefix_item) else {
return false;
};
let domain = distinct_values(values);
if domain.is_empty() {
return true;
}
for value in &domain {
push_distinct_owned(&mut global_union, value);
}
position_domains.push(domain);
}
if required_tail > 0 {
let Some(values) = finite_domain_values(items) else {
return false;
};
let tail_domain = distinct_values(values);
if tail_domain.is_empty() {
return true;
}
if required_tail > u64::try_from(tail_domain.len()).unwrap_or(u64::MAX) {
return true;
}
for value in &tail_domain {
push_distinct_owned(&mut global_union, value);
}
if let Ok(tail_count) = usize::try_from(required_tail) {
if required_prefix.saturating_add(tail_count) <= 10 {
for _ in 0..tail_count {
position_domains.push(tail_domain.clone());
}
}
}
}
if u64::try_from(global_union.len()).unwrap_or(u64::MAX) < required {
return true;
}
position_domains.len() == usize::try_from(required).unwrap_or(usize::MAX)
&& position_domains.len() <= 10
&& has_hall_violation(&position_domains)
}
fn distinct_values(values: Vec<Value>) -> Vec<Value> {
let mut distinct = Vec::new();
for value in values {
push_distinct_owned(&mut distinct, &value);
}
distinct
}
fn has_hall_violation(domains: &[Vec<Value>]) -> bool {
let n = domains.len();
if n <= 1 || n > 10 {
return false;
}
for mask in 1_usize..(1_usize << n) {
let slots = mask.count_ones() as usize;
if slots <= 1 {
continue;
}
let mut union: Vec<Value> = Vec::new();
for (index, domain) in domains.iter().enumerate() {
if (mask & (1_usize << index)) == 0 {
continue;
}
for value in domain {
push_distinct_owned(&mut union, value);
if union.len() >= slots {
break;
}
}
if union.len() >= slots {
break;
}
}
if union.len() < slots {
return true;
}
}
false
}
fn guaranteed_array_item_matches_at_least(
prefix_items: &[SchemaNode],
items: &SchemaNode,
guaranteed_items: u64,
sup_schema: &SchemaNode,
required_matches: u64,
context: &mut SubschemaCheckContext,
) -> bool {
if required_matches == 0 {
return true;
}
let guaranteed_prefix_items = prefix_items
.len()
.min(usize::try_from(guaranteed_items).unwrap_or(usize::MAX));
let mut guaranteed_matches = 0_u64;
for prefix_item in &prefix_items[..guaranteed_prefix_items] {
if is_subschema_of_with_productive_context(prefix_item, sup_schema, context) {
guaranteed_matches += 1;
if guaranteed_matches >= required_matches {
return true;
}
}
}
let guaranteed_tail_items =
guaranteed_items.saturating_sub(u64::try_from(guaranteed_prefix_items).unwrap_or(u64::MAX));
guaranteed_tail_items > 0
&& is_subschema_of_with_productive_context(items, sup_schema, context)
&& guaranteed_matches.saturating_add(guaranteed_tail_items) >= required_matches
}
fn array_index_can_exist(max_items: Option<u64>, index: usize) -> bool {
let Ok(index) = u64::try_from(index) else {
return false;
};
max_items.is_none_or(|max_items| index < max_items)
}