use std::{collections::BTreeMap, sync::Arc};
use referencing::Draft;
use serde_json::Value;
use crate::{
canonical::{
context::{CanonicalizationContext, CompiledMatcher},
ir::{
canonicalize_value_set, tighter, type_set_schema, typed_group, ArrayLeaf, ArrayLeaves,
AtLeastTwo, BoundCardinality, BoundInteger, BoundNumber, BoundRational, CanonicalJson,
ContainsFacet, Discrete, Distinctness, Divisors, ExcludedDivisors, IntegerBounds,
IntegerLeaf, IntegerLeaves, LengthBounds, NonEmpty, NumberLeaf, NumberLeaves,
ObjectLeaf, ObjectLeaves, ObjectViolation, Round, Schema, SchemaKind, Side, StringLeaf,
StringLeaves, UncheckableFacet, Verdict,
},
negate, oracle, parse, DefinitionMap,
},
JsonType, JsonTypeSet,
};
pub(crate) fn intersect(left: Schema, right: Schema, ctx: &CanonicalizationContext) -> Schema {
if let Some(remembered) = ctx.recall_intersection(&left, &right) {
return remembered;
}
let key = (left.clone(), right.clone());
let result = intersect_pair(left, right, ctx);
ctx.remember_intersection(key.0, key.1, &result);
result
}
fn intersect_pair(left: Schema, right: Schema, ctx: &CanonicalizationContext) -> Schema {
match (left.into_kind(), right.into_kind()) {
(SchemaKind::False, _)
| (_, SchemaKind::False)
| (
SchemaKind::TypedGroup { .. } | SchemaKind::Integer(_) | SchemaKind::Number(_),
SchemaKind::String(_),
)
| (
SchemaKind::String(_),
SchemaKind::TypedGroup { .. } | SchemaKind::Integer(_) | SchemaKind::Number(_),
)
| (
SchemaKind::Array(_) | SchemaKind::Object(_),
SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::TypedGroup { .. },
)
| (
SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::TypedGroup { .. },
SchemaKind::Array(_) | SchemaKind::Object(_),
)
| (SchemaKind::Array(_), SchemaKind::Object(_))
| (SchemaKind::Object(_), SchemaKind::Array(_)) => {
Schema::new(SchemaKind::False)
}
(SchemaKind::True, right) => Schema::new(right),
(left, SchemaKind::True) => Schema::new(left),
(SchemaKind::Reference(left), SchemaKind::Reference(right)) if left == right => {
Schema::new(SchemaKind::Reference(left))
}
(SchemaKind::AnyOf(branches), other) | (other, SchemaKind::AnyOf(branches)) => {
distribute(branches, Schema::new(other), ctx)
}
(
left @ (SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)),
right,
)
| (
left,
right @ (SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)),
) => opaque_intersection(Schema::new(left), Schema::new(right), ctx),
(left @ (SchemaKind::Const(_) | SchemaKind::Enum(_)), right) => {
restrict_members(into_members(left), Schema::new(right), ctx)
}
(left, right @ (SchemaKind::Const(_) | SchemaKind::Enum(_))) => {
restrict_members(into_members(right), Schema::new(left), ctx)
}
(SchemaKind::MultiType(first), SchemaKind::MultiType(second)) => {
let cover =
SchemaKind::semantic_cover(first).intersect(SchemaKind::semantic_cover(second));
if cover.is_empty() {
Schema::new(SchemaKind::False)
} else {
type_set_schema(cover)
}
}
(SchemaKind::MultiType(set), SchemaKind::TypedGroup { ty, body })
| (SchemaKind::TypedGroup { ty, body }, SchemaKind::MultiType(set)) => {
if SchemaKind::semantic_cover(set).contains(ty) {
Schema::new(SchemaKind::TypedGroup { ty, body })
} else {
Schema::new(SchemaKind::False)
}
}
(
SchemaKind::TypedGroup { ty: first, body },
SchemaKind::TypedGroup {
ty: second,
body: other,
},
) => {
if first == second {
typed_group(first, intersect(body, other, ctx))
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::MultiType(set), SchemaKind::String(leaf))
| (SchemaKind::String(leaf), SchemaKind::MultiType(set)) => {
if SchemaKind::semantic_cover(set).contains(JsonType::String) {
string_leaf(leaf.into_inner(), ctx)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::String(first), SchemaKind::String(second)) => {
string_leaf(
intersect_string_leaves(first.into_inner(), second.into_inner()),
ctx,
)
}
(SchemaKind::MultiType(set), SchemaKind::Integer(bounds))
| (SchemaKind::Integer(bounds), SchemaKind::MultiType(set)) => {
if SchemaKind::semantic_cover(set).contains(JsonType::Integer) {
integer_leaf(bounds.into_inner(), ctx)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::Integer(first), SchemaKind::Integer(second)) => {
integer_leaf(
intersect_integer_leaves(first.into_inner(), second.into_inner()),
ctx,
)
}
(SchemaKind::TypedGroup { ty, body }, SchemaKind::Number(leaf))
| (SchemaKind::Number(leaf), SchemaKind::TypedGroup { ty, body }) => {
let kept = into_members(body.into_kind())
.into_iter()
.filter(|member| number_leaf_admits(leaf.get(), member))
.collect();
typed_group(ty, canonicalize_value_set(kept))
}
(SchemaKind::TypedGroup { ty, body }, SchemaKind::Integer(leaf))
| (SchemaKind::Integer(leaf), SchemaKind::TypedGroup { ty, body }) => {
let kept = into_members(body.into_kind())
.into_iter()
.filter(|member| integer_leaf_admits(leaf.get(), member))
.collect();
typed_group(ty, canonicalize_value_set(kept))
}
(SchemaKind::Number(first), SchemaKind::Number(second)) => {
number_leaf(
intersect_number_leaves(first.into_inner(), second.into_inner()),
ctx,
)
}
(SchemaKind::MultiType(set), SchemaKind::Number(leaf))
| (SchemaKind::Number(leaf), SchemaKind::MultiType(set)) => {
if set.contains(JsonType::Number) {
number_leaf(leaf.into_inner(), ctx)
} else if set.contains(JsonType::Integer) {
integer_within(&leaf.into_inner(), ctx)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::MultiType(set), SchemaKind::Array(leaf))
| (SchemaKind::Array(leaf), SchemaKind::MultiType(set)) => {
if set.contains(JsonType::Array) {
array_leaf(leaf.into_inner(), ctx)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::Array(first), SchemaKind::Array(second)) => {
match intersect_array_leaves(first.into_inner(), second.into_inner(), ctx) {
Some(leaf) => array_leaf(leaf, ctx),
None => Schema::new(SchemaKind::False),
}
}
(SchemaKind::MultiType(set), SchemaKind::Object(leaf))
| (SchemaKind::Object(leaf), SchemaKind::MultiType(set)) => {
if set.contains(JsonType::Object) {
object_leaf(leaf.into_inner(), ctx)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::Object(first), SchemaKind::Object(second)) => {
object_leaf(
intersect_object_leaves(first.into_inner(), second.into_inner(), ctx),
ctx,
)
}
(SchemaKind::Integer(integers), SchemaKind::Number(numbers))
| (SchemaKind::Number(numbers), SchemaKind::Integer(integers)) => {
let within = integer_within(&numbers.into_inner(), ctx);
intersect(Schema::new(SchemaKind::Integer(integers)), within, ctx)
}
(SchemaKind::Raw(_), _) | (_, SchemaKind::Raw(_)) => {
unreachable!("`Raw` is whole-document; combinators never contain it")
}
}
}
fn opaque_intersection(left: Schema, right: Schema, ctx: &CanonicalizationContext) -> Schema {
let mut symbolic = Vec::new();
let mut structural = Schema::new(SchemaKind::True);
let mut stack = vec![left, right];
while let Some(schema) = stack.pop() {
match schema.into_kind() {
SchemaKind::AllOf(inner) => stack.extend(inner),
kind @ (SchemaKind::Not(_) | SchemaKind::OneOf(_) | SchemaKind::Reference(_)) => {
symbolic.push(Schema::new(kind));
}
kind @ (SchemaKind::MultiType(_)
| SchemaKind::TypedGroup { .. }
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::AnyOf(_)) => {
structural = intersect(structural, Schema::new(kind), ctx);
if matches!(structural.kind(), SchemaKind::False) {
return structural;
}
}
SchemaKind::True | SchemaKind::False | SchemaKind::Raw(_) => {
unreachable!("an opaque conjunct is neither a constant nor a whole document")
}
}
}
debug_assert!(
!symbolic.is_empty(),
"opaque intersection retains at least one symbolic branch"
);
match structural.into_kind() {
SchemaKind::AnyOf(branches) => union(
branches
.into_iter()
.map(|branch| {
let mut conjuncts = symbolic.clone();
conjuncts.push(branch);
opaque_conjunction(conjuncts)
})
.collect(),
ctx,
),
SchemaKind::True => opaque_conjunction(symbolic),
kind @ (SchemaKind::MultiType(_)
| SchemaKind::TypedGroup { .. }
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)
| SchemaKind::False
| SchemaKind::Raw(_)) => {
symbolic.push(Schema::new(kind));
opaque_conjunction(symbolic)
}
}
}
fn opaque_conjunction(branches: Vec<Schema>) -> Schema {
for branch in &branches {
if let SchemaKind::Not(inner) = branch.kind() {
if branches.iter().any(|candidate| candidate == inner) {
return Schema::new(SchemaKind::False);
}
}
}
let schema = match AtLeastTwo::new(branches) {
Ok(branches) => {
debug_assert!(
branches.as_slice().iter().all(|branch| !matches!(
branch.kind(),
SchemaKind::True
| SchemaKind::False
| SchemaKind::AllOf(_)
| SchemaKind::AnyOf(_)
)),
"opaque conjunction branches are flattened, non-trivial, and distributable unions are eliminated"
);
Schema::new(SchemaKind::AllOf(branches))
}
Err(mut lone) => lone.pop().unwrap_or_else(|| Schema::new(SchemaKind::True)),
};
debug_assert!(
contains_reference(&schema),
"opaque intersection is constructed only across a symbolic reference"
);
schema
}
fn partition_by_multiplicity(branches: &[Schema]) -> (Vec<Schema>, Vec<Schema>) {
let mut duplicates = Vec::new();
let mut singles = Vec::new();
let mut start = 0;
while start < branches.len() {
let mut end = start + 1;
while end < branches.len() && branches[end] == branches[start] {
end += 1;
}
if end - start >= 2 {
duplicates.push(branches[start].clone());
} else {
singles.push(branches[start].clone());
}
start = end;
}
(duplicates, singles)
}
pub(crate) fn one_of(
mut branches: Vec<Schema>,
definitions: &DefinitionMap,
ctx: &CanonicalizationContext,
) -> Option<Schema> {
if !branches.iter().any(contains_reference) {
return concrete_one_of(branches, definitions, ctx);
}
branches.retain(|branch| !matches!(branch.kind(), SchemaKind::False));
branches.sort();
let (duplicates, singles) = partition_by_multiplicity(&branches);
if !duplicates.is_empty() {
if singles.is_empty() {
return Some(Schema::new(SchemaKind::False));
}
let mut complements = Vec::with_capacity(duplicates.len());
for duplicate in &duplicates {
let Some(complement) = negate::negate_in_place(duplicate, definitions, ctx) else {
complements.clear();
break;
};
complements.push(complement);
}
if complements.len() == duplicates.len() {
let mut result = one_of(singles, definitions, ctx)?;
for complement in complements {
result = intersect(result, complement, ctx);
}
return Some(result);
}
}
if branches.len() == 1 {
return branches.pop();
}
debug_assert!(
branches.windows(2).all(|pair| pair[0] <= pair[1]),
"oneOf branches are sorted without deduplication"
);
Some(Schema::new(SchemaKind::OneOf(branches)))
}
fn concrete_one_of(
branches: Vec<Schema>,
definitions: &DefinitionMap,
ctx: &CanonicalizationContext,
) -> Option<Schema> {
let overlaps = pairwise_overlaps(&branches, ctx);
let mut result = union(branches, ctx);
for overlap in overlaps {
result = intersect(
result,
negate::negate_in_place(&overlap, definitions, ctx)?,
ctx,
);
}
Some(result)
}
fn pairwise_overlaps(branches: &[Schema], ctx: &CanonicalizationContext) -> Vec<Schema> {
let mut seen: ahash::AHashSet<&CanonicalJson> = ahash::AHashSet::new();
let mut shared: Vec<CanonicalJson> = Vec::new();
let mut finite: Vec<&Schema> = Vec::new();
let mut structural: Vec<&Schema> = Vec::new();
for branch in branches {
match branch.kind() {
SchemaKind::Const(value) => {
if !seen.insert(value) {
shared.push(value.clone());
}
finite.push(branch);
}
SchemaKind::Enum(values) => {
for value in values.as_slice() {
if !seen.insert(value) {
shared.push(value.clone());
}
}
finite.push(branch);
}
SchemaKind::MultiType(_)
| SchemaKind::TypedGroup { .. }
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::AnyOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Raw(_) => structural.push(branch),
}
}
let mut overlaps = Vec::new();
if !shared.is_empty() {
overlaps.push(canonicalize_value_set(shared));
}
for (index, left) in structural.iter().enumerate() {
for right in structural[index + 1..].iter().chain(&finite) {
let intersection = intersect((*left).clone(), (*right).clone(), ctx);
if !matches!(intersection.kind(), SchemaKind::False) {
overlaps.push(intersection);
}
}
}
overlaps
}
pub(crate) fn union(branches: Vec<Schema>, ctx: &CanonicalizationContext) -> Schema {
let mut members: Vec<CanonicalJson> = Vec::new();
let mut types = JsonTypeSet::empty();
let mut groups: Vec<(JsonType, Vec<CanonicalJson>)> = Vec::new();
let mut strings = StringLeaves::default();
let mut integers = IntegerLeaves::default();
let mut numbers = NumberLeaves::default();
let mut arrays = ArrayLeaves::default();
let mut objects = ObjectLeaves::default();
let mut symbolic_branches: Vec<Schema> = Vec::new();
let mut stack = branches;
while let Some(branch) = stack.pop() {
match branch.into_kind() {
SchemaKind::True => return Schema::new(SchemaKind::True),
SchemaKind::False => {}
SchemaKind::AnyOf(inner) => stack.extend(inner),
SchemaKind::MultiType(set) => {
types = union_type_sets(types, set);
}
SchemaKind::Const(value) => members.push(value),
SchemaKind::Enum(values) => members.extend(values),
SchemaKind::TypedGroup { ty, body } => {
let values = into_members(body.into_kind());
match groups.iter_mut().find(|(existing, _)| *existing == ty) {
Some((_, collected)) => collected.extend(values),
None => groups.push((ty, values)),
}
}
SchemaKind::String(leaf) => strings.insert(leaf.into_inner()),
SchemaKind::Integer(leaf) => integers.insert(leaf.into_inner()),
SchemaKind::Number(leaf) => numbers.insert(leaf.into_inner()),
SchemaKind::Array(leaf) => arrays.insert(leaf.into_inner()),
SchemaKind::Object(leaf) => objects.insert(leaf.into_inner()),
SchemaKind::Not(schema) => {
let complement = Schema::new(SchemaKind::Not(schema));
if !symbolic_branches
.iter()
.any(|existing| existing == &complement)
{
symbolic_branches.push(complement);
}
}
SchemaKind::AllOf(branches) => {
let conjunction = Schema::new(SchemaKind::AllOf(branches));
if !symbolic_branches
.iter()
.any(|existing| existing == &conjunction)
{
symbolic_branches.push(conjunction);
}
}
SchemaKind::OneOf(branches) => {
let exclusive = Schema::new(SchemaKind::OneOf(branches));
if !symbolic_branches
.iter()
.any(|existing| existing == &exclusive)
{
symbolic_branches.push(exclusive);
}
}
SchemaKind::Reference(uri) => {
let reference = Schema::new(SchemaKind::Reference(uri));
if !symbolic_branches
.iter()
.any(|existing| existing == &reference)
{
symbolic_branches.push(reference);
}
}
SchemaKind::Raw(_) => {
unreachable!("`Raw` is whole-document; combinators never contain it")
}
}
}
let cover = SchemaKind::semantic_cover(types);
if cover == JsonTypeSet::all() {
return Schema::new(SchemaKind::True);
}
members.retain(|member| !type_set_absorbs_member(cover, member, ctx.draft()));
groups.retain(|(ty, _)| !cover.contains(*ty));
if cover.contains(JsonType::String) {
strings.clear();
}
if cover.contains(JsonType::Integer) {
integers.clear();
}
if cover.contains(JsonType::Number) {
numbers.clear();
}
if cover.contains(JsonType::Array) {
arrays.clear();
}
if cover.contains(JsonType::Object) {
objects.clear();
}
if !strings.is_empty()
|| !integers.is_empty()
|| !numbers.is_empty()
|| !arrays.is_empty()
|| !objects.is_empty()
{
members.retain(|member| {
!lift_degenerate_member(
&mut strings,
&mut integers,
&mut numbers,
&mut arrays,
&mut objects,
member,
ctx,
)
});
}
if !integers.is_empty() {
let windows = integers.as_slice();
groups.retain(|(ty, values)| {
*ty != JsonType::Integer
|| !values
.iter()
.all(|member| windows.iter().any(|leaf| integer_leaf_admits(leaf, member)))
});
}
debug_assert!(integers.is_empty() || !cover.contains(JsonType::Integer));
debug_assert!(strings.is_empty() || !cover.contains(JsonType::String));
debug_assert!(numbers.is_empty() || !cover.contains(JsonType::Number));
debug_assert!(arrays.is_empty() || !cover.contains(JsonType::Array));
debug_assert!(objects.is_empty() || !cover.contains(JsonType::Object));
let mut objects: Vec<ObjectLeaf> = objects.into_iter().collect();
loop {
merge_sole_differing_keys(&mut objects, ctx);
if drop_object_branch_covered_by_siblings(&mut objects, ctx) {
continue;
}
if drop_required_covered_by_sibling(&mut objects, ctx) {
continue;
}
if drop_size_bound_covered_by_sibling(&mut objects, ctx) {
continue;
}
if collapse_object_leaves_covering_domain(&mut objects, ctx) {
continue;
}
if widen_size_window_covered_by_siblings(&mut objects, ctx) {
continue;
}
if !widen_entry_covered_by_sibling(&mut objects, ctx) {
break;
}
}
let mut widened = types;
integers.retain(|leaf| {
let spans_domain = leaf.bounds.is_unbounded()
&& leaf.multiple_of.is_empty()
&& leaf.not_multiple_of.is_empty();
if spans_domain {
widened = union_type_sets(widened, JsonTypeSet::from(JsonType::Integer));
}
!spans_domain
});
numbers.retain(|leaf| {
let spans_domain = leaf.minimum.is_none()
&& leaf.maximum.is_none()
&& leaf.multiple_of.is_empty()
&& leaf.not_multiple_of.is_empty()
&& !leaf.excludes_integers;
if spans_domain {
widened = union_type_sets(widened, JsonTypeSet::from(JsonType::Number));
}
!spans_domain
});
strings.retain(|leaf| {
let spans_domain = leaf.lengths.is_unbounded()
&& leaf.patterns.is_empty()
&& leaf.excluded_patterns.is_empty()
&& leaf.formats.is_empty()
&& leaf.excluded_formats.is_empty()
&& leaf.content_media_types.is_empty()
&& leaf.content_encodings.is_empty()
&& leaf.excluded.is_empty();
if spans_domain {
widened = union_type_sets(widened, JsonTypeSet::from(JsonType::String));
}
!spans_domain
});
arrays.retain(|leaf| {
let spans_domain = leaf.spans_domain();
if spans_domain {
widened = union_type_sets(widened, JsonTypeSet::from(JsonType::Array));
}
!spans_domain
});
objects.retain(|leaf| {
let spans_domain = leaf.spans_domain();
if spans_domain {
widened = union_type_sets(widened, JsonTypeSet::from(JsonType::Object));
}
!spans_domain
});
if widened != types {
debug_assert!(
SchemaKind::semantic_cover(widened).union(SchemaKind::semantic_cover(types))
== SchemaKind::semantic_cover(widened),
"type set lost a member"
);
return rerun(
widened,
members,
groups,
strings,
integers,
numbers,
arrays,
objects,
symbolic_branches,
ctx,
);
}
if !numbers.is_empty() {
let intervals = numbers.as_slice();
integers.retain(|window| {
!intervals
.iter()
.any(|interval| number_leaf_covers_integer_leaf(interval, window))
});
groups.retain(|(ty, values)| {
*ty != JsonType::Integer
|| !values.iter().all(|member| {
intervals
.iter()
.any(|leaf| number_leaf_admits(leaf, member))
})
});
}
if !members.is_empty()
&& (!strings.is_empty()
|| !integers.is_empty()
|| !numbers.is_empty()
|| !arrays.is_empty()
|| !objects.is_empty())
{
let compiled: Vec<(&StringLeaf, StringMatchers)> = strings
.as_slice()
.iter()
.map(|leaf| (leaf, StringMatchers::compile(leaf, ctx)))
.collect();
let windows = integers.as_slice();
let intervals = numbers.as_slice();
let array_leaves = arrays.as_slice();
let object_leaves = objects.as_slice();
members.retain(|member| {
!leaf_absorbs_member(
&compiled,
windows,
intervals,
array_leaves,
object_leaves,
member,
ctx,
)
});
}
let value_set = canonicalize_value_set(members);
if let SchemaKind::MultiType(saturated) = value_set.kind() {
let widened = union_type_sets(types, *saturated);
debug_assert!(
SchemaKind::semantic_cover(widened).union(SchemaKind::semantic_cover(types))
== SchemaKind::semantic_cover(widened),
"type set lost a member"
);
debug_assert!(widened != types, "re-run without a wider type set");
return rerun(
widened,
Vec::new(),
groups,
strings,
integers,
numbers,
arrays,
objects,
symbolic_branches,
ctx,
);
}
if !types.is_empty() {
if let Some(members) = value_set.kind().finite_values() {
let mut saturated = JsonTypeSet::empty();
if members.iter().any(|member| member.as_value().is_null()) {
saturated = saturated.insert(JsonType::Null);
}
let holds = |wanted: bool| {
members
.iter()
.any(|member| matches!(member.as_value(), Value::Bool(held) if *held == wanted))
};
if holds(false) && holds(true) {
saturated = saturated.insert(JsonType::Boolean);
}
let widened = union_type_sets(types, saturated);
if widened != types {
let remaining: Vec<CanonicalJson> = members
.iter()
.filter(|member| match member.as_value() {
Value::Null => !saturated.contains(JsonType::Null),
Value::Bool(_) => !saturated.contains(JsonType::Boolean),
Value::Number(_)
| Value::String(_)
| Value::Array(_)
| Value::Object(_) => true,
})
.cloned()
.collect();
return rerun(
widened,
remaining,
groups,
strings,
integers,
numbers,
arrays,
objects,
symbolic_branches,
ctx,
);
}
}
}
let finite_domains = JsonType::Null | JsonType::Boolean;
let (types, value_set) = match value_set.kind().finite_values() {
Some(members) if !types.is_empty() && finite_domains.union(types) == finite_domains => {
let mut expanded = members.to_vec();
if types.contains(JsonType::Null) {
expanded.push(CanonicalJson::from_value(&Value::Null));
}
if types.contains(JsonType::Boolean) {
expanded.push(CanonicalJson::from_value(&Value::Bool(false)));
expanded.push(CanonicalJson::from_value(&Value::Bool(true)));
}
let dissolved = canonicalize_value_set(expanded);
debug_assert!(
dissolved.kind().finite_values().is_some(),
"a dissolved type list saturated back into types"
);
(JsonTypeSet::empty(), dissolved)
}
_ => (types, value_set),
};
let mut out: Vec<Schema> = Vec::new();
if !types.is_empty() {
out.push(type_set_schema(types));
}
for (ty, values) in groups {
let body = canonicalize_value_set(values);
if body.kind().finite_values().is_some() && !value_set_admits_group(&value_set, &body) {
out.push(typed_group(ty, body));
}
}
for leaf in numbers {
out.push(number_leaf(leaf, ctx));
}
for leaf in strings {
out.push(string_leaf(leaf, ctx));
}
for bounds in integers {
out.push(integer_leaf(bounds, ctx));
}
for leaf in arrays {
out.push(array_leaf(leaf, ctx));
}
for leaf in objects {
debug_assert!(
!leaf.spans_domain(),
"a leaf spanning the object domain joins the type set before assembly"
);
out.push(object_leaf(leaf, ctx));
}
out.extend(symbolic_branches);
if !matches!(value_set.kind(), SchemaKind::False) {
out.push(value_set);
}
let held = conjuncts_held(&out);
if drop_conjuncts_a_complement_branch_covers(&mut out) {
debug_assert!(
conjuncts_held(&out) < held,
"shedding left the branches as they were"
);
return union(out, ctx);
}
let top_level: ahash::AHashSet<Schema> = out.iter().cloned().collect();
if out.iter().any(
|branch| matches!(branch.kind(), SchemaKind::Not(operand) if top_level.contains(operand)),
) {
return Schema::new(SchemaKind::True);
}
out.retain(|branch| {
let SchemaKind::AllOf(conjuncts) = branch.kind() else {
return true;
};
!conjuncts
.as_slice()
.iter()
.any(|conjunct| top_level.contains(conjunct))
});
drop_covered_conjunctions(&mut out, ctx);
drop_property_alternatives_covered_by_sibling(&mut out, ctx);
match AtLeastTwo::new(out) {
Ok(branches) => {
debug_assert!(
branches.as_slice().iter().all(|branch| !matches!(
branch.kind(),
SchemaKind::True | SchemaKind::False | SchemaKind::AnyOf(_)
)),
"union branch is not in normal form"
);
Schema::new(SchemaKind::AnyOf(branches))
}
Err(mut lone) => match lone.pop() {
Some(only) => only,
None => Schema::new(SchemaKind::False),
},
}
}
#[allow(clippy::wildcard_enum_match_arm)]
fn lift_degenerate_member(
strings: &mut StringLeaves,
integers: &mut IntegerLeaves,
numbers: &mut NumberLeaves,
arrays: &mut ArrayLeaves,
objects: &mut ObjectLeaves,
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> bool {
match member.as_value() {
Value::Array(items) if items.is_empty() && !arrays.is_empty() => {
arrays.insert(ArrayLeaf {
lengths: LengthBounds {
minimum: None,
maximum: Some(BoundCardinality::from(0)),
},
distinctness: Distinctness::Unconstrained,
prefix: Vec::new(),
items: None,
contains: Vec::new(),
});
true
}
Value::Object(map) if map.is_empty() && !objects.is_empty() => {
objects.insert(ObjectLeaf {
sizes: LengthBounds {
minimum: None,
maximum: Some(BoundCardinality::from(0)),
},
additional: None,
required: Vec::new(),
property_names: None,
properties: BTreeMap::new(),
pattern_properties: BTreeMap::new(),
violations: Vec::new(),
});
true
}
Value::String(text) if text.is_empty() && !strings.is_empty() => {
strings.insert(StringLeaf {
lengths: LengthBounds {
minimum: None,
maximum: Some(BoundCardinality::from(0)),
},
patterns: Vec::new(),
excluded_patterns: Vec::new(),
formats: Vec::new(),
excluded_formats: Vec::new(),
content_media_types: Vec::new(),
content_encodings: Vec::new(),
excluded: Vec::new(),
});
true
}
Value::Number(number)
if !integers.is_empty()
&& !matches!(ctx.draft(), Draft::Draft4)
&& BoundInteger::from_number(number).is_some() =>
{
let bound = BoundInteger::from_number(number).expect("checked in the guard");
integers.insert(IntegerLeaf {
bounds: IntegerBounds {
minimum: Some(bound.clone()),
maximum: Some(bound),
},
multiple_of: Divisors::default(),
not_multiple_of: ExcludedDivisors::default(),
});
true
}
Value::Number(number) if !numbers.is_empty() => {
let bound = BoundNumber::new(number, true);
let window = NumberLeaf {
minimum: Some(bound.clone()),
maximum: Some(bound),
multiple_of: Divisors::default(),
not_multiple_of: ExcludedDivisors::default(),
excludes_integers: false,
};
let collapses_back = matches!(
number_leaf(window.clone(), ctx).kind(),
SchemaKind::Const(point) if point.as_value() == &Value::Number(number.clone())
);
if collapses_back {
numbers.insert(window);
}
collapses_back
}
_ => false,
}
}
fn rerun(
types: JsonTypeSet,
members: Vec<CanonicalJson>,
groups: Vec<(JsonType, Vec<CanonicalJson>)>,
strings: StringLeaves,
integers: IntegerLeaves,
numbers: NumberLeaves,
arrays: ArrayLeaves,
objects: Vec<ObjectLeaf>,
symbolic_branches: Vec<Schema>,
ctx: &CanonicalizationContext,
) -> Schema {
let mut rest: Vec<Schema> = vec![Schema::new(SchemaKind::MultiType(types))];
rest.push(canonicalize_value_set(members));
rest.extend(
groups
.into_iter()
.map(|(ty, values)| typed_group(ty, canonicalize_value_set(values))),
);
rest.extend(strings.into_iter().map(|leaf| string_leaf(leaf, ctx)));
rest.extend(integers.into_iter().map(|leaf| integer_leaf(leaf, ctx)));
rest.extend(numbers.into_iter().map(|leaf| number_leaf(leaf, ctx)));
rest.extend(arrays.into_iter().map(|leaf| array_leaf(leaf, ctx)));
rest.extend(objects.into_iter().map(|leaf| object_leaf(leaf, ctx)));
rest.extend(symbolic_branches);
union(rest, ctx)
}
fn merge_sole_differing_keys(leaves: &mut Vec<ObjectLeaf>, ctx: &CanonicalizationContext) {
let mut folded = true;
while folded {
folded = false;
'search: for first in 0..leaves.len() {
for second in first + 1..leaves.len() {
if leaves[first].additional.is_some() || leaves[second].additional.is_some() {
continue;
}
if let Some(merged) = united_sole_key(&leaves[first], &leaves[second], ctx) {
leaves[first] = merged;
leaves.remove(second);
folded = true;
break 'search;
}
}
}
}
}
fn united_sole_key(
left: &ObjectLeaf,
right: &ObjectLeaf,
ctx: &CanonicalizationContext,
) -> Option<ObjectLeaf> {
if left.sizes != right.sizes
|| left.property_names != right.property_names
|| left.pattern_properties != right.pattern_properties
|| left.violations != right.violations
{
return None;
}
let key = sole_differing_key(left, right)?;
let required = if left.required.contains(&key) {
if right.required.contains(&key) {
left.required.clone()
} else {
right.required.clone()
}
} else {
left.required.clone()
};
let united_entry = match (left.properties.get(&key), right.properties.get(&key)) {
(Some(first), Some(second)) => {
let schema = if first == second {
first.clone()
} else {
union(vec![first.clone(), second.clone()], ctx)
};
if matches!(schema.kind(), SchemaKind::True) {
None
} else {
Some(schema)
}
}
_ => None,
};
let mut properties = left.properties.clone();
properties.remove(&key);
if let Some(schema) = united_entry {
properties.insert(Arc::clone(&key), schema);
}
Some(ObjectLeaf {
sizes: left.sizes.clone(),
required,
property_names: left.property_names.clone(),
properties,
pattern_properties: left.pattern_properties.clone(),
additional: None,
violations: left.violations.clone(),
})
}
fn sole_differing_key(left: &ObjectLeaf, right: &ObjectLeaf) -> Option<Arc<str>> {
let mut differing: Vec<Arc<str>> = Vec::new();
let note = |key: &Arc<str>, differing: &mut Vec<Arc<str>>| {
if !differing.iter().any(|seen| seen == key) {
differing.push(Arc::clone(key));
}
};
for key in &left.required {
if !right.required.contains(key) {
note(key, &mut differing);
}
}
for key in &right.required {
if !left.required.contains(key) {
note(key, &mut differing);
}
}
for (key, schema) in &left.properties {
if right.properties.get(key) != Some(schema) {
note(key, &mut differing);
}
}
for (key, schema) in &right.properties {
if left.properties.get(key) != Some(schema) {
note(key, &mut differing);
}
}
match differing.as_slice() {
[_] => differing.pop(),
_ => None,
}
}
fn collapse_object_leaves_covering_domain(
leaves: &mut Vec<ObjectLeaf>,
ctx: &CanonicalizationContext,
) -> bool {
if leaves.len() < 2 {
return false;
}
let mut keys: Vec<Arc<str>> = leaves
.iter()
.flat_map(|leaf| leaf.required.iter().chain(leaf.properties.keys()).cloned())
.collect();
keys.sort();
keys.dedup();
let piece = ObjectLeaf {
sizes: LengthBounds::default(),
required: Vec::new(),
property_names: None,
properties: BTreeMap::new(),
pattern_properties: BTreeMap::new(),
additional: None,
violations: Vec::new(),
};
let packed = packed_leaves(leaves, ctx);
if !split_piece_is_covered(piece.clone(), &packed, &keys, ctx) {
return false;
}
leaves.clear();
leaves.push(piece);
true
}
fn packed_leaves(leaves: &[ObjectLeaf], ctx: &CanonicalizationContext) -> Vec<Schema> {
leaves
.iter()
.map(|leaf| object_leaf(leaf.clone(), ctx))
.collect()
}
fn siblings_of(packed: &[Schema], index: usize) -> Vec<Schema> {
packed
.iter()
.enumerate()
.filter(|(sibling, _)| *sibling != index)
.map(|(_, schema)| schema.clone())
.collect()
}
fn keys_beside(leaves: &[ObjectLeaf], index: usize) -> Vec<Arc<str>> {
let mut keys: Vec<Arc<str>> = leaves
.iter()
.enumerate()
.filter(|(sibling, _)| *sibling != index)
.flat_map(|(_, leaf)| leaf.required.iter().chain(leaf.properties.keys()).cloned())
.collect();
keys.sort();
keys.dedup();
keys
}
fn split_piece_is_covered(
piece: ObjectLeaf,
leaves: &[Schema],
keys: &[Arc<str>],
ctx: &CanonicalizationContext,
) -> bool {
debug_assert!(
keys.windows(2).all(|pair| pair[0] < pair[1]),
"the split keys are sorted and deduplicated"
);
let schema = object_leaf(piece.clone(), ctx);
if matches!(schema.kind(), SchemaKind::False) {
return true;
}
let mut any_within_reach = false;
for leaf in leaves {
if !piece_meets_demands(&schema, leaf) {
continue;
}
any_within_reach = true;
if oracle::covers(&schema, leaf, ctx) == Verdict::Admits {
return true;
}
}
if !any_within_reach && barring_keys_keeps_the_piece(&piece, keys) {
return false;
}
let Some((key, rest)) = keys.split_first() else {
return false;
};
let mut holding = piece.clone();
if let Err(position) = holding.required.binary_search(key) {
holding.required.insert(position, Arc::clone(key));
}
let mut missing = piece;
missing
.properties
.insert(Arc::clone(key), Schema::new(SchemaKind::False));
split_piece_is_covered(holding, leaves, rest, ctx)
&& split_piece_is_covered(missing, leaves, rest, ctx)
}
fn piece_meets_demands(piece: &Schema, leaf: &Schema) -> bool {
let (SchemaKind::Object(piece_leaf), SchemaKind::Object(other)) = (piece.kind(), leaf.kind())
else {
return true;
};
let demanded = &piece_leaf.get().required;
other
.get()
.required
.iter()
.all(|key| demanded.binary_search(key).is_ok())
}
fn barring_keys_keeps_the_piece(piece: &ObjectLeaf, keys: &[Arc<str>]) -> bool {
piece.property_names.is_none()
&& piece.additional.is_none()
&& piece.pattern_properties.is_empty()
&& piece.sizes.maximum.is_none()
&& piece
.required
.iter()
.all(|key| keys.binary_search(key).is_err())
}
fn widen_size_window_covered_by_siblings(
leaves: &mut [ObjectLeaf],
ctx: &CanonicalizationContext,
) -> bool {
let packed = packed_leaves(leaves, ctx);
for index in 0..leaves.len() {
let Some(rays) = negate::length_windows(&leaves[index].sizes) else {
continue;
};
if rays.is_empty() {
continue;
}
let siblings = siblings_of(&packed, index);
let keys = keys_beside(leaves, index);
for ray in rays {
let drops_minimum = ray.minimum.is_none();
let mut piece = leaves[index].clone();
piece.sizes = ray;
if split_piece_is_covered(piece, &siblings, &keys, ctx) {
if drops_minimum {
leaves[index].sizes.minimum = None;
} else {
leaves[index].sizes.maximum = None;
}
return true;
}
}
}
false
}
fn drop_object_branch_covered_by_siblings(
leaves: &mut Vec<ObjectLeaf>,
ctx: &CanonicalizationContext,
) -> bool {
let packed = packed_leaves(leaves, ctx);
for index in 0..leaves.len() {
let siblings = siblings_of(&packed, index);
let keys = keys_beside(leaves, index);
if split_piece_is_covered(leaves[index].clone(), &siblings, &keys, ctx) {
leaves.remove(index);
return true;
}
for divider in 0..leaves.len() {
if divider == index {
continue;
}
let Some(mut windows) = negate::length_windows(&leaves[divider].sizes) else {
continue;
};
if windows.is_empty() {
continue;
}
windows.push(leaves[divider].sizes.clone());
let all_covered = windows.iter().all(|window| {
let mut piece = leaves[index].clone();
piece.sizes = LengthBounds {
minimum: tighter(piece.sizes.minimum.take(), window.minimum.clone(), Ord::max),
maximum: tighter(piece.sizes.maximum.take(), window.maximum.clone(), Ord::min),
};
split_piece_is_covered(piece, &siblings, &keys, ctx)
});
if all_covered {
leaves.remove(index);
return true;
}
}
}
false
}
fn drop_required_covered_by_sibling(
leaves: &mut [ObjectLeaf],
ctx: &CanonicalizationContext,
) -> bool {
for index in 0..leaves.len() {
if leaves[index].additional.is_some() {
continue;
}
for key_index in 0..leaves[index].required.len() {
let implied_floor = BoundCardinality::from(leaves[index].required.len() as u64);
for keep_floor in [false, true] {
if keep_floor && leaves[index].sizes.minimum.is_some() {
break;
}
let leaf = &leaves[index];
let key = Arc::clone(&leaf.required[key_index]);
let mut weakened = leaf.clone();
weakened.required.remove(key_index);
if keep_floor {
weakened.sizes.minimum = Some(implied_floor.clone());
}
let mut gained = weakened.clone();
gained
.properties
.insert(Arc::clone(&key), Schema::new(SchemaKind::False));
let gained = object_leaf(gained, ctx);
if matches!(gained.kind(), SchemaKind::False) {
continue;
}
let covered =
(0..leaves.len())
.filter(|&sibling| sibling != index)
.any(|sibling| {
intersect(
gained.clone(),
object_leaf(leaves[sibling].clone(), ctx),
ctx,
) == gained
});
if covered {
leaves[index] = weakened;
return true;
}
}
}
}
false
}
fn drop_size_bound_covered_by_sibling(
leaves: &mut [ObjectLeaf],
ctx: &CanonicalizationContext,
) -> bool {
for index in 0..leaves.len() {
let slice_covered = |slice: ObjectLeaf, leaves: &[ObjectLeaf]| {
let slice = object_leaf(slice, ctx);
!matches!(slice.kind(), SchemaKind::False)
&& (0..leaves.len())
.filter(|&sibling| sibling != index)
.any(|sibling| {
intersect(
slice.clone(),
object_leaf(leaves[sibling].clone(), ctx),
ctx,
) == slice
})
};
if let Some(below_ceiling) = leaves[index]
.sizes
.minimum
.as_ref()
.and_then(|minimum| minimum.clone().checked_decrement())
{
let mut slice = leaves[index].clone();
slice.sizes.minimum = None;
slice.sizes.maximum = Some(below_ceiling);
if slice_covered(slice, leaves) {
leaves[index].sizes.minimum = None;
return true;
}
}
if let Some(above_floor) = leaves[index]
.sizes
.maximum
.as_ref()
.and_then(|maximum| maximum.clone().checked_increment())
{
let mut slice = leaves[index].clone();
slice.sizes.minimum = Some(above_floor.clone());
slice.sizes.maximum = None;
if slice_covered(slice, leaves) {
leaves[index].sizes.maximum = None;
return true;
}
let slots_filled =
leaves[index].sizes.maximum.as_ref() == Some(&leaves[index].required_count());
if !slots_filled {
continue;
}
for sibling in (0..leaves.len()).filter(|&sibling| sibling != index) {
let mut enriched = leaves[index].clone();
enriched.sizes.maximum = None;
for (key, entry) in &leaves[sibling].properties {
if enriched.required.binary_search(key).is_err() {
enriched
.properties
.entry(Arc::clone(key))
.or_insert_with(|| entry.clone());
}
}
let mut slice = enriched.clone();
slice.sizes.minimum = Some(above_floor.clone());
let slice = object_leaf(slice, ctx);
let held = matches!(slice.kind(), SchemaKind::False)
|| intersect(
slice.clone(),
object_leaf(leaves[sibling].clone(), ctx),
ctx,
) == slice;
if held {
leaves[index] = enriched;
return true;
}
}
}
}
false
}
fn widen_entry_covered_by_sibling(
leaves: &mut [ObjectLeaf],
ctx: &CanonicalizationContext,
) -> bool {
for index in 0..leaves.len() {
if leaves[index].additional.is_some() {
continue;
}
let keys: Vec<Arc<str>> = leaves[index].properties.keys().cloned().collect();
for key in keys {
for sibling in (0..leaves.len()).filter(|&sibling| sibling != index) {
if leaves[sibling].additional.is_some() {
continue;
}
let entry = &leaves[index].properties[&key];
let sibling_entry = leaves[sibling].properties.get(&key);
let widened_entry = match sibling_entry {
Some(other) if other == entry => continue,
Some(other) => {
let united = union(vec![entry.clone(), other.clone()], ctx);
if &united == entry {
continue;
}
Some(united).filter(|united| !matches!(united.kind(), SchemaKind::True))
}
None => None,
};
let mut widened = leaves[index].clone();
match widened_entry {
Some(united) => {
widened.properties.insert(Arc::clone(&key), united);
}
None => {
widened.properties.remove(&key);
}
}
let mut gained = widened.clone();
match leaves[sibling].properties.get(&key) {
Some(other) => {
gained.properties.insert(Arc::clone(&key), other.clone());
}
None => {
gained.properties.remove(&key);
}
}
if let Err(position) = gained.required.binary_search(&key) {
gained.required.insert(position, Arc::clone(&key));
}
let gained = object_leaf(gained, ctx);
let covered = matches!(gained.kind(), SchemaKind::False)
|| intersect(
gained.clone(),
object_leaf(leaves[sibling].clone(), ctx),
ctx,
) == gained;
if covered {
leaves[index] = widened;
return true;
}
}
}
}
false
}
fn distribute(
branches: AtLeastTwo<Schema>,
other: Schema,
ctx: &CanonicalizationContext,
) -> Schema {
let (rest, last) = branches.split_last();
let mut out: Vec<Schema> = rest
.into_iter()
.map(|branch| intersect(branch, other.clone(), ctx))
.collect();
out.push(intersect(last, other, ctx));
union(out, ctx)
}
fn into_members(kind: SchemaKind) -> Vec<CanonicalJson> {
match kind {
SchemaKind::Const(value) => vec![value],
SchemaKind::Enum(values) => values.into_vec(),
other @ (SchemaKind::MultiType(_)
| SchemaKind::TypedGroup { .. }
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::AnyOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Raw(_)) => unreachable!("value-set kind expected: {other:?}"),
}
}
fn restrict_members(
members: Vec<CanonicalJson>,
other: Schema,
ctx: &CanonicalizationContext,
) -> Schema {
match other.into_kind() {
kind @ (SchemaKind::Const(_) | SchemaKind::Enum(_)) => {
let admitted = into_members(kind);
canonicalize_value_set(
members
.into_iter()
.filter(|member| admitted.binary_search(member).is_ok())
.collect(),
)
}
SchemaKind::MultiType(set) => parse::restrict_values_to_types(members, set, ctx),
SchemaKind::String(leaf) => {
let matchers = StringMatchers::compile(leaf.get(), ctx);
let kept = members
.into_iter()
.filter(|member| {
!matches!(
string_leaf_admits(
leaf.get(),
&matchers,
member,
UncheckableFacet::Skipped,
ctx
),
Verdict::Rejects
)
})
.collect();
canonicalize_value_set(kept)
}
SchemaKind::Integer(leaf) => {
let kept = members
.into_iter()
.filter(|member| integer_leaf_admits(leaf.get(), member))
.collect();
let value_set = canonicalize_value_set(kept);
if matches!(ctx.draft(), Draft::Draft4) {
typed_group(JsonType::Integer, value_set)
} else {
value_set
}
}
SchemaKind::TypedGroup { ty, body } => {
let admitted = into_members(body.into_kind());
let kept: Vec<_> = members
.into_iter()
.filter(|member| member.json_type() == ty && admitted.binary_search(member).is_ok())
.collect();
typed_group(ty, canonicalize_value_set(kept))
}
SchemaKind::Number(leaf) => {
let mut kept = Vec::new();
let mut partial = Vec::new();
for member in members {
match restrict_number_member(leaf.get(), &member, ctx) {
MemberRestriction::Full => kept.push(member),
MemberRestriction::Empty => {}
MemberRestriction::Partial(schema) => partial.push(schema),
}
}
let mut branches = vec![canonicalize_value_set(kept)];
branches.extend(partial);
union(branches, ctx)
}
SchemaKind::Array(leaf) => {
let mut kept = Vec::new();
let mut partial = Vec::new();
for member in members {
match restrict_array_member(leaf.get(), &member, ctx) {
MemberRestriction::Full => kept.push(member),
MemberRestriction::Empty => {}
MemberRestriction::Partial(schema) => partial.push(schema),
}
}
let mut branches = vec![canonicalize_value_set(kept)];
branches.extend(partial);
union(branches, ctx)
}
SchemaKind::Object(leaf) => {
let mut kept = Vec::new();
let mut partial = Vec::new();
for member in members {
match restrict_object_member(leaf.get(), &member, ctx) {
MemberRestriction::Full => kept.push(member),
MemberRestriction::Empty => {}
MemberRestriction::Partial(schema) => partial.push(schema),
}
}
let mut branches = vec![canonicalize_value_set(kept)];
branches.extend(partial);
union(branches, ctx)
}
other @ (SchemaKind::True
| SchemaKind::False
| SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::AnyOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)
| SchemaKind::Raw(_)) => unreachable!("dispatch handles the remaining kinds: {other:?}"),
}
}
fn type_set_absorbs_member(cover: JsonTypeSet, member: &CanonicalJson, draft: Draft) -> bool {
let ty = member.json_type();
if !cover.contains(ty) {
return false;
}
!(matches!(draft, Draft::Draft4)
&& ty == JsonType::Integer
&& !cover.contains(JsonType::Number))
}
fn value_set_admits_group(value_set: &Schema, body: &Schema) -> bool {
let (Some(admitted), Some(values)) = (
value_set.kind().finite_values(),
body.kind().finite_values(),
) else {
return false;
};
values
.iter()
.all(|value| admitted.binary_search(value).is_ok())
}
#[allow(clippy::wildcard_enum_match_arm)]
fn leaf_absorbs_member(
strings: &[(&StringLeaf, StringMatchers)],
integers: &[IntegerLeaf],
numbers: &[NumberLeaf],
arrays: &[ArrayLeaf],
objects: &[ObjectLeaf],
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> bool {
match member.as_value() {
Value::Array(items) => arrays
.iter()
.any(|leaf| matches!(array_leaf_admits(leaf, items, ctx), Verdict::Admits)),
Value::Object(map) => objects
.iter()
.any(|leaf| matches!(object_leaf_admits(leaf, map, ctx), Verdict::Admits)),
Value::String(_) => strings.iter().any(|(leaf, matchers)| {
matches!(
string_leaf_admits(leaf, matchers, member, UncheckableFacet::Undecided, ctx),
Verdict::Admits
)
}),
Value::Number(_) => {
numbers.iter().any(|leaf| number_leaf_admits(leaf, member))
|| (!matches!(ctx.draft(), Draft::Draft4)
&& integers
.iter()
.any(|leaf| integer_leaf_admits(leaf, member)))
}
_ => false,
}
}
fn union_type_sets(left: JsonTypeSet, right: JsonTypeSet) -> JsonTypeSet {
SchemaKind::canonical_type_set(left.union(right))
}
pub(crate) fn string_leaf(mut leaf: StringLeaf, ctx: &CanonicalizationContext) -> Schema {
if formats_conflict(&leaf, ctx) || patterns_conflict(&leaf) {
return Schema::new(SchemaKind::False);
}
absorb_empty_exclusion(&mut leaf);
prune_excluded_formats(&mut leaf, ctx);
prune_excluded(&mut leaf, ctx);
let Some(leaf) = NonEmpty::new(leaf) else {
return Schema::new(SchemaKind::False);
};
if leaf.get().patterns.is_empty()
&& leaf.get().excluded_patterns.is_empty()
&& leaf.get().formats.is_empty()
&& leaf.get().content_media_types.is_empty()
&& leaf.get().content_encodings.is_empty()
&& leaf
.get()
.lengths
.maximum
.as_ref()
.is_some_and(BoundCardinality::is_zero)
{
return Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::String(String::new()),
)));
}
Schema::new(SchemaKind::String(leaf))
}
fn drop_conjuncts_a_complement_branch_covers(branches: &mut [Schema]) -> bool {
let complemented: ahash::AHashSet<Schema> = branches
.iter()
.filter_map(|branch| {
if let SchemaKind::Not(operand) = branch.kind() {
Some(operand.clone())
} else {
None
}
})
.collect();
if complemented.is_empty() {
return false;
}
let mut shed = false;
for branch in branches.iter_mut() {
let SchemaKind::AllOf(conjuncts) = branch.kind() else {
continue;
};
let kept: Vec<Schema> = conjuncts
.as_slice()
.iter()
.filter(|conjunct| !complemented.contains(*conjunct))
.cloned()
.collect();
if kept.is_empty() || kept.len() == conjuncts.as_slice().len() {
continue;
}
*branch = match AtLeastTwo::new(kept) {
Ok(remaining) => Schema::new(SchemaKind::AllOf(remaining)),
Err(mut lone) => lone.pop().expect("a non-empty conjunct list"),
};
shed = true;
}
shed
}
fn conjuncts_held(branches: &[Schema]) -> usize {
branches.iter().map(|branch| demands(branch).len()).sum()
}
fn drop_property_alternatives_covered_by_sibling(
branches: &mut [Schema],
ctx: &CanonicalizationContext,
) {
for index in 0..branches.len() {
let Some(narrowed) = narrow_branch_entries(branches, index, ctx) else {
continue;
};
branches[index] = narrowed;
}
}
fn narrow_branch_entries(
branches: &[Schema],
index: usize,
ctx: &CanonicalizationContext,
) -> Option<Schema> {
let SchemaKind::AllOf(conjuncts) = branches[index].kind() else {
return None;
};
let mut rebuilt = conjuncts.as_slice().to_vec();
let mut narrowed = false;
let mut slot = 0;
while slot < rebuilt.len() {
let SchemaKind::Object(leaf) = rebuilt[slot].kind() else {
slot += 1;
continue;
};
for (key, entry) in &leaf.get().properties {
let SchemaKind::AnyOf(alternatives) = entry.kind() else {
continue;
};
let kept: Vec<Schema> = alternatives
.as_slice()
.iter()
.filter(|alternative| {
!alternative_is_covered(branches, index, slot, key, alternative, ctx)
})
.cloned()
.collect();
if kept.len() == alternatives.as_slice().len() {
continue;
}
let mut narrower = leaf.get().clone();
narrower
.properties
.insert(Arc::clone(key), union(kept, ctx));
rebuilt[slot] = object_leaf(narrower, ctx);
narrowed = true;
break;
}
slot += 1;
}
if !narrowed {
return None;
}
Some(conjoin(rebuilt, ctx))
}
fn alternative_is_covered(
branches: &[Schema],
index: usize,
slot: usize,
key: &Arc<str>,
alternative: &Schema,
ctx: &CanonicalizationContext,
) -> bool {
let mut restricted = demands(&branches[index]).to_vec();
let SchemaKind::Object(leaf) = restricted[slot].kind() else {
return false;
};
let mut pinned = leaf.get().clone();
pinned
.properties
.insert(Arc::clone(key), alternative.clone());
restricted[slot] = object_leaf(pinned, ctx);
let piece = conjoin(restricted, ctx);
branches
.iter()
.enumerate()
.filter(|(sibling, _)| *sibling != index)
.any(|(_, sibling)| intersect(piece.clone(), sibling.clone(), ctx) == piece)
}
fn drop_covered_conjunctions(branches: &mut Vec<Schema>, ctx: &CanonicalizationContext) {
if !branches
.iter()
.any(|branch| matches!(branch.kind(), SchemaKind::AllOf(_)))
{
return;
}
let mut index = 0;
while index < branches.len() {
if conjunction_is_covered(branches, index, ctx) {
branches.remove(index);
} else {
index += 1;
}
}
}
fn conjoin(members: Vec<Schema>, ctx: &CanonicalizationContext) -> Schema {
members
.into_iter()
.fold(Schema::new(SchemaKind::True), |held, member| {
intersect(held, member, ctx)
})
}
fn demands(branch: &Schema) -> &[Schema] {
if let SchemaKind::AllOf(conjuncts) = branch.kind() {
conjuncts.as_slice()
} else {
std::slice::from_ref(branch)
}
}
fn conjunction_is_covered(
branches: &[Schema],
index: usize,
ctx: &CanonicalizationContext,
) -> bool {
if !matches!(branches[index].kind(), SchemaKind::AllOf(_)) {
return false;
}
let covered = demands(&branches[index]);
branches
.iter()
.enumerate()
.filter(|(sibling, _)| *sibling != index)
.any(|(_, sibling)| {
demands(sibling).iter().all(|wanted| {
covered
.iter()
.any(|held| intersect(held.clone(), wanted.clone(), ctx) == *held)
})
})
}
fn absorb_empty_exclusion(leaf: &mut StringLeaf) {
if leaf.lengths.minimum.is_some() {
return;
}
let Some(index) = leaf.excluded.iter().position(|value| value.is_empty()) else {
return;
};
leaf.excluded.remove(index);
leaf.lengths.minimum = Some(BoundCardinality::from(1));
}
fn prune_excluded_formats(leaf: &mut StringLeaf, ctx: &CanonicalizationContext) {
if leaf.excluded_formats.is_empty() {
return;
}
let lengths = leaf.lengths.clone();
leaf.excluded_formats.retain(|format| {
let Some((minimum, maximum)) = crate::keywords::format::length_window(ctx.draft(), format)
else {
return true;
};
!lengths
.clone()
.intersect(LengthBounds {
minimum: Some(BoundCardinality::from(minimum)),
maximum: Some(BoundCardinality::from(maximum)),
})
.is_empty()
});
}
fn prune_excluded(leaf: &mut StringLeaf, ctx: &CanonicalizationContext) {
if leaf.excluded.is_empty() {
return;
}
let matchers = StringMatchers::compile(leaf, ctx);
let excluded = std::mem::take(&mut leaf.excluded);
leaf.excluded = excluded
.into_iter()
.filter(|value| {
!matches!(
string_leaf_admits_text(leaf, &matchers, value, UncheckableFacet::Undecided, ctx),
Verdict::Rejects
)
})
.collect();
}
fn intersect_integer_leaves(first: IntegerLeaf, second: IntegerLeaf) -> IntegerLeaf {
IntegerLeaf {
bounds: first.bounds.intersect(second.bounds),
multiple_of: first.multiple_of.intersect(second.multiple_of),
not_multiple_of: first.not_multiple_of.intersect(second.not_multiple_of),
}
}
pub(crate) fn number_leaf(leaf: NumberLeaf, ctx: &CanonicalizationContext) -> Schema {
let leaf = if leaf.excludes_integers && !matches!(ctx.draft(), Draft::Draft4) {
NumberLeaf {
not_multiple_of: leaf
.not_multiple_of
.intersect(ExcludedDivisors::one(whole_divisor())),
excludes_integers: false,
..leaf
}
} else {
leaf
};
let leaf = snap_to_progression(leaf);
if ctx.draft() != Draft::Draft4
&& leaf
.multiple_of
.sole()
.is_some_and(BoundRational::admits_only_whole)
{
if let Some(bounds) = integer_bounds_within(&leaf) {
return integer_leaf(
IntegerLeaf {
bounds,
multiple_of: leaf.multiple_of,
not_multiple_of: leaf.not_multiple_of,
},
ctx,
);
}
}
let Some(leaf) = NonEmpty::new(leaf) else {
return Schema::new(SchemaKind::False);
};
if let (Some(min), Some(max)) = (&leaf.get().minimum, &leaf.get().maximum) {
if min.is_inclusive() && max.is_inclusive() && min.to_number() == max.to_number() {
let point = min.to_number();
if leaf.get().excludes_integers && jsonschema_value::types::number_is_integer(&point) {
return Schema::new(SchemaKind::Number(leaf));
}
return if leaf.get().multiple_of.divide(&point)
&& !leaf.get().not_multiple_of.bars(&point)
{
Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Number(point),
)))
} else {
Schema::new(SchemaKind::False)
};
}
}
debug_assert!(
leaf.get().excludes_integers || integer_bounds_within(leaf.get()).is_some(),
"a number leaf admitting integers holds ends the integer bounds can spell"
);
Schema::new(SchemaKind::Number(leaf))
}
pub(crate) fn array_leaf(mut leaf: ArrayLeaf, ctx: &CanonicalizationContext) -> Schema {
if !normalize_contains(&mut leaf) {
return Schema::new(SchemaKind::False);
}
normalize_items(&mut leaf);
if !reconcile_contains_window(&mut leaf, ctx) {
return Schema::new(SchemaKind::False);
}
if !reconcile_contains_positions(&leaf, ctx) {
return Schema::new(SchemaKind::False);
}
match leaf.distinctness {
Distinctness::Unconstrained => {}
Distinctness::AllDistinct => {
if leaf.contains.iter().any(|facet| {
facet
.schema
.kind()
.finite_domain_size()
.is_some_and(|domain| {
facet.effective_minimum() > BoundCardinality::from(domain)
})
}) {
return Schema::new(SchemaKind::False);
}
if let Some(ceiling) = distinct_length_ceiling(&leaf, ctx) {
leaf.lengths.maximum = Some(match leaf.lengths.maximum.take() {
Some(maximum) => maximum.min(ceiling),
None => ceiling,
});
}
}
Distinctness::SomeRepeated => {
let floor = BoundCardinality::from(2);
leaf.lengths.minimum = Some(match leaf.lengths.minimum.take() {
Some(minimum) => minimum.max(floor),
None => floor,
});
}
}
if leaf
.lengths
.maximum
.as_ref()
.is_some_and(|max| *max <= BoundCardinality::from(1))
{
match leaf.distinctness {
Distinctness::AllDistinct => leaf.distinctness = Distinctness::Unconstrained,
Distinctness::SomeRepeated => debug_assert!(
leaf.lengths.is_empty(),
"a repeat demand inside a single-item window survived its length floor"
),
Distinctness::Unconstrained => {}
}
}
let Some(leaf) = NonEmpty::new(leaf) else {
return Schema::new(SchemaKind::False);
};
if leaf
.get()
.lengths
.maximum
.as_ref()
.is_some_and(BoundCardinality::is_zero)
{
return Schema::new(SchemaKind::Const(CanonicalJson::from_value(&Value::Array(
Vec::new(),
))));
}
Schema::new(SchemaKind::Array(leaf))
}
fn normalize_contains(leaf: &mut ArrayLeaf) -> bool {
if leaf.contains.is_empty() {
return true;
}
let mut facets = std::mem::take(&mut leaf.contains);
facets.sort_by(|left, right| left.schema.cmp(&right.schema));
let mut merged: Vec<ContainsFacet> = Vec::with_capacity(facets.len());
for facet in facets {
match merged.last_mut() {
Some(last) if last.schema == facet.schema => {
let minimum = last.effective_minimum().max(facet.effective_minimum());
last.minimum = Some(minimum);
last.maximum = match (last.maximum.take(), facet.maximum) {
(Some(left), Some(right)) => Some(left.min(right)),
(one, None) | (None, one) => one,
};
}
_ => merged.push(facet),
}
}
for mut facet in merged {
let minimum = facet.effective_minimum();
if facet.maximum.as_ref().is_some_and(|max| minimum > *max) {
return false;
}
if matches!(facet.schema.kind(), SchemaKind::True) {
if !minimum.is_zero()
&& leaf
.lengths
.minimum
.as_ref()
.is_none_or(|current| *current < minimum)
{
leaf.lengths.minimum = Some(minimum);
}
if let Some(maximum) = facet.maximum {
leaf.lengths.maximum = Some(match leaf.lengths.maximum.take() {
Some(current) => current.min(maximum),
None => maximum,
});
}
continue;
}
if matches!(facet.schema.kind(), SchemaKind::False) {
if minimum.is_zero() {
continue;
}
return false;
}
if minimum.is_zero() && facet.maximum.is_none() {
continue;
}
facet.minimum = (minimum != BoundCardinality::from(1)).then_some(minimum);
leaf.contains.push(facet);
}
true
}
fn reconcile_contains_window(leaf: &mut ArrayLeaf, ctx: &CanonicalizationContext) -> bool {
let Some(implied) = implied_length_floor(&leaf.contains, ctx) else {
return true;
};
if leaf
.lengths
.maximum
.as_ref()
.is_some_and(|max| implied > *max)
{
return false;
}
if leaf
.lengths
.minimum
.as_ref()
.is_some_and(|min| *min <= implied)
{
leaf.lengths.minimum = None;
}
debug_assert!(
leaf.lengths
.minimum
.as_ref()
.is_none_or(|min| *min > implied),
"a length minimum the demands already imply is dropped"
);
true
}
fn implied_length_floor(
demands: &[ContainsFacet],
ctx: &CanonicalizationContext,
) -> Option<BoundCardinality> {
let mut order: Vec<&ContainsFacet> = demands.iter().collect();
order.sort_by_key(|facet| std::cmp::Reverse(facet.effective_minimum()));
let mut summed: Vec<&Schema> = Vec::new();
let mut floor: Option<BoundCardinality> = None;
for facet in order {
let minimum = facet.effective_minimum();
if minimum.is_zero() {
continue;
}
let disjoint = summed.iter().all(|counted| {
matches!(
intersect((*counted).clone(), facet.schema.clone(), ctx).kind(),
SchemaKind::False
)
});
if !disjoint {
continue;
}
floor = match floor {
Some(current) => current.clone().checked_add(&minimum).or(Some(current)),
None => Some(minimum),
};
summed.push(&facet.schema);
}
debug_assert!(
demands
.iter()
.map(ContainsFacet::effective_minimum)
.max()
.unwrap_or_default()
<= floor.clone().unwrap_or_default(),
"the floor holds at least the largest single demanded count"
);
floor
}
fn distinct_length_ceiling(
leaf: &ArrayLeaf,
ctx: &CanonicalizationContext,
) -> Option<BoundCardinality> {
let tail = leaf.items.as_ref()?;
let domain = tail.kind().finite_domain_size()?;
let independent = leaf
.prefix
.iter()
.filter(|schema| intersect((*schema).clone(), tail.clone(), ctx) != **schema)
.count() as u64;
Some(BoundCardinality::from(domain.saturating_add(independent)))
}
fn reconcile_contains_positions(leaf: &ArrayLeaf, ctx: &CanonicalizationContext) -> bool {
for facet in &leaf.contains {
let minimum = facet.effective_minimum();
if minimum.is_zero() {
continue;
}
let tail_reachable = leaf
.lengths
.maximum
.as_ref()
.is_none_or(|max| BoundCardinality::from(leaf.prefix.len() as u64) < *max);
if tail_reachable {
let tail = leaf
.items
.clone()
.unwrap_or_else(|| Schema::new(SchemaKind::True));
if !matches!(
intersect(tail, facet.schema.clone(), ctx).kind(),
SchemaKind::False
) {
continue;
}
}
let matching = leaf
.prefix
.iter()
.filter(|schema| {
!matches!(
intersect((*schema).clone(), facet.schema.clone(), ctx).kind(),
SchemaKind::False
)
})
.count();
if BoundCardinality::from(matching as u64) < minimum {
return false;
}
}
true
}
fn normalize_items(leaf: &mut ArrayLeaf) {
if leaf
.items
.as_ref()
.is_some_and(|tail| matches!(tail.kind(), SchemaKind::True))
{
leaf.items = None;
}
if leaf
.items
.as_ref()
.is_some_and(|tail| matches!(tail.kind(), SchemaKind::False))
{
let prefix_len = leaf.prefix.len();
cap_length(leaf, prefix_len);
}
if let Some(rejecting) = leaf
.prefix
.iter()
.position(|schema| matches!(schema.kind(), SchemaKind::False))
{
cap_length(leaf, rejecting);
}
if leaf.lengths.maximum.is_some() {
let keep = reachable_prefix_len(leaf);
if keep < leaf.prefix.len() {
leaf.prefix.truncate(keep);
leaf.items = None;
}
}
while leaf.prefix.last().is_some_and(|last| match &leaf.items {
Some(tail) => last == tail,
None => matches!(last.kind(), SchemaKind::True),
}) {
leaf.prefix.pop();
}
debug_assert!(
!leaf
.prefix
.iter()
.any(|schema| matches!(schema.kind(), SchemaKind::False)),
"a rejecting prefix schema survived normalization"
);
debug_assert!(
reachable_prefix_len(leaf) == leaf.prefix.len(),
"a prefix schema beyond the length ceiling survived normalization"
);
}
fn reachable_prefix_len(leaf: &ArrayLeaf) -> usize {
leaf.prefix
.iter()
.enumerate()
.take_while(|(index, _)| {
leaf.lengths
.maximum
.as_ref()
.is_none_or(|max| BoundCardinality::from(*index as u64) < *max)
})
.count()
}
fn cap_length(leaf: &mut ArrayLeaf, ceiling: usize) {
let ceiling = BoundCardinality::from(ceiling as u64);
leaf.lengths.maximum = Some(match leaf.lengths.maximum.take() {
Some(max) => max.min(ceiling),
None => ceiling,
});
let keep = reachable_prefix_len(leaf);
leaf.prefix.truncate(keep);
leaf.items = None;
}
fn intersect_array_leaves(
first: ArrayLeaf,
second: ArrayLeaf,
ctx: &CanonicalizationContext,
) -> Option<ArrayLeaf> {
let distinctness = match (first.distinctness, second.distinctness) {
(Distinctness::Unconstrained, other) | (other, Distinctness::Unconstrained) => other,
(Distinctness::AllDistinct, Distinctness::AllDistinct) => Distinctness::AllDistinct,
(Distinctness::SomeRepeated, Distinctness::SomeRepeated) => Distinctness::SomeRepeated,
(Distinctness::AllDistinct, Distinctness::SomeRepeated)
| (Distinctness::SomeRepeated, Distinctness::AllDistinct) => return None,
};
let length = first.prefix.len().max(second.prefix.len());
let mut prefix = Vec::with_capacity(length);
for index in 0..length {
let left = element_constraint(&first, index);
let right = element_constraint(&second, index);
prefix.push(intersect(left, right, ctx));
}
let items = match (first.items, second.items) {
(Some(left), Some(right)) => Some(intersect(left, right, ctx)),
(items, None) | (None, items) => items,
};
let mut contains = first.contains;
contains.extend(second.contains);
Some(ArrayLeaf {
lengths: first.lengths.intersect(second.lengths),
distinctness,
prefix,
items,
contains,
})
}
fn element_schema(leaf: &ArrayLeaf, index: usize) -> Option<&Schema> {
leaf.prefix.get(index).or(leaf.items.as_ref())
}
fn element_constraint(leaf: &ArrayLeaf, index: usize) -> Schema {
element_schema(leaf, index)
.cloned()
.unwrap_or_else(|| Schema::new(SchemaKind::True))
}
fn has_duplicate_elements(elements: &[Value]) -> bool {
elements
.iter()
.enumerate()
.any(|(index, element)| elements[..index].contains(element))
}
fn meets_distinctness(leaf: &ArrayLeaf, elements: &[Value]) -> bool {
match leaf.distinctness {
Distinctness::Unconstrained => true,
Distinctness::AllDistinct => !has_duplicate_elements(elements),
Distinctness::SomeRepeated => has_duplicate_elements(elements),
}
}
fn array_leaf_admits(leaf: &ArrayLeaf, items: &[Value], ctx: &CanonicalizationContext) -> Verdict {
if !leaf
.lengths
.contains(&BoundCardinality::from(items.len() as u64))
{
return Verdict::Rejects;
}
if !meets_distinctness(leaf, items) {
return Verdict::Rejects;
}
contains_verdict(&leaf.contains, items, UncheckableFacet::Undecided, ctx).and(Verdict::all(
items
.iter()
.enumerate()
.map(|(index, element)| match element_schema(leaf, index) {
Some(schema) => admits_value(schema, element, UncheckableFacet::Undecided, ctx),
None => Verdict::Admits,
}),
))
}
fn contains_verdict(
facets: &[ContainsFacet],
elements: &[Value],
uncheckable: UncheckableFacet,
ctx: &CanonicalizationContext,
) -> Verdict {
let mut verdict = Verdict::Admits;
for facet in facets {
let mut definite: u64 = 0;
let mut possible: u64 = 0;
for element in elements {
match admits_value(&facet.schema, element, uncheckable, ctx) {
Verdict::Admits => {
definite += 1;
possible += 1;
}
Verdict::Unknown => possible += 1,
Verdict::Rejects => {}
}
}
let definite = BoundCardinality::from(definite);
let possible = BoundCardinality::from(possible);
if possible < facet.effective_minimum()
|| facet.maximum.as_ref().is_some_and(|max| definite > *max)
{
return Verdict::Rejects;
}
if definite < facet.effective_minimum()
|| facet.maximum.as_ref().is_some_and(|max| possible > *max)
{
verdict = Verdict::Unknown;
}
}
verdict
}
enum MemberRestriction {
Full,
Empty,
Partial(Schema),
}
fn restrict_array_member(
leaf: &ArrayLeaf,
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> MemberRestriction {
let Value::Array(elements) = member.as_value() else {
return MemberRestriction::Empty;
};
if !leaf
.lengths
.contains(&BoundCardinality::from(elements.len() as u64))
{
return MemberRestriction::Empty;
}
if !meets_distinctness(leaf, elements) {
return MemberRestriction::Empty;
}
let (mut full, contains) =
match contains_verdict(&leaf.contains, elements, UncheckableFacet::Skipped, ctx) {
Verdict::Rejects => return MemberRestriction::Empty,
Verdict::Unknown => (false, leaf.contains.clone()),
Verdict::Admits => (true, Vec::new()),
};
debug_assert!(
contains.is_empty()
|| leaf
.contains
.iter()
.any(|facet| contains_reference(&facet.schema)),
"only reference-bearing contains facets survive an undecidable finite member"
);
let mut restricted = Vec::with_capacity(elements.len());
for (index, element) in elements.iter().enumerate() {
let pin = Schema::new(SchemaKind::Const(CanonicalJson::from_value(element)));
let entry = match element_schema(leaf, index) {
None => pin,
Some(schema) => {
let entry = intersect(schema.clone(), pin.clone(), ctx);
if matches!(entry.kind(), SchemaKind::False) {
return MemberRestriction::Empty;
}
if entry != pin {
full = false;
}
entry
}
};
restricted.push(entry);
}
if full {
debug_assert!(
contains.is_empty(),
"a fully admitted array has no unresolved contains demand"
);
return MemberRestriction::Full;
}
let length = BoundCardinality::from(elements.len() as u64);
MemberRestriction::Partial(array_leaf(
ArrayLeaf {
lengths: LengthBounds {
minimum: Some(length.clone()),
maximum: Some(length),
},
distinctness: Distinctness::Unconstrained,
prefix: restricted,
items: None,
contains,
},
ctx,
))
}
pub(crate) fn object_leaf(mut leaf: ObjectLeaf, ctx: &CanonicalizationContext) -> Schema {
normalize_additional(&mut leaf, ctx);
normalize_property_names(&mut leaf, ctx);
debug_assert!(
!leaf.violations.iter().any(|violation| match violation {
ObjectViolation::NameFails(violated) => matches!(violated.kind(), SchemaKind::True)
|| matches!(violated.kind(), SchemaKind::MultiType(set) if set.contains(JsonType::String)),
ObjectViolation::UndeclaredValueFails { additional, .. } => {
matches!(additional.kind(), SchemaKind::True)
}
}),
"a demand no key can break survived construction"
);
if let Some(names) = &leaf.property_names {
for violation in &leaf.violations {
let ObjectViolation::NameFails(violated) = violation else {
continue;
};
if violated == names {
return Schema::new(SchemaKind::False);
}
if let Some(values) = names.kind().finite_values() {
if values.iter().all(|value| {
matches!(value.as_value(), Value::String(key)
if matches!(admits_key(violated, key, ctx), Verdict::Admits))
}) {
return Schema::new(SchemaKind::False);
}
}
}
}
if let Some(shield) = &leaf.additional {
for violation in &leaf.violations {
if let ObjectViolation::UndeclaredValueFails {
names,
patterns,
additional,
} = violation
{
if additional == shield
&& leaf.properties.keys().eq(names.iter())
&& leaf.pattern_properties.keys().eq(patterns.iter())
{
return Schema::new(SchemaKind::False);
}
}
}
}
let required = &leaf.required;
leaf.violations.retain(|violation| {
let ObjectViolation::NameFails(violated) = violation else {
return true;
};
!required
.iter()
.any(|key| matches!(admits_key(violated, key, ctx), Verdict::Rejects))
});
if leaf
.effective_sizes()
.maximum
.as_ref()
.is_some_and(|max| *max <= leaf.required_count())
&& leaf.violations.iter().any(|violation| match violation {
ObjectViolation::NameFails(violated) => required
.iter()
.all(|key| matches!(admits_key(violated, key, ctx), Verdict::Admits)),
ObjectViolation::UndeclaredValueFails {
names, patterns, ..
} => required.iter().all(|key| {
names.contains(key)
|| patterns
.iter()
.any(|pattern| matches_key(pattern, key, ctx))
}),
})
{
return Schema::new(SchemaKind::False);
}
expand_additional_over_admitted_keys(&mut leaf);
if leaf.spans_domain() {
return type_set_schema(JsonTypeSet::from(JsonType::Object));
}
debug_assert!(
!leaf.property_names.as_ref().is_some_and(|names| {
matches!(names.kind(), SchemaKind::False)
|| matches!(names.kind(), SchemaKind::MultiType(set) if *set == JsonTypeSet::from(JsonType::String))
}),
"a key constraint survived normalization without constraining keys"
);
if leaf
.required
.iter()
.any(|key| matches!(key_schema(&leaf, key, ctx).kind(), SchemaKind::False))
{
return Schema::new(SchemaKind::False);
}
if let Some(names) = &leaf.property_names {
if leaf
.required
.iter()
.any(|key| matches!(admits_key(names, key, ctx), Verdict::Rejects))
{
return Schema::new(SchemaKind::False);
}
}
normalize_properties(&mut leaf, ctx);
normalize_pattern_properties(&mut leaf, ctx);
if leaf
.sizes
.maximum
.as_ref()
.is_some_and(|max| *max == leaf.required_count())
{
let required = &leaf.required;
leaf.properties
.retain(|key, _| required.binary_search(key).is_ok());
}
if leaf
.sizes
.minimum
.as_ref()
.is_some_and(|min| *min <= leaf.required_count())
{
leaf.sizes.minimum = None;
}
if let Some(admitted) = leaf.admitted_key_count() {
if leaf
.sizes
.maximum
.as_ref()
.is_some_and(|max| *max >= admitted)
{
leaf.sizes.maximum = None;
}
}
let Some(leaf) = NonEmpty::new(leaf) else {
return Schema::new(SchemaKind::False);
};
if leaf
.get()
.effective_sizes()
.maximum
.as_ref()
.is_some_and(BoundCardinality::is_zero)
{
debug_assert!(
leaf.get().violations.is_empty(),
"a demand survived a zero ceiling past the slot check"
);
return Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Object(serde_json::Map::new()),
)));
}
Schema::new(SchemaKind::Object(leaf))
}
fn normalize_property_names(leaf: &mut ObjectLeaf, ctx: &CanonicalizationContext) {
let Some(names) = leaf.property_names.take() else {
return;
};
let names = if is_string_domain(names.kind()) {
names
} else {
narrow_to_strings(names, ctx)
};
if matches!(names.kind(), SchemaKind::MultiType(set) if *set == JsonTypeSet::from(JsonType::String))
{
return;
}
if matches!(names.kind(), SchemaKind::False) {
leaf.sizes = leaf.sizes.clone().intersect(LengthBounds {
minimum: None,
maximum: Some(BoundCardinality::from(0)),
});
return;
}
leaf.property_names = Some(names);
}
fn normalize_additional(leaf: &mut ObjectLeaf, ctx: &CanonicalizationContext) {
let Some(shield) = leaf.additional.take() else {
return;
};
if matches!(shield.kind(), SchemaKind::True) {
return;
}
if matches!(shield.kind(), SchemaKind::False) {
let allowed = union(
leaf.properties
.keys()
.map(|key| {
Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::String(key.to_string()),
)))
})
.collect(),
ctx,
);
leaf.property_names = Some(match leaf.property_names.take() {
Some(names) => intersect(names, allowed, ctx),
None => allowed,
});
return;
}
leaf.additional = Some(shield);
}
fn expand_additional_over_admitted_keys(leaf: &mut ObjectLeaf) {
if leaf.additional.is_none() {
return;
}
let Some(keys) = admitted_keys(leaf) else {
return;
};
let shield = leaf
.additional
.take()
.expect("the early return proved a shield present");
for key in keys {
leaf.properties.entry(key).or_insert_with(|| shield.clone());
}
}
fn normalize_properties(leaf: &mut ObjectLeaf, ctx: &CanonicalizationContext) {
let names = leaf.property_names.clone();
let shielded = leaf.additional.is_some();
leaf.properties.retain(|key, schema| {
(shielded || !matches!(schema.kind(), SchemaKind::True))
&& names
.as_ref()
.is_none_or(|names| !matches!(admits_key(names, key, ctx), Verdict::Rejects))
});
}
fn normalize_pattern_properties(leaf: &mut ObjectLeaf, ctx: &CanonicalizationContext) {
leaf.pattern_properties
.retain(|_, schema| !matches!(schema.kind(), SchemaKind::True));
if leaf.pattern_properties.is_empty() {
return;
}
if let Some(keys) = admitted_keys(leaf) {
let patterns = std::mem::take(&mut leaf.pattern_properties);
for key in keys {
merge_matching_patterns(&mut leaf.properties, &patterns, &key, ctx);
}
return;
}
let patterns = leaf.pattern_properties.clone();
let keys: Vec<Arc<str>> = leaf.properties.keys().cloned().collect();
for key in keys {
merge_matching_patterns(&mut leaf.properties, &patterns, &key, ctx);
}
}
fn merge_matching_patterns(
properties: &mut BTreeMap<Arc<str>, Schema>,
patterns: &BTreeMap<Arc<str>, Schema>,
key: &Arc<str>,
ctx: &CanonicalizationContext,
) {
for (pattern, schema) in patterns {
if !matches_key(pattern, key, ctx) {
continue;
}
let merged = match properties.remove(key) {
Some(existing) => intersect(existing, schema.clone(), ctx),
None => schema.clone(),
};
properties.insert(Arc::clone(key), merged);
}
}
fn admitted_keys(leaf: &ObjectLeaf) -> Option<Vec<Arc<str>>> {
let values = leaf.property_names.as_ref()?.kind().finite_values()?;
Some(
values
.iter()
.map(|value| {
let Value::String(key) = value.as_value() else {
unreachable!(
"a key constraint survives normalization only in the string domain"
)
};
Arc::from(key.as_str())
})
.collect(),
)
}
fn key_schema(leaf: &ObjectLeaf, key: &str, ctx: &CanonicalizationContext) -> Schema {
let mut schema = leaf
.properties
.get(key)
.or_else(|| governing_shield(leaf, key, ctx))
.cloned()
.unwrap_or_else(|| Schema::new(SchemaKind::True));
for (pattern, pattern_schema) in &leaf.pattern_properties {
if matches_key(pattern, key, ctx) {
schema = intersect(schema, pattern_schema.clone(), ctx);
}
}
schema
}
fn matches_key(pattern: &Arc<str>, key: &str, ctx: &CanonicalizationContext) -> bool {
ctx.compile_regex(pattern)
.expect("pattern validated during parsing")
.is_match(key)
}
fn narrow_to_strings(names: Schema, ctx: &CanonicalizationContext) -> Schema {
let strings = Schema::new(SchemaKind::MultiType(JsonTypeSet::from(JsonType::String)));
intersect(names, strings, ctx)
}
fn is_string_domain(kind: &SchemaKind) -> bool {
match kind {
SchemaKind::Const(value) => value.as_value().is_string(),
SchemaKind::Enum(values) => values
.as_slice()
.iter()
.all(|value| value.as_value().is_string()),
SchemaKind::String(_) | SchemaKind::False => true,
SchemaKind::MultiType(set) => *set == JsonTypeSet::from(JsonType::String),
SchemaKind::AnyOf(branches) => branches
.as_slice()
.iter()
.all(|branch| is_string_domain(branch.kind())),
SchemaKind::AllOf(branches) => branches
.as_slice()
.iter()
.any(|branch| is_string_domain(branch.kind())),
SchemaKind::True
| SchemaKind::TypedGroup { .. }
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Not(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)
| SchemaKind::Raw(_) => false,
}
}
fn admits_key(names: &Schema, key: &str, ctx: &CanonicalizationContext) -> Verdict {
match names.kind() {
SchemaKind::Const(value) => {
Verdict::from_bool(matches!(value.as_value(), Value::String(text) if text == key))
}
SchemaKind::Enum(values) => Verdict::from_bool(
values
.as_slice()
.iter()
.any(|value| matches!(value.as_value(), Value::String(text) if text == key)),
),
SchemaKind::String(leaf) => {
let matchers = StringMatchers::compile(leaf.get(), ctx);
string_leaf_admits_text(leaf.get(), &matchers, key, UncheckableFacet::Undecided, ctx)
}
SchemaKind::AnyOf(branches) => Verdict::any(
branches
.as_slice()
.iter()
.map(|branch| admits_key(branch, key, ctx)),
),
SchemaKind::AllOf(branches) => Verdict::all(
branches
.as_slice()
.iter()
.map(|branch| admits_key(branch, key, ctx)),
),
SchemaKind::Not(_) | SchemaKind::OneOf(_) | SchemaKind::Reference(_) => Verdict::Unknown,
SchemaKind::MultiType(set) => Verdict::from_bool(set.contains(JsonType::String)),
SchemaKind::TypedGroup { .. }
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Array(_)
| SchemaKind::Object(_)
| SchemaKind::Raw(_) => {
unreachable!("a key constraint survives normalization only in the string domain")
}
}
}
pub(crate) fn admits_value(
schema: &Schema,
value: &Value,
uncheckable: UncheckableFacet,
ctx: &CanonicalizationContext,
) -> Verdict {
if contains_reference(schema) {
return Verdict::Unknown;
}
let member = Schema::new(SchemaKind::Const(CanonicalJson::from_value(value)));
if intersect(schema.clone(), member.clone(), ctx) != member {
return Verdict::Rejects;
}
if matches!(uncheckable, UncheckableFacet::Undecided)
&& has_uncheckable_string_facet(schema, ctx)
{
return Verdict::Unknown;
}
Verdict::Admits
}
pub(crate) fn contains_reference(schema: &Schema) -> bool {
match schema.kind() {
SchemaKind::Reference(_) => true,
SchemaKind::Not(inner) | SchemaKind::TypedGroup { body: inner, .. } => {
contains_reference(inner)
}
SchemaKind::AllOf(branches) | SchemaKind::AnyOf(branches) => {
for branch in branches.as_slice() {
if contains_reference(branch) {
return true;
}
}
false
}
SchemaKind::OneOf(branches) => {
for branch in branches {
if contains_reference(branch) {
return true;
}
}
false
}
SchemaKind::Array(leaf) => {
let leaf = leaf.get();
for schema in &leaf.prefix {
if contains_reference(schema) {
return true;
}
}
if let Some(schema) = &leaf.items {
if contains_reference(schema) {
return true;
}
}
for facet in &leaf.contains {
if contains_reference(&facet.schema) {
return true;
}
}
false
}
SchemaKind::Object(leaf) => {
let leaf = leaf.get();
if let Some(schema) = &leaf.property_names {
if contains_reference(schema) {
return true;
}
}
for schema in leaf.properties.values() {
if contains_reference(schema) {
return true;
}
}
for schema in leaf.pattern_properties.values() {
if contains_reference(schema) {
return true;
}
}
if let Some(schema) = &leaf.additional {
if contains_reference(schema) {
return true;
}
}
leaf.violations.iter().any(|violation| match violation {
ObjectViolation::NameFails(schema) => contains_reference(schema),
ObjectViolation::UndeclaredValueFails { additional, .. } => {
contains_reference(additional)
}
})
}
SchemaKind::MultiType(_)
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Raw(_) => false,
}
}
fn has_uncheckable_string_facet(schema: &Schema, ctx: &CanonicalizationContext) -> bool {
match schema.kind() {
SchemaKind::String(leaf) => {
leaf.get()
.formats
.iter()
.chain(leaf.get().excluded_formats.iter())
.any(|format| crate::keywords::format::is_valid(ctx.draft(), format, "").is_none())
|| leaf
.get()
.content_media_types
.iter()
.any(|media_type| !is_known_content_media_type(media_type))
|| leaf
.get()
.content_encodings
.iter()
.any(|encoding| !is_known_content_encoding(encoding))
}
SchemaKind::AnyOf(branches) => branches
.as_slice()
.iter()
.any(|branch| has_uncheckable_string_facet(branch, ctx)),
SchemaKind::AllOf(_) | SchemaKind::OneOf(_) | SchemaKind::Not(_) => {
unreachable!("a symbolic branch never reaches the facet scan")
}
SchemaKind::Object(leaf) => {
leaf.get()
.property_names
.iter()
.chain(leaf.get().properties.values())
.chain(leaf.get().pattern_properties.values())
.chain(leaf.get().additional.iter())
.any(|nested| has_uncheckable_string_facet(nested, ctx))
|| leaf
.get()
.violations
.iter()
.any(|violation| match violation {
ObjectViolation::NameFails(schema) => {
has_uncheckable_string_facet(schema, ctx)
}
ObjectViolation::UndeclaredValueFails { additional, .. } => {
has_uncheckable_string_facet(additional, ctx)
}
})
}
SchemaKind::Array(leaf) => leaf
.get()
.prefix
.iter()
.chain(leaf.get().items.iter())
.chain(leaf.get().contains.iter().map(|facet| &facet.schema))
.any(|nested| has_uncheckable_string_facet(nested, ctx)),
SchemaKind::TypedGroup { .. }
| SchemaKind::MultiType(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::Reference(_)
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Raw(_) => false,
}
}
fn is_known_content_media_type(media_type: &str) -> bool {
crate::content_media_type::DEFAULT_CONTENT_MEDIA_TYPE_CHECKS.contains_key(media_type)
}
fn is_known_content_encoding(encoding: &str) -> bool {
crate::content_encoding::DEFAULT_CONTENT_ENCODING_CHECKS_AND_CONVERTERS.contains_key(encoding)
}
fn intersect_object_leaves(
first: ObjectLeaf,
second: ObjectLeaf,
ctx: &CanonicalizationContext,
) -> ObjectLeaf {
let properties = intersect_property_entries(&first, &second, ctx);
let pattern_properties = intersect_pattern_entries(&first, &second, ctx);
if !spells_shielded_meet(&first, &second, &properties, ctx) {
ctx.record_unspellable_meet();
}
let mut required = first.required;
required.extend(second.required);
required.sort();
required.dedup();
let property_names = match (first.property_names, second.property_names) {
(Some(left), Some(right)) => Some(intersect(left, right, ctx)),
(names, None) | (None, names) => names,
};
let additional = match (first.additional, second.additional) {
(Some(left), Some(right)) => Some(intersect(left, right, ctx)),
(shield, None) | (None, shield) => shield,
};
let mut violations = first.violations;
violations.extend(second.violations);
violations.sort();
violations.dedup();
ObjectLeaf {
sizes: first.sizes.intersect(second.sizes),
required,
property_names,
properties,
pattern_properties,
additional,
violations,
}
}
fn governing_shield<'leaf>(
leaf: &'leaf ObjectLeaf,
key: &str,
ctx: &CanonicalizationContext,
) -> Option<&'leaf Schema> {
let shield = leaf.additional.as_ref()?;
if leaf.properties.contains_key(key) {
return None;
}
(!leaf
.pattern_properties
.keys()
.any(|pattern| matches_key(pattern, key, ctx)))
.then_some(shield)
}
fn intersect_property_entries(
first: &ObjectLeaf,
second: &ObjectLeaf,
ctx: &CanonicalizationContext,
) -> BTreeMap<Arc<str>, Schema> {
let mut keys: Vec<&Arc<str>> = first
.properties
.keys()
.chain(second.properties.keys())
.collect();
keys.sort();
keys.dedup();
let mut entries = BTreeMap::new();
for key in keys {
let mut entry = Schema::new(SchemaKind::True);
for applicable in [
first.properties.get(key),
governing_shield(first, key, ctx),
second.properties.get(key),
governing_shield(second, key, ctx),
]
.into_iter()
.flatten()
{
entry = intersect(entry, applicable.clone(), ctx);
}
entries.insert(Arc::clone(key), entry);
}
entries
}
fn intersect_pattern_entries(
first: &ObjectLeaf,
second: &ObjectLeaf,
ctx: &CanonicalizationContext,
) -> BTreeMap<Arc<str>, Schema> {
let mut entries = first.pattern_properties.clone();
for (pattern, schema) in &second.pattern_properties {
let entry = match entries.remove(pattern) {
Some(existing) => intersect(existing, schema.clone(), ctx),
None => schema.clone(),
};
entries.insert(Arc::clone(pattern), entry);
}
let shield = match (
first.pattern_properties.is_empty(),
second.pattern_properties.is_empty(),
) {
(true, false) => first.additional.as_ref(),
(false, true) => second.additional.as_ref(),
(true, true) | (false, false) => None,
};
if let Some(shield) = shield {
for entry in entries.values_mut() {
*entry = intersect(entry.clone(), shield.clone(), ctx);
}
}
entries
}
fn spells_shielded_meet(
first: &ObjectLeaf,
second: &ObjectLeaf,
properties: &BTreeMap<Arc<str>, Schema>,
ctx: &CanonicalizationContext,
) -> bool {
if !first.pattern_properties.is_empty() && !second.pattern_properties.is_empty() {
let same_keys = first
.pattern_properties
.keys()
.eq(second.pattern_properties.keys());
return same_keys || (first.additional.is_none() && second.additional.is_none());
}
shield_spares_named_keys(first, second, properties, ctx)
&& shield_spares_named_keys(second, first, properties, ctx)
}
fn shield_spares_named_keys(
shielded: &ObjectLeaf,
patterned: &ObjectLeaf,
properties: &BTreeMap<Arc<str>, Schema>,
ctx: &CanonicalizationContext,
) -> bool {
let Some(shield) = &shielded.additional else {
return true;
};
if patterned.pattern_properties.is_empty() {
return true;
}
shielded.properties.keys().all(|key| {
!patterned
.pattern_properties
.keys()
.any(|pattern| matches_key(pattern, key, ctx))
|| properties
.get(key)
.is_some_and(|entry| oracle::covers(entry, shield, ctx) == Verdict::Admits)
})
}
fn restrict_object_member(
leaf: &ObjectLeaf,
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> MemberRestriction {
let Value::Object(map) = member.as_value() else {
return MemberRestriction::Empty;
};
if !leaf
.sizes
.contains(&BoundCardinality::from(map.len() as u64))
|| !leaf.required.iter().all(|key| map.contains_key(&**key))
{
return MemberRestriction::Empty;
}
let mut restricted_property_names = None;
if let Some(names) = &leaf.property_names {
for key in map.keys() {
match admits_key(names, key, ctx) {
Verdict::Admits => {}
Verdict::Rejects => return MemberRestriction::Empty,
Verdict::Unknown => restricted_property_names = Some(names.clone()),
}
}
}
let mut restricted_violations = Vec::new();
for violation in &leaf.violations {
match violation {
ObjectViolation::NameFails(violated) => {
let mut satisfied = Verdict::Rejects;
for key in map.keys() {
match admits_key(violated, key, ctx) {
Verdict::Rejects => {
satisfied = Verdict::Admits;
break;
}
Verdict::Unknown => satisfied = Verdict::Unknown,
Verdict::Admits => {}
}
}
match satisfied {
Verdict::Admits => {}
Verdict::Rejects => return MemberRestriction::Empty,
Verdict::Unknown => {
restricted_violations.push(ObjectViolation::NameFails(violated.clone()));
}
}
}
ObjectViolation::UndeclaredValueFails {
names,
patterns,
additional,
} => {
let mut satisfied = Verdict::Rejects;
for (key, value) in map {
if names.iter().any(|name| name.as_ref() == key.as_str())
|| patterns
.iter()
.any(|pattern| matches_key(pattern, key, ctx))
{
continue;
}
match admits_value(additional, value, UncheckableFacet::Undecided, ctx) {
Verdict::Rejects => {
satisfied = Verdict::Admits;
break;
}
Verdict::Unknown => satisfied = Verdict::Unknown,
Verdict::Admits => {}
}
}
match satisfied {
Verdict::Admits => {}
Verdict::Rejects => return MemberRestriction::Empty,
Verdict::Unknown => {
restricted_violations.push(ObjectViolation::UndeclaredValueFails {
names: names.clone(),
patterns: patterns.clone(),
additional: additional.clone(),
});
}
}
}
}
}
let mut full = restricted_property_names.is_none() && restricted_violations.is_empty();
let mut restricted: BTreeMap<Arc<str>, Schema> = BTreeMap::new();
for (key, value) in map {
let pin = Schema::new(SchemaKind::Const(CanonicalJson::from_value(value)));
let applicable = key_schema(leaf, key, ctx);
let entry = if matches!(applicable.kind(), SchemaKind::True) {
pin
} else {
let entry = intersect(applicable, pin.clone(), ctx);
if matches!(entry.kind(), SchemaKind::False) {
return MemberRestriction::Empty;
}
if entry != pin {
full = false;
}
entry
};
restricted.insert(Arc::from(key.as_str()), entry);
}
if full {
return MemberRestriction::Full;
}
MemberRestriction::Partial(object_leaf(
ObjectLeaf {
sizes: LengthBounds {
minimum: None,
maximum: Some(BoundCardinality::from(map.len() as u64)),
},
required: restricted.keys().cloned().collect(),
property_names: restricted_property_names,
properties: restricted,
pattern_properties: BTreeMap::new(),
additional: None,
violations: restricted_violations,
},
ctx,
))
}
fn object_leaf_admits(
leaf: &ObjectLeaf,
map: &serde_json::Map<String, Value>,
ctx: &CanonicalizationContext,
) -> Verdict {
if !leaf
.sizes
.contains(&BoundCardinality::from(map.len() as u64))
|| !leaf.required.iter().all(|key| map.contains_key(&**key))
{
return Verdict::Rejects;
}
let keys = match &leaf.property_names {
Some(names) => Verdict::all(map.keys().map(|key| admits_key(names, key, ctx))),
None => Verdict::Admits,
};
if keys == Verdict::Rejects {
return Verdict::Rejects;
}
let violations = Verdict::all(leaf.violations.iter().map(|violation| match violation {
ObjectViolation::NameFails(violated) => {
let mut satisfied = Verdict::Rejects;
for key in map.keys() {
match admits_key(violated, key, ctx) {
Verdict::Rejects => return Verdict::Admits,
Verdict::Unknown => satisfied = Verdict::Unknown,
Verdict::Admits => {}
}
}
satisfied
}
ObjectViolation::UndeclaredValueFails {
names,
patterns,
additional,
} => {
let mut satisfied = Verdict::Rejects;
for (key, value) in map {
if names.iter().any(|name| name.as_ref() == key.as_str())
|| patterns
.iter()
.any(|pattern| matches_key(pattern, key, ctx))
{
continue;
}
match admits_value(additional, value, UncheckableFacet::Undecided, ctx) {
Verdict::Rejects => return Verdict::Admits,
Verdict::Unknown => satisfied = Verdict::Unknown,
Verdict::Admits => {}
}
}
satisfied
}
}));
if violations == Verdict::Rejects {
return Verdict::Rejects;
}
let values = Verdict::all(map.iter().map(|(key, value)| {
let named = match (
leaf.properties.get(key.as_str()),
governing_shield(leaf, key, ctx),
) {
(Some(schema), _) => admits_value(schema, value, UncheckableFacet::Undecided, ctx),
(None, Some(shield)) => admits_value(shield, value, UncheckableFacet::Undecided, ctx),
(None, None) => Verdict::Admits,
};
if named == Verdict::Rejects {
return Verdict::Rejects;
}
named.and(Verdict::all(leaf.pattern_properties.iter().map(
|(pattern, schema)| {
if matches_key(pattern, key, ctx) {
admits_value(schema, value, UncheckableFacet::Undecided, ctx)
} else {
Verdict::Admits
}
},
)))
}));
keys.and(values).and(violations)
}
fn intersect_number_leaves(first: NumberLeaf, second: NumberLeaf) -> NumberLeaf {
NumberLeaf {
minimum: tightest(first.minimum, second.minimum, Side::Lower),
maximum: tightest(first.maximum, second.maximum, Side::Upper),
multiple_of: first.multiple_of.intersect(second.multiple_of),
not_multiple_of: first.not_multiple_of.intersect(second.not_multiple_of),
excludes_integers: first.excludes_integers || second.excludes_integers,
}
}
fn whole_divisor() -> BoundRational {
BoundRational::new(&serde_json::Number::from(1)).expect("one is a representable divisor")
}
fn snap_to_progression(leaf: NumberLeaf) -> NumberLeaf {
let Some(step) = leaf.multiple_of.sole() else {
return leaf;
};
let snap = |bound: Option<BoundNumber>, direction: Round| match bound {
Some(bound) => step.multiple_beyond(&bound, direction).or(Some(bound)),
None => None,
};
NumberLeaf {
minimum: snap(leaf.minimum, Round::Up),
maximum: snap(leaf.maximum, Round::Down),
multiple_of: leaf.multiple_of,
not_multiple_of: leaf.not_multiple_of,
excludes_integers: leaf.excludes_integers,
}
}
fn tightest(
first: Option<BoundNumber>,
second: Option<BoundNumber>,
side: Side,
) -> Option<BoundNumber> {
tighter(first, second, |left, right| {
if left.is_tighter_than(&right, side) {
left
} else {
right
}
})
}
fn integer_within(leaf: &NumberLeaf, ctx: &CanonicalizationContext) -> Schema {
if leaf.excludes_integers {
return Schema::new(SchemaKind::False);
}
let bounds = integer_bounds_within(leaf)
.expect("a number leaf admitting integers holds ends the integer bounds can spell");
integer_leaf(
IntegerLeaf {
bounds,
multiple_of: leaf.multiple_of.clone(),
not_multiple_of: leaf.not_multiple_of.clone(),
},
ctx,
)
}
pub(crate) fn integer_bounds_within(leaf: &NumberLeaf) -> Option<IntegerBounds> {
let step = |bound: &BoundNumber,
direction: Round,
inward: &dyn Fn(BoundInteger) -> Option<BoundInteger>| {
let limit = bound.to_number();
let rounded = BoundInteger::round_from_number(&limit, direction)?;
if bound.is_inclusive() || BoundInteger::from_number(&limit).is_none() {
Some(rounded)
} else {
inward(rounded)
}
};
let minimum = match &leaf.minimum {
Some(bound) => Some(step(bound, Round::Up, &|value: BoundInteger| {
value.checked_increment()
})?),
None => None,
};
let maximum = match &leaf.maximum {
Some(bound) => Some(step(bound, Round::Down, &BoundInteger::checked_decrement)?),
None => None,
};
Some(IntegerBounds { minimum, maximum })
}
fn number_leaf_covers_integer_leaf(interval: &NumberLeaf, window: &IntegerLeaf) -> bool {
if interval.excludes_integers {
return false;
}
let Some(reach) = integer_bounds_within(interval) else {
return false;
};
reach.covers(&window.bounds)
&& interval
.multiple_of
.clone()
.over_integers()
.divide_all(&window.multiple_of)
&& interval
.not_multiple_of
.bars_no_more_than(&window.not_multiple_of)
}
fn number_leaf_admits(leaf: &NumberLeaf, member: &CanonicalJson) -> bool {
let Value::Number(number) = member.as_value() else {
return false;
};
leaf.minimum
.as_ref()
.is_none_or(|min| min.admits(number, Side::Lower))
&& leaf
.maximum
.as_ref()
.is_none_or(|max| max.admits(number, Side::Upper))
&& leaf.multiple_of.divide(number)
&& !leaf.not_multiple_of.bars(number)
&& !(leaf.excludes_integers && jsonschema_value::types::number_is_integer(number))
}
fn restrict_number_member(
leaf: &NumberLeaf,
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> MemberRestriction {
if number_leaf_admits(leaf, member) {
return MemberRestriction::Full;
}
let Value::Number(number) = member.as_value() else {
return MemberRestriction::Empty;
};
if !(leaf.excludes_integers && jsonschema_value::types::number_is_integer(number)) {
return MemberRestriction::Empty;
}
let point = BoundNumber::new(number, true);
let window = intersect_number_leaves(
NumberLeaf {
minimum: Some(point.clone()),
maximum: Some(point),
multiple_of: Divisors::default(),
not_multiple_of: ExcludedDivisors::default(),
excludes_integers: false,
},
leaf.clone(),
);
debug_assert!(
matches!(ctx.draft(), Draft::Draft4),
"the integer exclusion survives normalization only under Draft 4"
);
let restricted = number_leaf(window, ctx);
if matches!(restricted.kind(), SchemaKind::False) {
MemberRestriction::Empty
} else {
MemberRestriction::Partial(restricted)
}
}
pub(crate) fn integer_leaf(leaf: IntegerLeaf, ctx: &CanonicalizationContext) -> Schema {
let leaf = IntegerLeaf {
multiple_of: leaf.multiple_of.over_integers(),
..leaf
};
if leaf.bounds.minimum.is_none()
&& leaf.bounds.maximum.is_none()
&& leaf.multiple_of.is_empty()
&& leaf.not_multiple_of.is_empty()
{
return type_set_schema(JsonTypeSet::from(JsonType::Integer));
}
let Some(leaf) = snap_to_multiples(leaf).and_then(NonEmpty::new) else {
return Schema::new(SchemaKind::False);
};
if let (Some(min), Some(max)) = (&leaf.get().bounds.minimum, &leaf.get().bounds.maximum) {
if min == max {
let point = min.to_number();
if !leaf.get().multiple_of.divide(&point) || leaf.get().not_multiple_of.bars(&point) {
return Schema::new(SchemaKind::False);
}
let value = Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Number(point),
)));
return if matches!(ctx.draft(), Draft::Draft4) {
typed_group(JsonType::Integer, value)
} else {
value
};
}
}
Schema::new(SchemaKind::Integer(leaf))
}
fn snap_to_multiples(leaf: IntegerLeaf) -> Option<IntegerLeaf> {
let Some(step) = leaf
.multiple_of
.sole()
.and_then(BoundRational::exact_integer)
else {
return Some(leaf);
};
let minimum = leaf
.bounds
.minimum
.as_ref()
.map(|min| step.multiple_beyond(min, Round::Up).unwrap_or(min.clone()));
let maximum = leaf.bounds.maximum.as_ref().map(|max| {
step.multiple_beyond(max, Round::Down)
.unwrap_or(max.clone())
});
Some(IntegerLeaf {
bounds: IntegerBounds { minimum, maximum },
multiple_of: leaf.multiple_of,
not_multiple_of: leaf.not_multiple_of,
})
}
fn integer_leaf_admits(leaf: &IntegerLeaf, member: &CanonicalJson) -> bool {
let Value::Number(number) = member.as_value() else {
return false;
};
if leaf.not_multiple_of.bars(number) {
return false;
}
match BoundInteger::from_number(number) {
Some(value) => leaf.bounds.contains(&value) && leaf.multiple_of.divide(number),
None => admits_out_of_range(&leaf.bounds, number) && leaf.multiple_of.divide(number),
}
}
#[cfg(not(feature = "arbitrary-precision"))]
fn admits_out_of_range(bounds: &IntegerBounds, number: &serde_json::Number) -> bool {
if !jsonschema_value::types::number_is_integer(number) {
return false;
}
if number.as_f64().is_some_and(|float| float > 0.0) {
bounds.maximum.is_none()
} else {
bounds.minimum.is_none()
}
}
#[cfg(feature = "arbitrary-precision")]
fn admits_out_of_range(_bounds: &IntegerBounds, _number: &serde_json::Number) -> bool {
false
}
fn intersect_string_leaves(first: StringLeaf, second: StringLeaf) -> StringLeaf {
let mut patterns = first.patterns;
patterns.extend(second.patterns);
patterns.sort();
patterns.dedup();
let mut excluded_patterns = first.excluded_patterns;
excluded_patterns.extend(second.excluded_patterns);
excluded_patterns.sort();
excluded_patterns.dedup();
let mut formats = first.formats;
formats.extend(second.formats);
formats.sort();
formats.dedup();
let mut excluded_formats = first.excluded_formats;
excluded_formats.extend(second.excluded_formats);
excluded_formats.sort();
excluded_formats.dedup();
let mut content_media_types = first.content_media_types;
content_media_types.extend(second.content_media_types);
content_media_types.sort();
content_media_types.dedup();
let mut content_encodings = first.content_encodings;
content_encodings.extend(second.content_encodings);
content_encodings.sort();
content_encodings.dedup();
let mut excluded = first.excluded;
excluded.extend(second.excluded);
excluded.sort();
excluded.dedup();
StringLeaf {
lengths: first.lengths.intersect(second.lengths),
patterns,
excluded_patterns,
formats,
excluded_formats,
content_media_types,
content_encodings,
excluded,
}
}
fn formats_conflict(leaf: &StringLeaf, ctx: &CanonicalizationContext) -> bool {
if leaf
.excluded_formats
.iter()
.any(|format| leaf.formats.contains(format))
{
return true;
}
let mut window = leaf.lengths.clone();
for format in &leaf.formats {
let Some((minimum, maximum)) = crate::keywords::format::length_window(ctx.draft(), format)
else {
continue;
};
window = window.intersect(LengthBounds {
minimum: Some(BoundCardinality::from(minimum)),
maximum: Some(BoundCardinality::from(maximum)),
});
}
window.is_empty()
}
fn patterns_conflict(leaf: &StringLeaf) -> bool {
leaf.excluded_patterns
.iter()
.any(|pattern| leaf.patterns.contains(pattern))
}
struct StringMatchers {
required: Vec<Arc<CompiledMatcher>>,
barred: Vec<Arc<CompiledMatcher>>,
}
impl StringMatchers {
fn compile(leaf: &StringLeaf, ctx: &CanonicalizationContext) -> Self {
let compile_all = |patterns: &[Arc<str>]| {
patterns
.iter()
.map(|pattern| {
ctx.compile_regex(pattern)
.expect("pattern validated during parsing")
})
.collect()
};
Self {
required: compile_all(&leaf.patterns),
barred: compile_all(&leaf.excluded_patterns),
}
}
}
fn string_leaf_admits(
leaf: &StringLeaf,
matchers: &StringMatchers,
member: &CanonicalJson,
uncheckable: UncheckableFacet,
ctx: &CanonicalizationContext,
) -> Verdict {
let Value::String(text) = member.as_value() else {
return Verdict::Rejects;
};
string_leaf_admits_text(leaf, matchers, text, uncheckable, ctx)
}
fn string_leaf_admits_text(
leaf: &StringLeaf,
matchers: &StringMatchers,
text: &str,
uncheckable: UncheckableFacet,
ctx: &CanonicalizationContext,
) -> Verdict {
let length = BoundCardinality::from(bytecount::num_chars(text.as_bytes()) as u64);
if !leaf.lengths.contains(&length)
|| !matchers.required.iter().all(|regex| regex.is_match(text))
|| matchers.barred.iter().any(|regex| regex.is_match(text))
|| leaf.excluded.iter().any(|value| value.as_ref() == text)
{
return Verdict::Rejects;
}
let demanded = |checked: Option<bool>| match (checked, uncheckable) {
(Some(admitted), _) => Verdict::from_bool(admitted),
(None, UncheckableFacet::Skipped) => Verdict::Admits,
(None, UncheckableFacet::Undecided) => Verdict::Unknown,
};
let barred = |checked: Option<bool>| match (checked, uncheckable) {
(Some(admitted), _) => Verdict::from_bool(!admitted),
(None, UncheckableFacet::Skipped) => Verdict::Rejects,
(None, UncheckableFacet::Undecided) => Verdict::Unknown,
};
Verdict::all(
leaf.formats
.iter()
.map(|format| demanded(crate::keywords::format::is_valid(ctx.draft(), format, text)))
.chain(
leaf.excluded_formats.iter().map(|format| {
barred(crate::keywords::format::is_valid(ctx.draft(), format, text))
}),
)
.chain(leaf.content_media_types.iter().map(|media_type| {
demanded(
crate::content_media_type::DEFAULT_CONTENT_MEDIA_TYPE_CHECKS
.get(media_type.as_ref())
.map(|check| check(text)),
)
}))
.chain(leaf.content_encodings.iter().map(|encoding| {
demanded(
crate::content_encoding::DEFAULT_CONTENT_ENCODING_CHECKS_AND_CONVERTERS
.get(encoding.as_ref())
.map(|(check, _)| check(text)),
)
})),
)
}