use std::{
cell::Cell,
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
hash::{Hash, Hasher},
sync::Arc,
};
use referencing::Draft;
use serde_json::{Number, Value};
use strum::{IntoStaticStr, VariantArray};
use crate::{
canonical::{
algebra,
context::CanonicalizationContext,
emit, emptiness,
error::OperandMismatch,
ir::{
BoundCardinality, BoundInteger, BoundNumber, Distinctness, ObjectLeaf, Schema,
SchemaKind, UncheckableFacet, Verdict,
},
negate, oracle, parse, rename, CanonicalizationError, ROOT_DEFINITION_KEY,
},
options::PatternEngineOptions,
JsonType,
};
pub(crate) type DefinitionMap = BTreeMap<Arc<str>, Schema>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoStaticStr, VariantArray)]
#[strum(serialize_all = "snake_case")]
pub enum Containment {
Yes,
No,
Unknown,
}
impl Containment {
#[must_use]
pub fn as_str(self) -> &'static str {
self.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoStaticStr, VariantArray)]
#[strum(serialize_all = "snake_case")]
pub enum Satisfiability {
Yes,
No,
Unknown,
}
impl Satisfiability {
#[must_use]
pub fn as_str(self) -> &'static str {
self.into()
}
}
fn pointers_read(schema: &Schema, definitions: &DefinitionMap) -> BTreeSet<Arc<str>> {
let reached = emptiness::reachable_definition_keys(schema, None, definitions);
let mut pointers: BTreeSet<Arc<str>> = reached.iter().map(Arc::clone).collect();
let bodies = reached
.iter()
.filter_map(|uri| definitions.get(uri.as_ref() as &str));
for node in std::iter::once(schema).chain(bodies) {
let mut referenced = Vec::new();
emptiness::collect_classified_references(
node,
emptiness::Position::InPlace,
&mut referenced,
);
pointers.extend(referenced.drain(..).map(|(uri, _)| Arc::clone(uri)));
}
pointers
}
const CANDIDATE_DEPTH: u32 = 6;
const CANDIDATE_LENGTH: u64 = 64;
const FILLER_KEY: &str = "a";
const CANDIDATE_NODES: u32 = 4_096;
const COVERS_DIFFERENCE_BUDGET: u64 = 20_000;
const CANDIDATE_KEYS_PER_SEED: usize = 8;
fn demanded_length(minimum: Option<&BoundCardinality>) -> Option<usize> {
let Some(minimum) = minimum else {
return Some(0);
};
minimum
.to_usize()
.filter(|length| *length as u64 <= CANDIDATE_LENGTH)
}
fn candidate_instances(
node: &Schema,
document: &Document,
depth: u32,
budget: &Cell<u32>,
ctx: &CanonicalizationContext,
) -> Vec<Value> {
if depth == 0 || budget.get() == 0 {
return Vec::new();
}
budget.set(budget.get() - 1);
match node.kind() {
SchemaKind::Reference(uri) => document
.target(uri)
.map(|target| candidate_instances(target, document, depth - 1, budget, ctx))
.unwrap_or_default(),
SchemaKind::True => vec![Value::Null],
SchemaKind::Const(value) => vec![value.as_value().clone()],
SchemaKind::Enum(values) => values
.as_slice()
.iter()
.map(|value| value.as_value().clone())
.collect(),
SchemaKind::MultiType(set) => set.iter().map(shortest_instance).collect(),
SchemaKind::TypedGroup { body, .. } => {
candidate_instances(body, document, depth - 1, budget, ctx)
}
SchemaKind::String(leaf) => {
let Some(length) = demanded_length(leaf.get().lengths.minimum.as_ref()) else {
return Vec::new();
};
vec![Value::String("a".repeat(length))]
}
SchemaKind::Integer(leaf) => {
let bounds = &leaf.get().bounds;
let end = |bound: &BoundInteger| WindowEnd {
limit: bound.to_number(),
admitted: true,
};
whole_number_candidates(
bounds.minimum.as_ref().map(end),
bounds.maximum.as_ref().map(end),
)
}
SchemaKind::Number(leaf) => {
let leaf = leaf.get();
let end = |bound: &BoundNumber| WindowEnd {
limit: bound.to_number(),
admitted: bound.is_inclusive(),
};
whole_number_candidates(
leaf.minimum.as_ref().map(end),
leaf.maximum.as_ref().map(end),
)
}
SchemaKind::Array(leaf) => {
let leaf = leaf.get();
let Some(length) = demanded_length(leaf.lengths.minimum.as_ref()) else {
return Vec::new();
};
let demanded: Option<Vec<Value>> = leaf
.contains
.iter()
.map(|facet| {
candidate_instances(&facet.schema, document, depth - 1, budget, ctx)
.into_iter()
.next()
})
.collect();
let Some(demanded) = demanded else {
return Vec::new();
};
let element = |index: usize| {
let schema = leaf.prefix.get(index).or(leaf.items.as_ref());
schema.map_or(Some(Value::Null), |schema| {
candidate_instances(schema, document, depth - 1, budget, ctx)
.into_iter()
.next()
})
};
let mut items = Vec::new();
for index in 0..leaf.prefix.len() {
let Some(value) = element(index) else {
return Vec::new();
};
items.push(value);
}
items.extend(demanded);
for index in items.len()..length {
let Some(value) = element(index) else {
return Vec::new();
};
items.push(value);
}
vec![Value::Array(items)]
}
SchemaKind::Object(leaf) => {
let leaf = leaf.get();
let Some(size) = demanded_length(leaf.sizes.minimum.as_ref()) else {
return Vec::new();
};
let mut object = serde_json::Map::new();
for key in candidate_keys(leaf, size, ctx) {
let governing = leaf
.properties
.get(key.as_str())
.or_else(|| {
leaf.pattern_properties
.iter()
.find(|(pattern, _)| algebra::matches_key(pattern, &key, ctx))
.map(|(_, schema)| schema)
})
.or(leaf.additional.as_ref());
let Some(value) = governing.map_or(Some(Value::Null), |schema| {
candidate_instances(schema, document, depth - 1, budget, ctx)
.into_iter()
.next()
}) else {
return Vec::new();
};
object.insert(key, value);
}
vec![Value::Object(object)]
}
SchemaKind::AllOf(branches) | SchemaKind::AnyOf(branches) => branches
.as_slice()
.iter()
.flat_map(|branch| candidate_instances(branch, document, depth - 1, budget, ctx))
.collect(),
SchemaKind::OneOf(branches) => branches
.iter()
.flat_map(|branch| candidate_instances(branch, document, depth - 1, budget, ctx))
.collect(),
SchemaKind::False | SchemaKind::Not(_) | SchemaKind::Raw(_) => Vec::new(),
}
}
fn candidate_keys(leaf: &ObjectLeaf, size: usize, ctx: &CanonicalizationContext) -> Vec<String> {
let mut keys: Vec<String> = leaf.required.iter().map(ToString::to_string).collect();
let wanted = size.max(keys.len());
if keys.len() >= wanted {
return keys;
}
let admitted = |key: &str| {
leaf.property_names.as_ref().is_none_or(|names| {
algebra::admits_value(
names,
&Value::String(key.to_string()),
UncheckableFacet::Undecided,
ctx,
) == Verdict::Admits
})
};
let seeds = leaf
.properties
.keys()
.map(ToString::to_string)
.chain(
leaf.pattern_properties
.keys()
.map(|pattern| literal_prefix(pattern)),
)
.chain(std::iter::once(FILLER_KEY.to_string()));
for seed in seeds {
for index in 0..CANDIDATE_KEYS_PER_SEED {
if keys.len() >= wanted {
return keys;
}
let key = format!("{seed}{index}");
if !keys.contains(&key) && admitted(&key) {
keys.push(key);
}
}
}
keys
}
fn literal_prefix(pattern: &str) -> String {
pattern
.strip_prefix('^')
.unwrap_or(pattern)
.chars()
.take_while(|character| character.is_alphanumeric() || *character == '_')
.collect()
}
fn whole_number_candidates(minimum: Option<WindowEnd>, maximum: Option<WindowEnd>) -> Vec<Value> {
let interior = interior_point(
minimum.as_ref().map(|end| &end.limit),
maximum.as_ref().map(|end| &end.limit),
);
let mut candidates: Vec<Value> = [minimum, maximum]
.into_iter()
.flatten()
.filter(|end| end.admitted)
.map(|end| Value::Number(end.limit))
.collect();
candidates.extend(interior);
candidates.push(Value::Number(0.into()));
candidates
}
struct WindowEnd {
limit: Number,
admitted: bool,
}
fn interior_point(minimum: Option<&Number>, maximum: Option<&Number>) -> Option<Value> {
let point = match (
minimum.and_then(Number::as_f64),
maximum.and_then(Number::as_f64),
) {
(Some(low), Some(high)) => low + (high - low) / 2.0,
(Some(low), None) => low + 1.0,
(None, Some(high)) => high - 1.0,
(None, None) => return None,
};
Number::from_f64(point).map(Value::Number)
}
fn shortest_instance(ty: JsonType) -> Value {
match ty {
JsonType::Null => Value::Null,
JsonType::Boolean => Value::Bool(false),
JsonType::String => Value::String(String::new()),
JsonType::Integer | JsonType::Number => Value::Number(0.into()),
JsonType::Array => Value::Array(Vec::new()),
JsonType::Object => Value::Object(serde_json::Map::new()),
}
}
fn holds_a_value(window_is_enough: bool) -> Satisfiability {
if window_is_enough {
Satisfiability::Yes
} else {
Satisfiability::Unknown
}
}
pub(crate) fn reads_document_root(schema: &Schema, definitions: &DefinitionMap) -> bool {
pointers_read(schema, definitions).contains(ROOT_DEFINITION_KEY)
}
fn document_slice(schema: &Schema, document: &Document) -> (Option<Schema>, DefinitionMap) {
let mut pointers = pointers_read(schema, &document.definitions);
let root = pointers.contains(ROOT_DEFINITION_KEY).then(|| {
pointers.extend(pointers_read(&document.root, &document.definitions));
document.root.clone()
});
let targets = pointers
.into_iter()
.filter_map(|uri| {
let body = document.definitions.get(uri.as_ref() as &str)?;
Some((uri, body.clone()))
})
.collect();
(root, targets)
}
#[derive(Clone, Debug, Eq)]
struct Document {
root: Schema,
definitions: Arc<DefinitionMap>,
local: Arc<BTreeSet<Arc<str>>>,
}
impl Document {
fn target(&self, uri: &str) -> Option<&Schema> {
if uri == ROOT_DEFINITION_KEY {
return Some(&self.root);
}
self.definitions.get(uri)
}
}
impl PartialEq for Document {
fn eq(&self, other: &Self) -> bool {
self.root == other.root
&& (Arc::ptr_eq(&self.definitions, &other.definitions)
|| self.definitions == other.definitions)
}
}
impl Ord for Document {
fn cmp(&self, other: &Self) -> Ordering {
self.definitions
.cmp(&other.definitions)
.then_with(|| self.root.cmp(&other.root))
}
}
impl PartialOrd for Document {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug)]
pub struct CanonicalSchema {
inner: Schema,
draft: Draft,
pattern_options: PatternEngineOptions,
validate_formats: bool,
document: Document,
}
impl PartialEq for CanonicalSchema {
fn eq(&self, other: &Self) -> bool {
self.draft == other.draft
&& self.validate_formats == other.validate_formats
&& self.pattern_options == other.pattern_options
&& self.inner == other.inner
&& self.reads_same_document_as(other)
}
}
impl Eq for CanonicalSchema {}
impl PartialOrd for CanonicalSchema {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CanonicalSchema {
fn cmp(&self, other: &Self) -> Ordering {
self.inner
.cmp(&other.inner)
.then_with(|| self.draft.cmp(&other.draft))
.then_with(|| self.validate_formats.cmp(&other.validate_formats))
.then_with(|| self.pattern_options.cmp(&other.pattern_options))
.then_with(|| {
if self.document == other.document || self.reads_no_document() {
return Ordering::Equal;
}
document_slice(&self.inner, &self.document)
.cmp(&document_slice(&other.inner, &other.document))
})
}
}
impl Hash for CanonicalSchema {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
self.draft.hash(state);
self.validate_formats.hash(state);
self.pattern_options.hash(state);
}
}
impl CanonicalSchema {
pub(crate) fn new(
inner: Schema,
draft: Draft,
pattern_options: PatternEngineOptions,
validate_formats: bool,
definitions: Arc<DefinitionMap>,
local: Arc<BTreeSet<Arc<str>>>,
) -> Self {
let document = Document {
root: inner.clone(),
definitions,
local,
};
Self {
inner,
draft,
pattern_options,
validate_formats,
document,
}
}
fn within(
inner: Schema,
draft: Draft,
pattern_options: PatternEngineOptions,
validate_formats: bool,
document: Document,
) -> Self {
Self {
inner,
draft,
pattern_options,
validate_formats,
document,
}
}
#[must_use]
pub fn to_json_schema(&self) -> Value {
if self.inner == self.document.root {
return emit::to_json_schema(&self.inner, self.draft, &self.document.definitions);
}
let definitions = emit::reachable_definitions(
&self.inner,
&self.document.root,
&self.document.definitions,
);
let value = emit::to_json_schema(&self.inner, self.draft, &definitions);
emit::rebind_document_root(value, &self.document.root, self.draft)
}
#[must_use]
pub fn satisfiability(&self) -> Satisfiability {
let mut node = &self.inner;
let mut walked: Vec<&Arc<str>> = Vec::new();
while let SchemaKind::Reference(uri) = node.kind() {
if walked.contains(&uri) {
return Satisfiability::Unknown;
}
let target = if uri.as_ref() == ROOT_DEFINITION_KEY {
&self.document.root
} else {
match self.document.definitions.get(uri.as_ref() as &str) {
Some(target) => target,
None => return Satisfiability::Unknown,
}
};
walked.push(uri);
node = target;
}
let answer = match node.kind() {
SchemaKind::False => Satisfiability::No,
SchemaKind::True
| SchemaKind::Const(_)
| SchemaKind::Enum(_)
| SchemaKind::MultiType(_) => Satisfiability::Yes,
SchemaKind::TypedGroup { ty, body } => {
debug_assert!(
body.kind()
.finite_values()
.is_some_and(|values| values.iter().any(|value| value.json_type() == *ty)),
"a typed group holds a value of its own type"
);
Satisfiability::Yes
}
SchemaKind::String(leaf) => {
let leaf = leaf.get();
holds_a_value(
leaf.patterns.is_empty()
&& leaf.excluded_patterns.is_empty()
&& leaf.formats.is_empty()
&& leaf.excluded_formats.is_empty()
&& leaf.content_media_types.is_empty()
&& leaf.content_encodings.is_empty()
&& leaf.excluded.is_empty(),
)
}
SchemaKind::Array(leaf) => {
let leaf = leaf.get();
holds_a_value(
leaf.lengths.contains(&BoundCardinality::from(0))
&& leaf.contains.is_empty()
&& leaf.distinctness != Distinctness::SomeRepeated,
)
}
SchemaKind::Object(leaf) => {
let leaf = leaf.get();
holds_a_value(
leaf.sizes.contains(&BoundCardinality::from(0))
&& leaf.required.is_empty()
&& leaf.violations.is_empty(),
)
}
SchemaKind::Integer(_)
| SchemaKind::Number(_)
| SchemaKind::Not(_)
| SchemaKind::AllOf(_)
| SchemaKind::AnyOf(_)
| SchemaKind::OneOf(_)
| SchemaKind::Reference(_)
| SchemaKind::Raw(_) => Satisfiability::Unknown,
};
if answer != Satisfiability::Unknown {
return answer;
}
let context = self.context_reading(
&[&self.inner],
&[&self.document.root],
&self.document.definitions,
);
for candidate in candidate_instances(
node,
&self.document,
CANDIDATE_DEPTH,
&Cell::new(CANDIDATE_NODES),
&context,
) {
let (verdict, inexact) = context.probe(|| {
algebra::admits_value(node, &candidate, UncheckableFacet::Undecided, &context)
});
if verdict == Verdict::Admits && !inexact && !context.outgrew_distribution() {
return Satisfiability::Yes;
}
}
Satisfiability::Unknown
}
fn reads_no_document(&self) -> bool {
!algebra::contains_reference(&self.inner)
}
fn reads_same_document_as(&self, other: &Self) -> bool {
if self.document == other.document {
return true;
}
if self.reads_no_document() {
return true;
}
let mut pointers = pointers_read(&self.inner, &self.document.definitions);
if pointers.contains(ROOT_DEFINITION_KEY) {
if self.document.root != other.document.root {
return false;
}
pointers.extend(pointers_read(
&self.document.root,
&self.document.definitions,
));
}
pointers.iter().all(|uri| {
self.document.definitions.get(uri.as_ref() as &str)
== other.document.definitions.get(uri.as_ref() as &str)
})
}
#[must_use]
pub(crate) fn schema_kind(&self) -> &SchemaKind {
self.inner.kind()
}
#[must_use]
pub fn draft(&self) -> Draft {
self.draft
}
pub(crate) fn wrap_child(&self, child: &Schema) -> Self {
Self::within(
child.clone(),
self.draft,
self.pattern_options,
self.validate_formats,
self.document.clone(),
)
}
#[must_use]
pub fn definition(&self, uri: &str) -> Option<CanonicalSchema> {
if uri == ROOT_DEFINITION_KEY {
return Some(self.wrap_child(&self.document.root));
}
self.document
.definitions
.get(uri)
.map(|body| self.wrap_child(body))
}
#[must_use]
pub fn definitions(&self) -> impl ExactSizeIterator<Item = (String, CanonicalSchema)> + '_ {
self.document
.definitions
.iter()
.map(|(uri, body)| (uri.to_string(), self.wrap_child(body)))
}
pub fn intersect(&self, other: &Self) -> Result<Self, CanonicalizationError> {
self.combine(other, |left, right, ctx, _| {
match (left.kind(), right.kind()) {
_ if left == right => return Some(left.clone()),
(SchemaKind::True, _) | (_, SchemaKind::False) => return Some(right.clone()),
(SchemaKind::False, _) | (_, SchemaKind::True) => return Some(left.clone()),
_ => {}
}
Some(algebra::intersect(left.clone(), right.clone(), ctx))
})
}
pub fn union(&self, other: &Self) -> Result<Self, CanonicalizationError> {
self.combine(other, |left, right, ctx, _| {
match (left.kind(), right.kind()) {
_ if left == right => return Some(left.clone()),
(SchemaKind::False, _) | (_, SchemaKind::True) => return Some(right.clone()),
(SchemaKind::True, _) | (_, SchemaKind::False) => return Some(left.clone()),
_ => {}
}
Some(algebra::union(vec![left.clone(), right.clone()], ctx))
})
}
pub fn subtract(&self, other: &Self) -> Result<Self, CanonicalizationError> {
self.combine(other, |left, right, ctx, definitions| {
self.difference(left, right, ctx, definitions)
})
}
fn difference(
&self,
taken: &Schema,
removed: &Schema,
ctx: &CanonicalizationContext,
definitions: &DefinitionMap,
) -> Option<Schema> {
match (taken.kind(), removed.kind()) {
_ if taken == removed => return Some(Schema::falsy()),
(SchemaKind::False, _) | (_, SchemaKind::True) => return Some(Schema::falsy()),
(_, SchemaKind::False) => return Some(taken.clone()),
_ => {}
}
let (met, inexact) = ctx.probe(|| algebra::intersect(taken.clone(), removed.clone(), ctx));
if !inexact && matches!(met.kind(), SchemaKind::False) {
return Some(taken.clone());
}
if oracle::covers(removed, taken, ctx) == Verdict::Admits {
return Some(Schema::falsy());
}
if !algebra::uncheckable_string_facets(removed, ctx)
.is_subset(&algebra::uncheckable_string_facets(taken, ctx))
{
return None;
}
let same_document = self.document.definitions.as_ref() == definitions;
let (complement, inexact) =
ctx.probe(|| negate::negate_in_place(removed, definitions, ctx));
let complement = complement?;
if !same_document && reads_document_root(&complement, definitions) {
return None;
}
if inexact || ctx.outgrew_distribution() {
return None;
}
Some(algebra::intersect(taken.clone(), complement, ctx))
}
fn combine(
&self,
other: &Self,
op: impl FnOnce(&Schema, &Schema, &CanonicalizationContext, &DefinitionMap) -> Option<Schema>,
) -> Result<Self, CanonicalizationError> {
self.check_operands(other)?;
let merged = self.merged_definitions(other)?;
let definitions = &merged.definitions;
Self::check_document_roots(&merged, definitions)?;
let context = self.context_reading(
&[&merged.left, &merged.right],
&[&merged.left_root, &merged.right_root],
definitions,
);
let inner = op(&merged.left, &merged.right, &context, definitions)
.ok_or(CanonicalizationError::UnsupportedResult)?;
if context.outgrew_distribution() || context.saw_inexact_intersection() {
return Err(CanonicalizationError::UnsupportedResult);
}
let document = Self::combined_document(&merged, &inner, definitions);
Ok(Self::within(
inner,
self.draft,
self.pattern_options,
self.validate_formats,
document,
))
}
pub fn covers(&self, other: &Self) -> Result<Containment, CanonicalizationError> {
self.check_operands(other)?;
let merged = self.merged_definitions(other)?;
let definitions = &merged.definitions;
Self::check_document_roots(&merged, definitions)?;
let context = self.context_reading(
&[&merged.left, &merged.right],
&[&merged.left_root, &merged.right_root],
definitions,
);
let decided = |verdict: Verdict, decided: Containment| {
if context.saw_inexact_intersection() || context.outgrew_distribution() {
return Some(Containment::Unknown);
}
(verdict == Verdict::Admits).then_some(decided)
};
if let Some(answer) = decided(
oracle::covers(&merged.left, &merged.right, &context),
Containment::Yes,
) {
return Ok(answer);
}
if let Some(values) = merged.right.kind().finite_values() {
let (refuted, inexact) =
context.probe(|| {
Verdict::from_bool(values.iter().any(|value| {
algebra::rejects_value(&merged.left, value.as_value(), &context)
}))
});
let refuted = if inexact { Verdict::Unknown } else { refuted };
if let Some(answer) = decided(refuted, Containment::No) {
return Ok(answer);
}
}
let flipped = Combined {
definitions: Arc::clone(definitions),
left: merged.right.clone(),
right: merged.left.clone(),
left_root: merged.right_root.clone(),
right_root: merged.left_root.clone(),
local: Arc::clone(&merged.local),
};
let ((difference, inexact), outgrew) = context.capped(COVERS_DIFFERENCE_BUDGET, || {
context.probe(|| self.difference(&merged.right, &merged.left, &context, definitions))
});
let left_over = if inexact || outgrew || context.outgrew_distribution() {
Satisfiability::Unknown
} else {
difference.map_or(Satisfiability::Unknown, |difference| {
let document = Self::combined_document(&flipped, &difference, definitions);
Self::within(
difference,
self.draft,
self.pattern_options,
self.validate_formats,
document,
)
.satisfiability()
})
};
Ok(match left_over {
Satisfiability::Yes => Containment::No,
Satisfiability::No => Containment::Yes,
Satisfiability::Unknown => Containment::Unknown,
})
}
pub fn negate(&self) -> Result<Self, CanonicalizationError> {
if matches!(self.schema_kind(), SchemaKind::Raw(_)) {
return Err(CanonicalizationError::UnsupportedOperand);
}
let context = self.context_reading(
&[&self.inner],
&[&self.document.root],
&self.document.definitions,
);
let inner =
negate::negate_with_definitions(&self.inner, &self.document.definitions, &context)
.ok_or(CanonicalizationError::UnsupportedResult)?;
if context.outgrew_distribution() || context.saw_inexact_intersection() {
return Err(CanonicalizationError::UnsupportedResult);
}
let mut definitions = (*self.document.definitions).clone();
parse::prune_unreachable_definitions(&inner, &mut definitions);
let local = narrowed(&self.document.local, &definitions);
Ok(Self::new(
inner,
self.draft,
self.pattern_options,
self.validate_formats,
Arc::new(definitions),
local,
))
}
fn combined_document(
other: &Combined,
inner: &Schema,
definitions: &Arc<DefinitionMap>,
) -> Document {
let root = if !reads_document_root(inner, definitions) {
inner.clone()
} else if reads_document_root(&other.left, definitions) {
other.left_root.clone()
} else {
other.right_root.clone()
};
let mut retained = emit::reachable_definitions(inner, &root, definitions).into_owned();
if root != *inner {
retained.extend(
emit::reachable_definitions(&root, &root, definitions)
.iter()
.map(|(uri, body)| (Arc::clone(uri), body.clone())),
);
}
Document {
local: narrowed(&other.local, &retained),
definitions: Arc::new(retained),
root,
}
}
fn check_document_roots(
other: &Combined,
definitions: &Arc<DefinitionMap>,
) -> Result<(), CanonicalizationError> {
if other.left_root != other.right_root
&& reads_document_root(&other.left, definitions)
&& reads_document_root(&other.right, definitions)
{
return Err(CanonicalizationError::IncompatibleOperands(
OperandMismatch::DocumentRoots,
));
}
Ok(())
}
fn context_reading(
&self,
nodes: &[&Schema],
roots: &[&Schema],
definitions: &Arc<DefinitionMap>,
) -> CanonicalizationContext {
let mut reachable = BTreeSet::new();
for node in nodes {
reachable.extend(pointers_read(node, definitions));
}
if reachable.contains(ROOT_DEFINITION_KEY) {
for root in roots {
reachable.extend(pointers_read(root, definitions));
}
}
CanonicalizationContext::new(self.draft, self.pattern_options, self.validate_formats)
.resolving(
Arc::clone(definitions),
emptiness::cyclic_definition_keys(definitions, &reachable),
)
}
fn check_operands(&self, other: &Self) -> Result<(), CanonicalizationError> {
if matches!(self.schema_kind(), SchemaKind::Raw(_))
|| matches!(other.schema_kind(), SchemaKind::Raw(_))
{
return Err(CanonicalizationError::UnsupportedOperand);
}
let mismatch = if self.draft != other.draft {
OperandMismatch::Drafts {
left: self.draft,
right: other.draft,
}
} else if self.validate_formats != other.validate_formats {
OperandMismatch::FormatAssertions
} else if self.pattern_options != other.pattern_options {
OperandMismatch::PatternEngine
} else {
return Ok(());
};
Err(CanonicalizationError::IncompatibleOperands(mismatch))
}
fn merged_definitions(&self, other: &Self) -> Result<Combined, CanonicalizationError> {
let ours = &self.document.definitions;
let theirs = &other.document.definitions;
let mut combined = Combined {
definitions: Arc::clone(ours),
left: self.inner.clone(),
right: other.inner.clone(),
left_root: self.document.root.clone(),
right_root: other.document.root.clone(),
local: Arc::clone(&self.document.local),
};
if Arc::ptr_eq(ours, theirs) || theirs.is_empty() {
combined.local = united(&self.document.local, &other.document.local);
return Ok(combined);
}
if ours.is_empty() {
combined.definitions = Arc::clone(theirs);
combined.local = united(&self.document.local, &other.document.local);
return Ok(combined);
}
let Some(renames) =
rename::reconcile(ours, theirs, &self.document.local, &other.document.local)
else {
return Err(CanonicalizationError::IncompatibleOperands(
OperandMismatch::Definitions,
));
};
let (ours, theirs) = if renames.is_empty() {
(Arc::clone(ours), Arc::clone(theirs))
} else {
combined.left = rename::rename_references(&self.inner, &renames.left);
combined.right = rename::rename_references(&other.inner, &renames.right);
combined.left_root = rename::rename_references(&self.document.root, &renames.left);
combined.right_root = rename::rename_references(&other.document.root, &renames.right);
combined.local = Arc::new(rename::rename_keys(
&self.document.local,
&renames.left,
&other.document.local,
&renames.right,
));
(
Arc::new(rename::rename_definitions(ours, &renames.left)),
Arc::new(rename::rename_definitions(theirs, &renames.right)),
)
};
if renames.is_empty() {
combined.local = united(&self.document.local, &other.document.local);
}
combined.definitions = if theirs.keys().all(|uri| ours.contains_key(uri)) {
ours
} else {
let mut merged = (*ours).clone();
merged.extend(
theirs
.iter()
.map(|(uri, body)| (Arc::clone(uri), body.clone())),
);
Arc::new(merged)
};
Ok(combined)
}
}
fn narrowed(
local: &Arc<BTreeSet<Arc<str>>>,
definitions: &DefinitionMap,
) -> Arc<BTreeSet<Arc<str>>> {
if local.iter().all(|uri| definitions.contains_key(uri)) {
return Arc::clone(local);
}
Arc::new(
local
.iter()
.filter(|uri| definitions.contains_key(&***uri))
.map(Arc::clone)
.collect(),
)
}
fn united(
ours: &Arc<BTreeSet<Arc<str>>>,
theirs: &Arc<BTreeSet<Arc<str>>>,
) -> Arc<BTreeSet<Arc<str>>> {
if Arc::ptr_eq(ours, theirs) {
return Arc::clone(ours);
}
Arc::new(ours.union(theirs).map(Arc::clone).collect())
}
struct Combined {
definitions: Arc<DefinitionMap>,
left: Schema,
right: Schema,
left_root: Schema,
right_root: Schema,
local: Arc<BTreeSet<Arc<str>>>,
}