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, Divisors, IntegerBounds, IntegerLeaf, IntegerLeaves,
LengthBounds, NonEmpty, NumberLeaf, NumberLeaves, ObjectLeaf, ObjectLeaves, Round,
Schema, SchemaKind, Side, StringLeaf, StringLeaves, Verdict,
},
negate, parse,
},
JsonType, JsonTypeSet,
};
pub(crate) fn intersect(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)) => {
array_leaf(
intersect_array_leaves(first.into_inner(), second.into_inner(), ctx),
ctx,
)
}
(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
}
pub(crate) fn one_of(symbolic_branch: Schema, mut branches: Vec<Schema>) -> Schema {
debug_assert!(
contains_reference(&symbolic_branch),
"oneOf receives a symbolic reference branch"
);
branches.retain(|branch| !matches!(branch.kind(), SchemaKind::False));
if branches.is_empty() {
return symbolic_branch;
}
branches.push(symbolic_branch);
branches.sort();
debug_assert!(
branches.windows(2).all(|pair| pair[0] <= pair[1]),
"oneOf branches are sorted without deduplication"
);
Schema::new(SchemaKind::OneOf(branches))
}
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();
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();
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.formats.is_empty()
&& leaf.content_media_types.is_empty()
&& leaf.content_encodings.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 !members.is_empty()
&& (!strings.is_empty()
|| !integers.is_empty()
|| !numbers.is_empty()
|| !arrays.is_empty()
|| !objects.is_empty())
{
let compiled: Vec<(&StringLeaf, Vec<Arc<CompiledMatcher>>)> = strings
.as_slice()
.iter()
.map(|leaf| {
let regexes = leaf
.patterns
.iter()
.map(|pattern| {
ctx.compile_regex(pattern)
.expect("pattern validated during parsing")
})
.collect();
(leaf, regexes)
})
.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 top_level: ahash::AHashSet<Schema> = out.iter().cloned().collect();
out.retain(|branch| {
let SchemaKind::AllOf(conjuncts) = branch.kind() else {
return true;
};
!conjuncts
.as_slice()
.iter()
.any(|conjunct| top_level.contains(conjunct))
});
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)),
},
unique: false,
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(),
});
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(),
formats: Vec::new(),
content_media_types: Vec::new(),
content_encodings: 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(),
});
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(),
};
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
{
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,
})
}
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,
};
if !split_piece_is_covered(piece.clone(), leaves, &keys, ctx) {
return false;
}
leaves.clear();
leaves.push(piece);
true
}
fn split_piece_is_covered(
piece: ObjectLeaf,
leaves: &[ObjectLeaf],
keys: &[Arc<str>],
ctx: &CanonicalizationContext,
) -> bool {
let schema = object_leaf(piece.clone(), ctx);
if matches!(schema.kind(), SchemaKind::False) {
return true;
}
if leaves
.iter()
.any(|leaf| intersect(schema.clone(), object_leaf(leaf.clone(), ctx), ctx) == schema)
{
return true;
}
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 widen_size_window_covered_by_siblings(
leaves: &mut [ObjectLeaf],
ctx: &CanonicalizationContext,
) -> bool {
for index in 0..leaves.len() {
let Some(rays) = negate::length_windows(&leaves[index].sizes) else {
continue;
};
if rays.is_empty() {
continue;
}
let siblings: Vec<ObjectLeaf> = leaves
.iter()
.enumerate()
.filter(|(sibling, _)| *sibling != index)
.map(|(_, leaf)| leaf.clone())
.collect();
let mut keys: Vec<Arc<str>> = siblings
.iter()
.flat_map(|leaf| leaf.required.iter().chain(leaf.properties.keys()).cloned())
.collect();
keys.sort();
keys.dedup();
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 {
for index in 0..leaves.len() {
let siblings: Vec<ObjectLeaf> = leaves
.iter()
.enumerate()
.filter(|(sibling, _)| *sibling != index)
.map(|(_, leaf)| leaf.clone())
.collect();
let mut keys: Vec<Arc<str>> = siblings
.iter()
.flat_map(|leaf| leaf.required.iter().chain(leaf.properties.keys()).cloned())
.collect();
keys.sort();
keys.dedup();
if split_piece_is_covered(leaves[index].clone(), &siblings, &keys, ctx) {
leaves.remove(index);
return true;
}
for divider in 0..siblings.len() {
let Some(mut windows) = negate::length_windows(&siblings[divider].sizes) else {
continue;
};
if windows.is_empty() {
continue;
}
windows.push(siblings[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);
}
}
if widened == leaves[index] {
continue;
}
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 regexes: Vec<_> = leaf
.get()
.patterns
.iter()
.map(|pattern| {
ctx.compile_regex(pattern)
.expect("pattern validated during parsing")
})
.collect();
let kept = members
.into_iter()
.filter(|member| {
!matches!(
string_leaf_admits(leaf.get(), ®exes, member, 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 kept = members
.into_iter()
.filter(|member| number_leaf_admits(leaf.get(), member))
.collect();
canonicalize_value_set(kept)
}
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, Vec<Arc<CompiledMatcher>>)],
integers: &[IntegerLeaf],
numbers: &[NumberLeaf],
arrays: &[ArrayLeaf],
objects: &[ObjectLeaf],
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> bool {
match member.as_value() {
Value::Array(_) => arrays
.iter()
.any(|leaf| matches!(array_leaf_admits(leaf, member, ctx), Verdict::Admits)),
Value::Object(_) => objects
.iter()
.any(|leaf| matches!(object_leaf_admits(leaf, member, ctx), Verdict::Admits)),
Value::String(_) => strings.iter().any(|(leaf, regexes)| {
matches!(
string_leaf_admits(leaf, regexes, member, 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(leaf: StringLeaf, ctx: &CanonicalizationContext) -> Schema {
if formats_conflict(&leaf, ctx) {
return Schema::new(SchemaKind::False);
}
let Some(leaf) = NonEmpty::new(leaf) else {
return Schema::new(SchemaKind::False);
};
if leaf.get().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 intersect_integer_leaves(first: IntegerLeaf, second: IntegerLeaf) -> IntegerLeaf {
IntegerLeaf {
bounds: first.bounds.intersect(second.bounds),
multiple_of: first.multiple_of.intersect(second.multiple_of),
}
}
pub(crate) fn number_leaf(leaf: NumberLeaf, ctx: &CanonicalizationContext) -> Schema {
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,
},
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();
return if leaf.get().multiple_of.divide(&point) {
Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Number(point),
)))
} else {
Schema::new(SchemaKind::False)
};
}
}
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) {
return Schema::new(SchemaKind::False);
}
if !reconcile_contains_positions(&leaf, ctx) {
return Schema::new(SchemaKind::False);
}
if leaf.unique {
if let Some(ceiling) = unique_length_ceiling(&leaf, ctx) {
leaf.lengths.maximum = Some(match leaf.lengths.maximum.take() {
Some(maximum) => maximum.min(ceiling),
None => ceiling,
});
}
}
if leaf
.lengths
.maximum
.as_ref()
.is_some_and(|max| *max <= BoundCardinality::from(1))
{
leaf.unique = false;
}
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) -> bool {
let Some(implied) = leaf
.contains
.iter()
.map(ContainsFacet::effective_minimum)
.max()
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;
}
true
}
fn unique_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,
) -> ArrayLeaf {
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);
ArrayLeaf {
lengths: first.lengths.intersect(second.lengths),
unique: first.unique || second.unique,
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 array_leaf_admits(
leaf: &ArrayLeaf,
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> Verdict {
let Value::Array(items) = member.as_value() else {
return Verdict::Rejects;
};
if !leaf
.lengths
.contains(&BoundCardinality::from(items.len() as u64))
{
return Verdict::Rejects;
}
if leaf.unique && has_duplicate_elements(items) {
return Verdict::Rejects;
}
contains_verdict(&leaf.contains, items, ctx).and(Verdict::all(items.iter().enumerate().map(
|(index, element)| match element_schema(leaf, index) {
Some(schema) => admits_value(schema, element, ctx),
None => Verdict::Admits,
},
)))
}
fn contains_verdict(
facets: &[ContainsFacet],
elements: &[Value],
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, 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 leaf.unique && has_duplicate_elements(elements) {
return MemberRestriction::Empty;
}
let contains_symbolic_reference = leaf
.contains
.iter()
.any(|facet| contains_reference(&facet.schema));
let (mut full, contains) = match contains_verdict(&leaf.contains, elements, ctx) {
Verdict::Rejects => return MemberRestriction::Empty,
Verdict::Unknown if contains_symbolic_reference => (false, leaf.contains.clone()),
Verdict::Admits | Verdict::Unknown => (true, Vec::new()),
};
debug_assert!(
contains.is_empty() || contains_symbolic_reference,
"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),
},
unique: false,
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);
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)
{
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 Some(shield) = leaf.additional.take() else {
return;
};
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).cloned().unwrap_or_else(|| {
leaf.additional
.clone()
.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 regexes: Vec<_> = leaf
.get()
.patterns
.iter()
.map(|pattern| {
ctx.compile_regex(pattern)
.expect("pattern validated during parsing")
})
.collect();
string_leaf_admits_text(leaf.get(), ®exes, key, 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")
}
}
}
fn admits_value(schema: &Schema, value: &Value, 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 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 {
return contains_reference(schema);
}
false
}
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()
.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())
.any(|nested| has_uncheckable_string_facet(nested, 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 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 first_shield = first.additional;
let second_shield = second.additional;
let mut properties = first.properties;
let first_named: Vec<Arc<str>> = properties.keys().cloned().collect();
let mut second_named: Vec<Arc<str>> = Vec::with_capacity(second.properties.len());
for (key, schema) in second.properties {
second_named.push(Arc::clone(&key));
let entry = match (properties.remove(&key), &first_shield) {
(Some(existing), _) => intersect(existing, schema, ctx),
(None, Some(shield)) => intersect(shield.clone(), schema, ctx),
(None, None) => schema,
};
properties.insert(key, entry);
}
if let Some(shield) = &second_shield {
for key in &first_named {
if !second_named.contains(key) {
let entry = properties
.remove(key)
.map(|entry| intersect(entry, shield.clone(), ctx));
if let Some(entry) = entry {
properties.insert(Arc::clone(key), entry);
}
}
}
}
let additional = match (first_shield, second_shield) {
(Some(left), Some(right)) => Some(intersect(left, right, ctx)),
(shield, None) | (None, shield) => shield,
};
let mut pattern_properties = first.pattern_properties;
for (pattern, schema) in second.pattern_properties {
match pattern_properties.remove(&pattern) {
Some(existing) => pattern_properties.insert(pattern, intersect(existing, schema, ctx)),
None => pattern_properties.insert(pattern, schema),
};
}
ObjectLeaf {
sizes: first.sizes.intersect(second.sizes),
required,
property_names,
properties,
pattern_properties,
additional,
}
}
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 full = restricted_property_names.is_none();
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,
},
ctx,
))
}
fn object_leaf_admits(
leaf: &ObjectLeaf,
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> Verdict {
let Value::Object(map) = member.as_value() else {
return Verdict::Rejects;
};
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 values = Verdict::all(map.iter().map(|(key, value)| {
let named = match (leaf.properties.get(key.as_str()), &leaf.additional) {
(Some(schema), _) => admits_value(schema, value, ctx),
(None, Some(shield)) => admits_value(shield, value, 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, ctx)
} else {
Verdict::Admits
}
},
)))
}));
keys.and(values)
}
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),
}
}
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,
}
}
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 {
let bounds = integer_bounds_within(leaf)
.expect("interval bounds hold representable integers, checked during parsing");
integer_leaf(
IntegerLeaf {
bounds,
multiple_of: leaf.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_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)
}
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()
{
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) {
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,
})
}
fn integer_leaf_admits(leaf: &IntegerLeaf, member: &CanonicalJson) -> bool {
let Value::Number(number) = member.as_value() else {
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 formats = first.formats;
formats.extend(second.formats);
formats.sort();
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();
StringLeaf {
lengths: first.lengths.intersect(second.lengths),
patterns,
formats,
content_media_types,
content_encodings,
}
}
fn formats_conflict(leaf: &StringLeaf, ctx: &CanonicalizationContext) -> bool {
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 string_leaf_admits(
leaf: &StringLeaf,
regexes: &[Arc<CompiledMatcher>],
member: &CanonicalJson,
ctx: &CanonicalizationContext,
) -> Verdict {
let Value::String(text) = member.as_value() else {
return Verdict::Rejects;
};
string_leaf_admits_text(leaf, regexes, text, ctx)
}
fn string_leaf_admits_text(
leaf: &StringLeaf,
regexes: &[Arc<CompiledMatcher>],
text: &str,
ctx: &CanonicalizationContext,
) -> Verdict {
let length = BoundCardinality::from(bytecount::num_chars(text.as_bytes()) as u64);
if !leaf.lengths.contains(&length) || !regexes.iter().all(|regex| regex.is_match(text)) {
return Verdict::Rejects;
}
Verdict::all(
leaf.formats
.iter()
.map(
|format| match crate::keywords::format::is_valid(ctx.draft(), format, text) {
Some(admitted) => Verdict::from_bool(admitted),
None => Verdict::Unknown,
},
)
.chain(leaf.content_media_types.iter().map(|media_type| {
match crate::content_media_type::DEFAULT_CONTENT_MEDIA_TYPE_CHECKS
.get(media_type.as_ref())
{
Some(check) => Verdict::from_bool(check(text)),
None => Verdict::Unknown,
}
}))
.chain(leaf.content_encodings.iter().map(|encoding| {
match crate::content_encoding::DEFAULT_CONTENT_ENCODING_CHECKS_AND_CONVERTERS
.get(encoding.as_ref())
{
Some((check, _)) => Verdict::from_bool(check(text)),
None => Verdict::Unknown,
}
})),
)
}