use std::{collections::BTreeSet, sync::Arc};
use crate::canonical::{
algebra,
context::CanonicalizationContext,
emptiness,
ir::{ArrayLeaf, ContainsFacet, ObjectLeaf, ObjectViolation, PropertyMap, Schema, SchemaKind},
parse::{self, ParseOutput},
DefinitionMap, ROOT_DEFINITION_KEY,
};
const FOLD_BUDGET: u64 = 40_000;
const SETTLE_ROUNDS: usize = 8;
pub(crate) fn through_targets(
mut parsed: ParseOutput,
ctx: &CanonicalizationContext,
) -> ParseOutput {
if !parsed.has_references || !foldable(&parsed) {
return parsed;
}
let Some(order) = emptiness::settling_order(&parsed.definitions) else {
return parsed;
};
let plain =
CanonicalizationContext::new(ctx.draft(), ctx.pattern_options(), ctx.validate_formats());
let mut definitions = parsed.definitions.clone();
let mut resolving = reading(&definitions, ctx);
for uri in order {
let body = definitions
.get(&uri)
.cloned()
.expect("the order names the map's own keys");
let settled = settle(&body, &definitions, &resolving, &plain);
if approximated(&resolving) || approximated(&plain) {
return parsed;
}
if settled == body {
continue;
}
definitions.insert(uri, settled);
resolving.read_targets(Arc::new(definitions.clone()));
}
let root = settle(&parsed.root, &definitions, &resolving, &plain);
if approximated(&resolving) || approximated(&plain) {
return parsed;
}
parsed.root = root;
parsed.definitions = definitions;
parse::prune_unreachable_definitions(&parsed.root, &mut parsed.definitions);
parsed
}
fn settle(
schema: &Schema,
definitions: &DefinitionMap,
ctx: &CanonicalizationContext,
plain: &CanonicalizationContext,
) -> Schema {
let mut current = schema.clone();
for _ in 0..SETTLE_ROUNDS {
let next = folded(¤t, definitions, ctx, plain);
if approximated(ctx) || approximated(plain) || next == current {
return next;
}
current = next;
}
current
}
fn approximated(ctx: &CanonicalizationContext) -> bool {
ctx.saw_inexact_intersection() || ctx.outgrew_distribution()
}
fn reading(definitions: &DefinitionMap, ctx: &CanonicalizationContext) -> CanonicalizationContext {
CanonicalizationContext::new(ctx.draft(), ctx.pattern_options(), ctx.validate_formats())
.within(FOLD_BUDGET)
.resolving(Arc::new(definitions.clone()), BTreeSet::new())
}
fn foldable(parsed: &ParseOutput) -> bool {
parsed
.definitions
.values()
.chain(std::iter::once(&parsed.root))
.all(settled)
}
fn settled(schema: &Schema) -> bool {
match schema.kind() {
SchemaKind::Reference(uri) => uri.as_ref() != ROOT_DEFINITION_KEY,
SchemaKind::Not(inner) | SchemaKind::TypedGroup { body: inner, .. } => settled(inner),
SchemaKind::AllOf(branches) | SchemaKind::AnyOf(branches) => {
branches.as_slice().iter().all(settled)
}
SchemaKind::OneOf(branches) => branches.iter().all(settled),
SchemaKind::Array(leaf) => {
let leaf = leaf.get();
leaf.prefix
.iter()
.chain(leaf.items.iter())
.chain(leaf.contains.iter().map(|facet| &facet.schema))
.all(settled)
}
SchemaKind::Object(leaf) => {
let leaf = leaf.get();
leaf.property_names
.as_ref()
.is_none_or(|names| !algebra::contains_reference(names))
&& leaf.violations.iter().all(|violation| match violation {
ObjectViolation::NameFails(names) => !algebra::contains_reference(names),
ObjectViolation::UndeclaredValueFails { additional, .. } => {
!algebra::contains_reference(additional)
}
})
&& leaf
.properties
.values()
.chain(leaf.pattern_properties.values())
.chain(leaf.additional.iter())
.all(settled)
}
SchemaKind::MultiType(_)
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Raw(_) => true,
}
}
fn folded(
schema: &Schema,
definitions: &DefinitionMap,
ctx: &CanonicalizationContext,
plain: &CanonicalizationContext,
) -> Schema {
match schema.kind() {
SchemaKind::AllOf(branches) => {
let conjuncts = each(branches.as_slice(), definitions, ctx, plain);
if conjuncts == branches.as_slice()
&& !conjuncts.iter().any(|branch| names_a_body(branch, ctx))
{
return schema.clone();
}
conjuncts
.into_iter()
.reduce(|left, right| algebra::intersect(left, right, ctx))
.unwrap_or_else(|| schema.clone())
}
SchemaKind::AnyOf(branches) => {
let union = each(branches.as_slice(), definitions, ctx, plain);
if union == branches.as_slice() {
return schema.clone();
}
algebra::union(union, plain)
}
SchemaKind::OneOf(branches) => {
let mut choice = each(branches, definitions, ctx, plain);
if !choice.iter().any(algebra::contains_reference) {
if let Some(expanded) = algebra::concrete_one_of(choice.clone(), definitions, ctx) {
return expanded;
}
}
if algebra::choice_folds(&choice, definitions, ctx) {
return algebra::union(choice, ctx);
}
if choice == *branches {
return schema.clone();
}
choice.retain(|branch| !matches!(branch.kind(), SchemaKind::False));
choice.sort();
debug_assert!(
choice.len() > 1,
"a choice this thin degrades to a union above"
);
Schema::new(SchemaKind::OneOf(choice))
}
SchemaKind::Not(inner) => {
let complemented = folded(inner, definitions, ctx, plain);
if complemented == *inner {
return schema.clone();
}
Schema::new(SchemaKind::Not(complemented))
}
SchemaKind::Array(leaf) => {
let leaf = leaf.get();
let items = ArrayLeaf {
lengths: leaf.lengths.clone(),
distinctness: leaf.distinctness,
prefix: each(&leaf.prefix, definitions, ctx, plain),
items: leaf
.items
.as_ref()
.map(|schema| folded(schema, definitions, ctx, plain)),
contains: leaf
.contains
.iter()
.map(|facet| ContainsFacet {
schema: folded(&facet.schema, definitions, ctx, plain),
minimum: facet.minimum.clone(),
maximum: facet.maximum.clone(),
})
.collect(),
};
let reads_a_body = leaf
.items
.as_ref()
.is_some_and(|tail| names_a_body(tail, ctx))
|| leaf
.contains
.iter()
.any(|facet| names_a_body(&facet.schema, ctx));
if items == *leaf && !reads_a_body {
return schema.clone();
}
algebra::array_leaf(items, ctx)
}
SchemaKind::Object(leaf) => {
let leaf = leaf.get();
let entries = |map: &PropertyMap| -> PropertyMap {
map.iter()
.map(|(key, schema)| (Arc::clone(key), folded(schema, definitions, ctx, plain)))
.collect()
};
let keys = ObjectLeaf {
sizes: leaf.sizes.clone(),
required: leaf.required.clone(),
property_names: leaf.property_names.clone(),
properties: entries(&leaf.properties),
pattern_properties: entries(&leaf.pattern_properties),
additional: leaf
.additional
.as_ref()
.map(|schema| folded(schema, definitions, ctx, plain)),
violations: leaf.violations.clone(),
};
if keys == *leaf {
return schema.clone();
}
algebra::object_leaf(keys, ctx)
}
SchemaKind::Reference(uri) => match ctx.definition(uri) {
Some(target) if matches!(target.kind(), SchemaKind::False) => Schema::falsy(),
Some(_) | None => schema.clone(),
},
SchemaKind::TypedGroup { .. }
| SchemaKind::MultiType(_)
| SchemaKind::String(_)
| SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::True
| SchemaKind::False
| SchemaKind::Raw(_) => schema.clone(),
}
}
fn each(
branches: &[Schema],
definitions: &DefinitionMap,
ctx: &CanonicalizationContext,
plain: &CanonicalizationContext,
) -> Vec<Schema> {
branches
.iter()
.map(|branch| folded(branch, definitions, ctx, plain))
.collect()
}
fn names_a_body(schema: &Schema, ctx: &CanonicalizationContext) -> bool {
matches!(schema.kind(), SchemaKind::Reference(uri) if ctx.definition(uri).is_some())
}