use std::{
cmp::Ordering,
collections::BTreeMap,
hash::{Hash, Hasher},
sync::Arc,
};
use serde_json::{Number, Value};
use strum::{EnumDiscriminants, IntoStaticStr};
use crate::{JsonType, JsonTypeSet};
mod array_leaves;
mod bound_cardinality;
mod bound_integer;
mod bound_number;
mod bound_rational;
mod constructors;
mod divisors;
mod integer_leaves;
mod number_leaves;
mod object_leaves;
mod raw;
mod string_leaves;
mod verdict;
pub(crate) use array_leaves::ArrayLeaves;
pub(crate) use bound_cardinality::BoundCardinality;
pub(crate) use bound_integer::{BoundInteger, Round};
pub(crate) use bound_number::{BoundNumber, Side};
pub(crate) use bound_rational::BoundRational;
pub(crate) use constructors::{canonicalize_value_set, type_set_schema, typed_group};
pub(crate) use divisors::Divisors;
pub(crate) use integer_leaves::IntegerLeaves;
pub(crate) use number_leaves::NumberLeaves;
pub(crate) use object_leaves::ObjectLeaves;
pub(crate) use raw::RawJson;
pub(crate) use string_leaves::StringLeaves;
pub(crate) use verdict::Verdict;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CanonicalJson(Arc<Value>);
impl CanonicalJson {
#[must_use]
pub(crate) fn from_value(value: &Value) -> Self {
Self(Arc::new(normalized(value)))
}
#[must_use]
pub(crate) fn as_value(&self) -> &Value {
&self.0
}
#[must_use]
pub(crate) fn to_value(&self) -> Value {
(*self.0).clone()
}
#[must_use]
pub(crate) fn json_type(&self) -> JsonType {
match self.as_value() {
Value::Null => JsonType::Null,
Value::Bool(_) => JsonType::Boolean,
Value::Number(number) => {
if jsonschema_value::types::number_is_integer(number) {
JsonType::Integer
} else {
JsonType::Number
}
}
Value::String(_) => JsonType::String,
Value::Array(_) => JsonType::Array,
Value::Object(_) => JsonType::Object,
}
}
}
impl PartialOrd for CanonicalJson {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CanonicalJson {
fn cmp(&self, other: &Self) -> Ordering {
raw::compare_values(&self.0, &other.0)
}
}
impl Hash for CanonicalJson {
fn hash<H: Hasher>(&self, state: &mut H) {
raw::hash_value(&self.0, state);
}
}
fn normalized(value: &Value) -> Value {
match value {
Value::Number(number) => Value::Number(normalized_number(number)),
Value::Array(items) => Value::Array(items.iter().map(normalized).collect()),
Value::Object(map) => Value::Object(
map.iter()
.map(|(key, item)| (key.clone(), normalized(item)))
.collect(),
),
other @ (Value::Null | Value::Bool(_) | Value::String(_)) => other.clone(),
}
}
#[cfg(not(feature = "arbitrary-precision"))]
pub(crate) fn normalized_number(number: &Number) -> Number {
use crate::canonical::json::{integer_valued_i64, integer_valued_u64};
let Some(float) = number
.as_f64()
.filter(|_| !number.is_i64() && !number.is_u64())
else {
return number.clone();
};
integer_valued_u64(float)
.map(Number::from)
.or_else(|| integer_valued_i64(float).map(Number::from))
.unwrap_or_else(|| number.clone())
}
#[cfg(feature = "arbitrary-precision")]
pub(crate) fn normalized_number(number: &Number) -> Number {
match crate::canonical::json::canonical_number(number.as_str()) {
Some(text) => text.parse().expect("canonical number text parses"),
None => number.clone(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Schema(Arc<SchemaData>);
impl Schema {
#[must_use]
pub(crate) fn new(kind: SchemaKind) -> Self {
let hash = structural_hash(&kind);
Self(Arc::new(SchemaData { kind, hash }))
}
#[inline]
#[must_use]
pub(crate) fn kind(&self) -> &SchemaKind {
&self.0.kind
}
#[must_use]
pub(crate) fn into_kind(self) -> SchemaKind {
match Arc::try_unwrap(self.0) {
Ok(data) => data.kind,
Err(shared) => shared.kind.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumDiscriminants)]
#[strum_discriminants(
name(CanonicalKind),
vis(pub),
derive(Hash, IntoStaticStr),
strum(serialize_all = "snake_case"),
doc = "Structural discriminant of a [`CanonicalSchema`](crate::CanonicalSchema), one variant per IR arm."
)]
pub(crate) enum SchemaKind {
MultiType(JsonTypeSet),
TypedGroup { ty: JsonType, body: Schema },
String(NonEmpty<StringLeaf>),
Integer(NonEmpty<IntegerLeaf>),
Number(NonEmpty<NumberLeaf>),
Array(NonEmpty<ArrayLeaf>),
Object(NonEmpty<ObjectLeaf>),
Const(CanonicalJson),
Enum(AtLeastTwo<CanonicalJson>),
Not(Schema),
AllOf(AtLeastTwo<Schema>),
AnyOf(AtLeastTwo<Schema>),
OneOf(Vec<Schema>),
Reference(Arc<str>),
True,
False,
Raw(RawJson),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(crate) struct NumberLeaf {
pub(crate) minimum: Option<BoundNumber>,
pub(crate) maximum: Option<BoundNumber>,
pub(crate) multiple_of: Divisors,
}
impl NumberLeaf {
pub(crate) fn is_vacant(&self) -> bool {
if !self
.multiple_of
.admit_between(self.minimum.as_ref(), self.maximum.as_ref())
{
return true;
}
let (Some(min), Some(max)) = (&self.minimum, &self.maximum) else {
return false;
};
!min.admits(&max.to_number(), Side::Lower) || !max.admits(&min.to_number(), Side::Upper)
}
}
impl MaybeEmpty for NumberLeaf {
fn is_empty(&self) -> bool {
self.is_vacant()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(crate) struct IntegerLeaf {
pub(crate) bounds: IntegerBounds,
pub(crate) multiple_of: Divisors,
}
impl MaybeEmpty for IntegerLeaf {
fn is_empty(&self) -> bool {
self.bounds.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(crate) struct ArrayLeaf {
pub(crate) lengths: LengthBounds,
pub(crate) unique: bool,
pub(crate) prefix: Vec<Schema>,
pub(crate) items: Option<Schema>,
pub(crate) contains: Vec<ContainsFacet>,
}
impl ArrayLeaf {
pub(crate) fn spans_domain(&self) -> bool {
self.lengths.is_unbounded()
&& !self.unique
&& self.prefix.is_empty()
&& self.items.is_none()
&& self.contains.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct ContainsFacet {
pub(crate) schema: Schema,
pub(crate) minimum: Option<BoundCardinality>,
pub(crate) maximum: Option<BoundCardinality>,
}
impl ContainsFacet {
pub(crate) fn effective_minimum(&self) -> BoundCardinality {
self.minimum
.clone()
.unwrap_or_else(|| BoundCardinality::from(1))
}
}
impl MaybeEmpty for ArrayLeaf {
fn is_empty(&self) -> bool {
self.lengths.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(crate) struct ObjectLeaf {
pub(crate) sizes: LengthBounds,
pub(crate) required: Vec<Arc<str>>,
pub(crate) property_names: Option<Schema>,
pub(crate) properties: BTreeMap<Arc<str>, Schema>,
pub(crate) pattern_properties: BTreeMap<Arc<str>, Schema>,
pub(crate) additional: Option<Schema>,
}
impl ObjectLeaf {
#[must_use]
pub(crate) fn admitted_key_count(&self) -> Option<BoundCardinality> {
let values = self.property_names.as_ref()?.kind().finite_values()?;
let present = values
.iter()
.filter(|value| {
!matches!(value.as_value(), serde_json::Value::String(key)
if self
.properties
.get(key.as_str())
.is_some_and(|child| matches!(child.kind(), SchemaKind::False)))
})
.count();
Some(BoundCardinality::from(present as u64))
}
#[must_use]
pub(crate) fn effective_sizes(&self) -> LengthBounds {
LengthBounds {
minimum: self.sizes.minimum.clone(),
maximum: tighter(
self.sizes.maximum.clone(),
self.admitted_key_count(),
Ord::min,
),
}
}
pub(crate) fn spans_domain(&self) -> bool {
self.sizes.is_unbounded()
&& self.required.is_empty()
&& self.property_names.is_none()
&& self.properties.is_empty()
&& self.pattern_properties.is_empty()
&& self.additional.is_none()
}
#[must_use]
pub(crate) fn required_count(&self) -> BoundCardinality {
BoundCardinality::from(self.required.len() as u64)
}
}
impl MaybeEmpty for ObjectLeaf {
fn is_empty(&self) -> bool {
if self.sizes.is_empty() {
return true;
}
let Some(ceiling) = self.effective_sizes().maximum else {
return false;
};
ceiling < self.required_count()
|| self
.sizes
.minimum
.as_ref()
.is_some_and(|min| ceiling < *min)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(crate) struct StringLeaf {
pub(crate) lengths: LengthBounds,
pub(crate) patterns: Vec<Arc<str>>,
pub(crate) formats: Vec<Arc<str>>,
pub(crate) content_media_types: Vec<Arc<str>>,
pub(crate) content_encodings: Vec<Arc<str>>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct AtLeastTwo<T>(Vec<T>);
impl<T: Ord> AtLeastTwo<T> {
pub(crate) fn new(mut items: Vec<T>) -> Result<Self, Vec<T>> {
items.sort();
items.dedup();
if items.len() < 2 {
return Err(items);
}
debug_assert!(
items.windows(2).all(|pair| pair[0] < pair[1]),
"items left unsorted or duplicated"
);
Ok(Self(items))
}
}
impl<T> AtLeastTwo<T> {
pub(crate) fn as_slice(&self) -> &[T] {
&self.0
}
pub(crate) fn into_vec(self) -> Vec<T> {
self.0
}
pub(crate) fn split_last(self) -> (Vec<T>, T) {
let mut items = self.0;
let last = items.pop().expect("at least two elements");
(items, last)
}
}
impl<T> IntoIterator for AtLeastTwo<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct NonEmpty<T>(T);
pub(crate) trait MaybeEmpty {
fn is_empty(&self) -> bool;
}
impl<T: MaybeEmpty> NonEmpty<T> {
pub(crate) fn new(inner: T) -> Option<Self> {
(!inner.is_empty()).then_some(Self(inner))
}
pub(crate) fn get(&self) -> &T {
&self.0
}
pub(crate) fn into_inner(self) -> T {
self.0
}
}
impl<T: Ord> MaybeEmpty for Bounds<T> {
fn is_empty(&self) -> bool {
Bounds::is_empty(self)
}
}
impl MaybeEmpty for StringLeaf {
fn is_empty(&self) -> bool {
self.lengths.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Bounds<T> {
pub(crate) minimum: Option<T>,
pub(crate) maximum: Option<T>,
}
impl<T> Default for Bounds<T> {
fn default() -> Self {
Self {
minimum: None,
maximum: None,
}
}
}
impl<T: Ord> Bounds<T> {
pub(crate) fn intersect(self, other: Self) -> Self {
Self {
minimum: tighter(self.minimum, other.minimum, Ord::max),
maximum: tighter(self.maximum, other.maximum, Ord::min),
}
}
pub(crate) fn contains(&self, value: &T) -> bool {
self.minimum.as_ref().is_none_or(|min| value >= min)
&& self.maximum.as_ref().is_none_or(|max| value <= max)
}
pub(crate) fn is_empty(&self) -> bool {
matches!((&self.minimum, &self.maximum), (Some(min), Some(max)) if min > max)
}
pub(crate) fn covers(&self, other: &Self) -> bool {
self.minimum
.as_ref()
.is_none_or(|min| other.minimum.as_ref().is_some_and(|start| start >= min))
&& self
.maximum
.as_ref()
.is_none_or(|max| other.maximum.as_ref().is_some_and(|end| end <= max))
}
pub(crate) fn is_unbounded(&self) -> bool {
self.minimum.is_none() && self.maximum.is_none()
}
fn hull(self, other: Self) -> Self {
Self {
minimum: self.minimum.zip(other.minimum).map(|(a, b)| a.min(b)),
maximum: self.maximum.zip(other.maximum).map(|(a, b)| a.max(b)),
}
}
}
impl<T: Discrete> Bounds<T> {
pub(crate) fn merge_all(mut windows: Vec<Self>) -> Vec<Self> {
if windows.len() < 2 {
return windows;
}
let count = windows.len();
windows.sort_by(|left, right| left.minimum.cmp(&right.minimum));
let mut merged: Vec<Self> = Vec::with_capacity(windows.len());
for window in windows {
match merged.last_mut() {
Some(last) if last.reaches(&window) => {
*last = std::mem::take(last).hull(window);
}
_ => merged.push(window),
}
}
debug_assert!(
Self::is_canonical(&merged),
"windows left unsorted or mergeable"
);
debug_assert!(merged.len() <= count, "merging invented a window");
debug_assert!(!merged.is_empty(), "merging dropped every window");
merged
}
fn is_canonical(windows: &[Self]) -> bool {
windows
.windows(2)
.all(|pair| pair[0].minimum <= pair[1].minimum && !pair[0].reaches(&pair[1]))
}
fn reaches(&self, next: &Self) -> bool {
debug_assert!(
self.minimum <= next.minimum,
"windows compared out of order"
);
let (Some(end), Some(start)) = (self.maximum.as_ref(), next.minimum.as_ref()) else {
return true;
};
end.clone()
.checked_increment()
.is_none_or(|above| *start <= above)
}
}
pub(crate) trait Discrete: Ord + Clone {
fn checked_increment(self) -> Option<Self>;
}
pub(crate) fn tighter<T>(
first: Option<T>,
second: Option<T>,
keep: impl FnOnce(T, T) -> T,
) -> Option<T> {
match (first, second) {
(Some(a), Some(b)) => Some(keep(a, b)),
(bound, None) | (None, bound) => bound,
}
}
pub(crate) fn drop_subsumed<T>(leaves: &mut Vec<T>, subsumes: impl Fn(&T, &T) -> bool) {
if leaves.len() < 2 {
return;
}
let mut keep = vec![true; leaves.len()];
for (index, leaf) in leaves.iter().enumerate() {
for (other_index, other) in leaves.iter().enumerate() {
if index == other_index || !keep[other_index] || !keep[index] {
continue;
}
if subsumes(other, leaf) {
keep[index] = false;
}
}
}
let mut index = 0;
leaves.retain(|_| {
let keeps = keep[index];
index += 1;
keeps
});
}
pub(crate) type LengthBounds = Bounds<BoundCardinality>;
pub(crate) type IntegerBounds = Bounds<BoundInteger>;
impl SchemaKind {
#[must_use]
pub(crate) fn finite_values(&self) -> Option<&[CanonicalJson]> {
match self {
SchemaKind::Const(value) => Some(std::slice::from_ref(value)),
SchemaKind::Enum(values) => Some(values.as_slice()),
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(_) => None,
}
}
#[must_use]
pub(crate) fn finite_domain_size(&self) -> Option<u64> {
if let Some(values) = self.finite_values() {
return Some(values.len() as u64);
}
if let SchemaKind::Integer(leaf) = self {
let bounds = &leaf.get().bounds;
return bounds
.minimum
.as_ref()
.zip(bounds.maximum.as_ref())
.and_then(|(minimum, maximum)| minimum.span_to(maximum));
}
let SchemaKind::MultiType(set) = self else {
return None;
};
let finite_types = JsonType::Null | JsonType::Boolean;
if set.intersect(finite_types) != *set {
return None;
}
let mut count = 0u64;
if set.contains(JsonType::Null) {
count += 1;
}
if set.contains(JsonType::Boolean) {
count += 2;
}
Some(count)
}
#[must_use]
pub(crate) fn canonical_type_set(set: JsonTypeSet) -> JsonTypeSet {
if set.contains(JsonType::Number) {
set.remove(JsonType::Integer)
} else {
set
}
}
#[must_use]
pub(crate) fn semantic_cover(set: JsonTypeSet) -> JsonTypeSet {
if set.contains(JsonType::Number) {
set.insert(JsonType::Integer)
} else {
set
}
}
#[must_use]
pub(crate) fn finite_values_saturated_domain(values: &[CanonicalJson]) -> Option<JsonTypeSet> {
const NULL: u8 = 1 << 0;
const FALSE: u8 = 1 << 1;
const TRUE: u8 = 1 << 2;
const BOTH_BOOLEANS: u8 = FALSE | TRUE;
const ALL: u8 = NULL | FALSE | TRUE;
let mut bits: u8 = 0;
for value in values {
bits |= match value.as_value() {
Value::Null => NULL,
Value::Bool(false) => FALSE,
Value::Bool(true) => TRUE,
Value::Number(_) | Value::String(_) | Value::Array(_) | Value::Object(_) => {
return None
}
};
}
match bits {
NULL => Some(JsonTypeSet::from(JsonType::Null)),
BOTH_BOOLEANS => Some(JsonTypeSet::from(JsonType::Boolean)),
ALL => Some(JsonType::Null | JsonType::Boolean),
_ => None,
}
}
}
#[derive(Debug, Clone)]
struct SchemaData {
kind: SchemaKind,
hash: u64,
}
impl PartialEq for SchemaData {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash && self.kind == other.kind
}
}
impl Eq for SchemaData {}
impl PartialOrd for SchemaData {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SchemaData {
fn cmp(&self, other: &Self) -> Ordering {
if std::ptr::eq(self, other) {
return Ordering::Equal;
}
self.kind.cmp(&other.kind)
}
}
impl Hash for SchemaData {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.hash);
}
}
fn structural_hash(kind: &SchemaKind) -> u64 {
let mut hasher = ahash::AHasher::default();
kind.hash(&mut hasher);
hasher.finish()
}