use std::{collections::BTreeMap, sync::Arc};
use serde_json::{Number, Value};
use crate::{
canonical::{
algebra,
context::CanonicalizationContext,
ir::{
type_set_schema, ArrayLeaf, AtLeastTwo, BoundCardinality, BoundInteger, BoundNumber,
CanonicalJson, ContainsFacet, Discrete, Divisors, ExcludedDivisors, IntegerBounds,
IntegerLeaf, LengthBounds, NumberLeaf, ObjectLeaf, Schema, SchemaKind, StringLeaf,
},
},
JsonType, JsonTypeSet,
};
pub(crate) fn negate(schema: &Schema, ctx: &CanonicalizationContext) -> Option<Schema> {
match schema.kind() {
SchemaKind::True => Some(Schema::new(SchemaKind::False)),
SchemaKind::False => Some(Schema::new(SchemaKind::True)),
SchemaKind::MultiType(set) => negate_type_set(*set, ctx),
SchemaKind::Const(value) => negate_finite_values(std::slice::from_ref(value), ctx),
SchemaKind::Enum(values) => negate_finite_values(values.as_slice(), ctx),
SchemaKind::Number(leaf) => negate_number_leaf(leaf.get(), ctx),
SchemaKind::Integer(leaf) => negate_integer_leaf(leaf.get(), ctx),
SchemaKind::String(leaf) => negate_string_leaf(leaf.get(), ctx),
SchemaKind::Array(leaf) => negate_array_leaf(leaf.get(), ctx),
SchemaKind::Object(leaf) => negate_object_leaf(leaf.get(), ctx),
SchemaKind::Not(inner) => Some(inner.clone()),
SchemaKind::AnyOf(branches) => {
let mut result = Schema::new(SchemaKind::True);
for branch in branches.as_slice() {
result = algebra::intersect(result, negate(branch, ctx)?, ctx);
}
Some(result)
}
SchemaKind::AllOf(branches) => {
let mut complements = Vec::with_capacity(branches.as_slice().len());
for branch in branches.as_slice() {
let Some(complement) = negate(branch, ctx) else {
return Some(Schema::new(SchemaKind::Not(schema.clone())));
};
complements.push(complement);
}
Some(algebra::union(complements, ctx))
}
SchemaKind::OneOf(_) | SchemaKind::Reference(_) => {
Some(Schema::new(SchemaKind::Not(schema.clone())))
}
SchemaKind::TypedGroup { ty, body } => negate_typed_group(*ty, body, ctx),
SchemaKind::Raw(_) => None,
}
}
fn negate_typed_group(
ty: JsonType,
body: &Schema,
ctx: &CanonicalizationContext,
) -> Option<Schema> {
let off_type = negate_type_set(JsonTypeSet::from(ty), ctx)?;
let off_body = negate(body, ctx)?;
let within = algebra::intersect(type_set_schema(JsonTypeSet::from(ty)), off_body, ctx);
Some(algebra::union(vec![off_type, within], ctx))
}
fn negate_finite_values(values: &[CanonicalJson], ctx: &CanonicalizationContext) -> Option<Schema> {
let mut remaining = JsonTypeSet::all();
let mut booleans = Vec::new();
let mut numbers: Vec<Number> = Vec::new();
let mut strings: Vec<Arc<str>> = Vec::new();
let mut empty_array = false;
let mut empty_object = false;
for value in values {
match value.as_value() {
Value::Null => remaining = remaining.remove(JsonType::Null),
Value::Bool(member) => {
remaining = remaining.remove(JsonType::Boolean);
booleans.push(*member);
}
Value::Number(number) => {
remaining = remaining.remove(JsonType::Number).remove(JsonType::Integer);
numbers.push(number.clone());
}
Value::String(text) => {
remaining = remaining.remove(JsonType::String);
strings.push(Arc::from(text.as_str()));
}
Value::Array(items) if items.is_empty() => {
remaining = remaining.remove(JsonType::Array);
empty_array = true;
}
Value::Object(entries) if entries.is_empty() => {
remaining = remaining.remove(JsonType::Object);
empty_object = true;
}
Value::Array(_) | Value::Object(_) => return None,
}
}
let mut branches = vec![type_set_schema(remaining)];
if empty_array {
branches.push(algebra::array_leaf(
ArrayLeaf {
lengths: above_empty(),
unique: false,
prefix: Vec::new(),
items: None,
contains: Vec::new(),
},
ctx,
));
}
if empty_object {
branches.push(object_branch(
above_empty(),
Vec::new(),
BTreeMap::new(),
ctx,
));
}
if let [member] = booleans.as_slice() {
branches.push(Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Bool(!member),
))));
}
branches.extend(number_gaps(&numbers, ctx)?);
if !strings.is_empty() {
strings.sort();
strings.dedup();
branches.push(algebra::string_leaf(
StringLeaf {
lengths: LengthBounds::default(),
patterns: Vec::new(),
excluded_patterns: Vec::new(),
formats: Vec::new(),
excluded_formats: Vec::new(),
content_media_types: Vec::new(),
content_encodings: Vec::new(),
excluded: strings,
},
ctx,
));
}
Some(algebra::union(branches, ctx))
}
fn number_gaps(numbers: &[Number], ctx: &CanonicalizationContext) -> Option<Vec<Schema>> {
if numbers.is_empty() {
return Some(Vec::new());
}
let mut ends: Vec<BoundNumber> = numbers
.iter()
.map(|number| BoundNumber::new(number, false))
.collect();
ends.sort();
let mut branches = Vec::with_capacity(ends.len() + 1);
let mut lower: Option<BoundNumber> = None;
for end in ends {
branches.push(number_window(lower.take(), Some(end.clone()), ctx)?);
lower = Some(end);
}
branches.push(number_window(lower, None, ctx)?);
Some(branches)
}
fn number_window(
minimum: Option<BoundNumber>,
maximum: Option<BoundNumber>,
ctx: &CanonicalizationContext,
) -> Option<Schema> {
let leaf = NumberLeaf {
minimum,
maximum,
multiple_of: Divisors::default(),
not_multiple_of: ExcludedDivisors::default(),
excludes_integers: false,
};
algebra::integer_bounds_within(&leaf)?;
Some(algebra::number_leaf(leaf, ctx))
}
fn negate_number_leaf(leaf: &NumberLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
let mut branches = vec![type_set_schema(
JsonTypeSet::all()
.remove(JsonType::Number)
.remove(JsonType::Integer),
)];
if let Some(minimum) = &leaf.minimum {
branches.push(number_window(None, Some(flipped(minimum)), ctx)?);
}
if let Some(maximum) = &leaf.maximum {
branches.push(number_window(Some(flipped(maximum)), None, ctx)?);
}
if leaf.excludes_integers {
branches.push(type_set_schema(JsonTypeSet::from(JsonType::Integer)));
}
branches.extend(leaf.multiple_of.as_slice().iter().map(|step| {
algebra::number_leaf(
NumberLeaf {
not_multiple_of: ExcludedDivisors::one(step.clone()),
..NumberLeaf::default()
},
ctx,
)
}));
branches.extend(leaf.not_multiple_of.as_slice().iter().map(|step| {
algebra::number_leaf(
NumberLeaf {
multiple_of: Divisors::one(step.clone()),
..NumberLeaf::default()
},
ctx,
)
}));
Some(algebra::union(branches, ctx))
}
fn negate_integer_leaf(leaf: &IntegerLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
let mut branches = vec![type_set_schema(
JsonTypeSet::all()
.remove(JsonType::Number)
.remove(JsonType::Integer),
)];
branches.push(non_integer_number(ctx));
if let Some(minimum) = &leaf.bounds.minimum {
let below = minimum.clone().checked_decrement()?;
branches.push(integer_window(None, Some(below), ctx));
}
if let Some(maximum) = &leaf.bounds.maximum {
let above = maximum.clone().checked_increment()?;
branches.push(integer_window(Some(above), None, ctx));
}
branches.extend(leaf.multiple_of.as_slice().iter().map(|step| {
algebra::integer_leaf(
IntegerLeaf {
not_multiple_of: ExcludedDivisors::one(step.clone()),
..IntegerLeaf::default()
},
ctx,
)
}));
branches.extend(leaf.not_multiple_of.as_slice().iter().map(|step| {
algebra::integer_leaf(
IntegerLeaf {
multiple_of: Divisors::one(step.clone()),
..IntegerLeaf::default()
},
ctx,
)
}));
Some(algebra::union(branches, ctx))
}
fn non_integer_number(ctx: &CanonicalizationContext) -> Schema {
algebra::number_leaf(
NumberLeaf {
excludes_integers: true,
..NumberLeaf::default()
},
ctx,
)
}
fn integer_window(
minimum: Option<BoundInteger>,
maximum: Option<BoundInteger>,
ctx: &CanonicalizationContext,
) -> Schema {
algebra::integer_leaf(
IntegerLeaf {
bounds: IntegerBounds { minimum, maximum },
..IntegerLeaf::default()
},
ctx,
)
}
fn above_empty() -> LengthBounds {
LengthBounds {
minimum: Some(BoundCardinality::from(1)),
maximum: None,
}
}
fn finite_strings(values: &[Arc<str>]) -> Schema {
let members: Vec<CanonicalJson> = values
.iter()
.map(|value| CanonicalJson::from_value(&Value::String(value.to_string())))
.collect();
match AtLeastTwo::new(members) {
Ok(set) => Schema::new(SchemaKind::Enum(set)),
Err(mut single) => Schema::new(SchemaKind::Const(
single.pop().expect("a non-empty exclusion list"),
)),
}
}
fn flipped(bound: &BoundNumber) -> BoundNumber {
BoundNumber::new(&bound.to_number(), !bound.is_inclusive())
}
pub(crate) fn length_windows(lengths: &LengthBounds) -> Option<Vec<LengthBounds>> {
let mut windows = Vec::new();
if let Some(below) = lengths
.minimum
.as_ref()
.and_then(|minimum| minimum.clone().checked_decrement())
{
windows.push(LengthBounds {
minimum: None,
maximum: Some(below),
});
}
if let Some(maximum) = &lengths.maximum {
let above = maximum.clone().checked_increment()?;
windows.push(LengthBounds {
minimum: Some(above),
maximum: None,
});
}
Some(windows)
}
fn negate_string_leaf(leaf: &StringLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
if !leaf.excluded.is_empty() {
let mut branches = vec![type_set_schema(JsonTypeSet::all().remove(JsonType::String))];
branches.push(finite_strings(&leaf.excluded));
let positive = StringLeaf {
excluded: Vec::new(),
..leaf.clone()
};
branches.push(negate_string_leaf(&positive, ctx)?);
return Some(algebra::union(branches, ctx));
}
if !leaf.content_media_types.is_empty() || !leaf.content_encodings.is_empty() {
return None;
}
let windows = length_windows(&leaf.lengths)?;
let mut branches = vec![type_set_schema(JsonTypeSet::all().remove(JsonType::String))];
branches.extend(windows.into_iter().map(|lengths| {
algebra::string_leaf(
StringLeaf {
lengths,
..StringLeaf::default()
},
ctx,
)
}));
branches.extend(leaf.formats.iter().map(|format| {
algebra::string_leaf(
StringLeaf {
excluded_formats: vec![Arc::clone(format)],
..StringLeaf::default()
},
ctx,
)
}));
branches.extend(leaf.excluded_formats.iter().map(|format| {
algebra::string_leaf(
StringLeaf {
formats: vec![Arc::clone(format)],
..StringLeaf::default()
},
ctx,
)
}));
branches.extend(leaf.patterns.iter().map(|pattern| {
algebra::string_leaf(
StringLeaf {
excluded_patterns: vec![Arc::clone(pattern)],
..StringLeaf::default()
},
ctx,
)
}));
branches.extend(leaf.excluded_patterns.iter().map(|pattern| {
algebra::string_leaf(
StringLeaf {
patterns: vec![Arc::clone(pattern)],
..StringLeaf::default()
},
ctx,
)
}));
Some(algebra::union(branches, ctx))
}
fn negate_array_leaf(leaf: &ArrayLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
if leaf.unique || !leaf.prefix.is_empty() {
return None;
}
let windows = length_windows(&leaf.lengths)?;
let mut branches = vec![type_set_schema(JsonTypeSet::all().remove(JsonType::Array))];
if let Some(items) = &leaf.items {
if !ctx.draft().is_known_keyword("contains") {
return None;
}
branches.push(algebra::array_leaf(
ArrayLeaf {
lengths: LengthBounds::default(),
unique: false,
prefix: Vec::new(),
items: None,
contains: vec![ContainsFacet {
schema: negate(items, ctx)?,
minimum: None,
maximum: None,
}],
},
ctx,
));
}
for facet in &leaf.contains {
if facet.maximum.is_some() || facet.effective_minimum() != BoundCardinality::from(1) {
return None;
}
branches.push(algebra::array_leaf(
ArrayLeaf {
lengths: LengthBounds::default(),
unique: false,
prefix: Vec::new(),
items: Some(negate(&facet.schema, ctx)?),
contains: Vec::new(),
},
ctx,
));
}
branches.extend(windows.into_iter().map(|lengths| {
algebra::array_leaf(
ArrayLeaf {
lengths,
unique: false,
prefix: Vec::new(),
items: None,
contains: Vec::new(),
},
ctx,
)
}));
Some(algebra::union(branches, ctx))
}
fn negate_object_leaf(leaf: &ObjectLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
if leaf.property_names.is_some()
|| !leaf.pattern_properties.is_empty()
|| leaf.additional.is_some()
{
return None;
}
let mut branches = vec![type_set_schema(JsonTypeSet::all().remove(JsonType::Object))];
for sizes in length_windows(&leaf.sizes)? {
branches.push(object_branch(sizes, Vec::new(), BTreeMap::new(), ctx));
}
for key in &leaf.required {
let absent = BTreeMap::from([(key.clone(), Schema::new(SchemaKind::False))]);
branches.push(object_branch(
LengthBounds::default(),
Vec::new(),
absent,
ctx,
));
}
for (key, schema) in &leaf.properties {
let violating = negate(schema, ctx)?;
let held = BTreeMap::from([(key.clone(), violating)]);
branches.push(object_branch(
LengthBounds::default(),
vec![key.clone()],
held,
ctx,
));
}
Some(algebra::union(branches, ctx))
}
fn object_branch(
sizes: LengthBounds,
required: Vec<std::sync::Arc<str>>,
properties: BTreeMap<std::sync::Arc<str>, Schema>,
ctx: &CanonicalizationContext,
) -> Schema {
algebra::object_leaf(
ObjectLeaf {
sizes,
required,
property_names: None,
properties,
pattern_properties: BTreeMap::new(),
additional: None,
},
ctx,
)
}
fn negate_type_set(set: JsonTypeSet, ctx: &CanonicalizationContext) -> Option<Schema> {
let mut complement = JsonTypeSet::empty();
for ty in [
JsonType::Null,
JsonType::Boolean,
JsonType::String,
JsonType::Array,
JsonType::Object,
] {
if !set.contains(ty) {
complement = complement.insert(ty);
}
}
if set.contains(JsonType::Integer) && !set.contains(JsonType::Number) {
let mut branches = vec![non_integer_number(ctx)];
if !complement.is_empty() {
branches.push(type_set_schema(complement));
}
return Some(algebra::union(branches, ctx));
}
if !set.contains(JsonType::Number) {
complement = complement.insert(JsonType::Number);
}
if complement.is_empty() {
return Some(Schema::new(SchemaKind::False));
}
Some(type_set_schema(complement))
}
#[cfg(test)]
mod tests {
use referencing::Draft;
use serde_json::{json, Value};
use super::*;
use crate::{canonical::ir::BoundRational, options::PatternEngineOptions};
fn context() -> CanonicalizationContext {
CanonicalizationContext::new(Draft::Draft202012, PatternEngineOptions::default(), false)
}
const TYPES: [JsonType; 7] = [
JsonType::Null,
JsonType::Boolean,
JsonType::Integer,
JsonType::Number,
JsonType::String,
JsonType::Array,
JsonType::Object,
];
fn representatives() -> [Value; 7] {
[
json!(null),
json!(true),
json!(1),
json!(1.5),
json!("x"),
json!([]),
json!({}),
]
}
fn admits(set: JsonTypeSet, value: &Value) -> bool {
match value {
Value::Null => set.contains(JsonType::Null),
Value::Bool(_) => set.contains(JsonType::Boolean),
Value::Number(number) if number.is_i64() => {
set.contains(JsonType::Integer) || set.contains(JsonType::Number)
}
Value::Number(_) => set.contains(JsonType::Number),
Value::String(_) => set.contains(JsonType::String),
Value::Array(_) => set.contains(JsonType::Array),
Value::Object(_) => set.contains(JsonType::Object),
}
}
#[allow(clippy::wildcard_enum_match_arm)]
fn complement_admits(schema: &Schema, value: &Value) -> bool {
match schema.kind() {
SchemaKind::True => true,
SchemaKind::False => false,
SchemaKind::MultiType(set) => admits(*set, value),
SchemaKind::Const(constant) => {
assert_eq!(constant.as_value(), &Value::Null);
value.is_null()
}
SchemaKind::Enum(values) => {
let members: Vec<&Value> = values
.as_slice()
.iter()
.map(CanonicalJson::as_value)
.collect();
assert_eq!(members, [&Value::Bool(false), &Value::Bool(true)]);
value.is_boolean()
}
SchemaKind::AnyOf(branches) => branches
.as_slice()
.iter()
.any(|branch| complement_admits(branch, value)),
SchemaKind::Number(leaf) => {
assert!(leaf.get().minimum.is_none());
assert!(leaf.get().maximum.is_none());
assert!(leaf.get().multiple_of.is_empty());
let barred: Vec<Number> = leaf
.get()
.not_multiple_of
.as_slice()
.iter()
.map(BoundRational::to_number)
.collect();
assert_eq!(barred, [Number::from(1)]);
matches!(value, Value::Number(number) if !number.is_i64() && !number.is_u64())
}
other => {
panic!("scaffold complement of a type set is a type-set shape, got {other:?}")
}
}
}
#[test]
fn type_set_complement_partitions_the_value_space() {
let ctx = context();
for mask in 0u8..128 {
let mut set = JsonTypeSet::empty();
for ty in TYPES {
if mask & ty as u8 != 0 {
set = set.insert(ty);
}
}
let schema = Schema::new(SchemaKind::MultiType(set));
let complement = negate(&schema, &ctx).expect("expressible complement");
for value in &representatives() {
assert_ne!(
admits(set, value),
complement_admits(&complement, value),
"membership not partitioned for set {set:?} on {value}"
);
}
}
}
#[test]
fn boolean_schemas_negate_to_each_other() {
let ctx = context();
assert!(matches!(
negate(&Schema::new(SchemaKind::True), &ctx).map(|s| s.kind().clone()),
Some(SchemaKind::False)
));
assert!(matches!(
negate(&Schema::new(SchemaKind::False), &ctx).map(|s| s.kind().clone()),
Some(SchemaKind::True)
));
}
}