use std::sync::Arc;
use referencing::Draft;
use serde_json::Value;
use crate::{
canonical::{
context::{CanonicalizationContext, CompiledMatcher},
ir::{
BoundCardinality, BoundInteger, CanonicalJson, IntegerBounds, Schema, SchemaKind,
StringLeaf,
},
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::String(_))
| (SchemaKind::String(_), SchemaKind::TypedGroup { .. } | SchemaKind::Integer(_)) => {
Schema::new(SchemaKind::False)
}
(SchemaKind::True, right) => Schema::new(right),
(left, SchemaKind::True) => Schema::new(left),
(SchemaKind::AnyOf(branches), other) | (other, SchemaKind::AnyOf(branches)) => {
distribute(branches, Schema::new(other), 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 {
parse::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)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::String(first), SchemaKind::String(second)) => {
string_leaf(intersect_string_leaves(first, second))
}
(SchemaKind::MultiType(set), SchemaKind::Integer(bounds))
| (SchemaKind::Integer(bounds), SchemaKind::MultiType(set)) => {
if SchemaKind::semantic_cover(set).contains(JsonType::Integer) {
integer_leaf(bounds, ctx)
} else {
Schema::new(SchemaKind::False)
}
}
(SchemaKind::Integer(first), SchemaKind::Integer(second)) => {
integer_leaf(first.intersect(second), ctx)
}
(SchemaKind::TypedGroup { ty, body }, SchemaKind::Integer(bounds))
| (SchemaKind::Integer(bounds), SchemaKind::TypedGroup { ty, body }) => {
let kept = into_members(body.into_kind())
.into_iter()
.filter(|member| integer_leaf_admits(&bounds, member))
.collect();
typed_group(ty, parse::canonicalize_value_set(kept))
}
(SchemaKind::Raw(_), _) | (_, SchemaKind::Raw(_)) => {
unreachable!("`Raw` is whole-document; combinators never contain it")
}
}
}
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: Vec<StringLeaf> = Vec::new();
let mut integers: Vec<IntegerBounds> = 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((_, pool)) => pool.extend(values),
None => groups.push((ty, values)),
}
}
SchemaKind::String(leaf) => strings.push(leaf),
SchemaKind::Integer(bounds) => integers.push(bounds),
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();
}
let value_set = parse::canonicalize_value_set(members);
if let SchemaKind::MultiType(saturated) = value_set.kind() {
let mut rest: Vec<Schema> = vec![Schema::new(SchemaKind::MultiType(union_type_sets(
types, *saturated,
)))];
rest.extend(
groups
.into_iter()
.map(|(ty, pool)| typed_group(ty, parse::canonicalize_value_set(pool))),
);
rest.extend(
strings
.into_iter()
.map(|leaf| Schema::new(SchemaKind::String(leaf))),
);
rest.extend(
integers
.into_iter()
.map(|bounds| Schema::new(SchemaKind::Integer(bounds))),
);
return union(rest, ctx);
}
let mut out: Vec<Schema> = Vec::new();
if !types.is_empty() {
out.push(parse::type_set_schema(types));
}
for (ty, pool) in groups {
let body = parse::canonicalize_value_set(pool);
if body.kind().finite_values().is_some() && !value_set_admits_group(&value_set, &body) {
out.push(typed_group(ty, body));
}
}
for leaf in strings {
out.push(Schema::new(SchemaKind::String(leaf)));
}
for bounds in integers {
out.push(Schema::new(SchemaKind::Integer(bounds)));
}
if !matches!(value_set.kind(), SchemaKind::False) {
out.push(value_set);
}
out.sort();
out.dedup();
match out.len() {
0 => Schema::new(SchemaKind::False),
1 => out.into_iter().next().expect("len == 1"),
_ => Schema::new(SchemaKind::AnyOf(out)),
}
}
fn distribute(mut branches: Vec<Schema>, other: Schema, ctx: &CanonicalizationContext) -> Schema {
let last = branches.pop().expect("AnyOf carries at least two branches");
let mut out: Vec<Schema> = branches
.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,
other => 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);
parse::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
.patterns
.iter()
.map(|pattern| {
ctx.compile_regex(pattern)
.expect("pattern validated during parsing")
})
.collect();
let kept = members
.into_iter()
.filter(|member| string_leaf_admits(&leaf, ®exes, member))
.collect();
parse::canonicalize_value_set(kept)
}
SchemaKind::Integer(bounds) => {
let kept = members
.into_iter()
.filter(|member| integer_leaf_admits(&bounds, member))
.collect();
let value_set = parse::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, parse::canonicalize_value_set(kept))
}
other => unreachable!("dispatch handles the remaining kinds: {other:?}"),
}
}
fn typed_group(ty: JsonType, body: Schema) -> Schema {
if matches!(body.kind(), SchemaKind::False) {
Schema::new(SchemaKind::False)
} else {
Schema::new(SchemaKind::TypedGroup { ty, body })
}
}
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())
}
fn union_type_sets(left: JsonTypeSet, right: JsonTypeSet) -> JsonTypeSet {
SchemaKind::canonical_type_set(left.union(right))
}
pub(crate) fn string_leaf(leaf: StringLeaf) -> Schema {
if leaf.lengths.is_empty() {
return Schema::new(SchemaKind::False);
}
Schema::new(SchemaKind::String(leaf))
}
pub(crate) fn integer_leaf(bounds: IntegerBounds, ctx: &CanonicalizationContext) -> Schema {
if bounds.is_empty() {
return Schema::new(SchemaKind::False);
}
if let (Some(min), Some(max)) = (&bounds.minimum, &bounds.maximum) {
if min == max {
let value = Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Number(min.to_number()),
)));
return if matches!(ctx.draft(), Draft::Draft4) {
typed_group(JsonType::Integer, value)
} else {
value
};
}
}
Schema::new(SchemaKind::Integer(bounds))
}
fn integer_leaf_admits(bounds: &IntegerBounds, member: &CanonicalJson) -> bool {
let Value::Number(number) = member.as_value() else {
return false;
};
match BoundInteger::from_number(number) {
Some(value) => bounds.contains(&value),
None => admits_out_of_range(bounds, 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();
StringLeaf {
lengths: first.lengths.intersect(second.lengths),
patterns,
}
}
fn string_leaf_admits(
leaf: &StringLeaf,
regexes: &[Arc<CompiledMatcher>],
member: &CanonicalJson,
) -> bool {
let Value::String(text) = member.as_value() else {
return false;
};
let length = BoundCardinality::from(bytecount::num_chars(text.as_bytes()) as u64);
leaf.lengths.contains(&length) && regexes.iter().all(|regex| regex.is_match(text))
}