use std::collections::BTreeMap;
use serde_json::{Number, Value};
use crate::{
canonical::{
algebra,
context::CanonicalizationContext,
ir::{
type_set_schema, ArrayLeaf, BoundNumber, CanonicalJson, Discrete, Divisors,
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),
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::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);
}
debug_assert!(
complements.len() >= 2,
"an AllOf has at least two branches before applying De Morgan"
);
Some(algebra::union(complements, ctx))
}
SchemaKind::OneOf(_) | SchemaKind::Reference(_) => {
Some(Schema::new(SchemaKind::Not(schema.clone())))
}
SchemaKind::TypedGroup { .. } | SchemaKind::Integer(_) | SchemaKind::Raw(_) => None,
}
}
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();
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(_) | Value::Array(_) | Value::Object(_) => return None,
}
}
let mut branches = vec![type_set_schema(remaining)];
if let [member] = booleans.as_slice() {
branches.push(Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::Bool(!member),
))));
}
branches.extend(number_gaps(&numbers, ctx));
Some(algebra::union(branches, ctx))
}
fn number_gaps(numbers: &[Number], ctx: &CanonicalizationContext) -> Vec<Schema> {
if numbers.is_empty() {
return 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));
branches
}
fn number_window(
minimum: Option<BoundNumber>,
maximum: Option<BoundNumber>,
ctx: &CanonicalizationContext,
) -> Schema {
algebra::number_leaf(
NumberLeaf {
minimum,
maximum,
multiple_of: Divisors::default(),
},
ctx,
)
}
fn negate_number_leaf(leaf: &NumberLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
if !leaf.multiple_of.is_empty() {
return None;
}
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));
}
Some(algebra::union(branches, ctx))
}
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.patterns.is_empty()
|| !leaf.formats.is_empty()
|| !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,
patterns: Vec::new(),
formats: Vec::new(),
content_media_types: Vec::new(),
content_encodings: Vec::new(),
},
ctx,
)
}));
Some(algebra::union(branches, ctx))
}
fn negate_array_leaf(leaf: &ArrayLeaf, ctx: &CanonicalizationContext) -> Option<Schema> {
if leaf.unique || !leaf.prefix.is_empty() || leaf.items.is_some() || !leaf.contains.is_empty() {
return None;
}
let windows = length_windows(&leaf.lengths)?;
let mut branches = vec![type_set_schema(JsonTypeSet::all().remove(JsonType::Array))];
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) -> Option<Schema> {
if set.contains(JsonType::Integer) && !set.contains(JsonType::Number) {
return None;
}
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::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::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()
}
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);
if set.contains(JsonType::Integer) && !set.contains(JsonType::Number) {
assert!(
complement.is_none(),
"integer-only set {set:?} must decline"
);
continue;
}
let complement = complement.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)
));
}
}