use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
use ahash::{AHashMap, AHashSet};
use referencing::{Draft, Resolver};
use serde_json::Value;
use crate::{
canonical::{
algebra,
context::CanonicalizationContext,
emptiness,
ir::{
canonicalize_value_set, type_set_schema, typed_group, ArrayLeaf, BoundCardinality,
BoundNumber, BoundRational, CanonicalJson, ContainsFacet, Divisors, IntegerLeaf,
LengthBounds, NumberLeaf, ObjectLeaf, Schema, SchemaKind, Side, StringLeaf,
},
negate, CanonicalizationError, DefinitionMap, CANONICAL_REFERENCE_PREFIX,
ROOT_DEFINITION_KEY,
},
JsonType, JsonTypeSet,
};
pub(crate) struct ParseOutput {
pub(crate) root: Schema,
pub(crate) definitions: DefinitionMap,
pub(crate) has_references: bool,
}
pub(crate) fn parse<'a>(
value: &'a Value,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
) -> Result<Option<ParseOutput>, CanonicalizationError> {
parse_inner(value, ctx, resolver, None, Pruning::Prune)
}
pub(crate) fn parse_with_empty<'a>(
value: &'a Value,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
assumed_empty: &'a AHashSet<Arc<str>>,
) -> Result<Option<ParseOutput>, CanonicalizationError> {
parse_inner(value, ctx, resolver, Some(assumed_empty), Pruning::Prune)
}
pub(crate) fn parse_hypothesis<'a>(
value: &'a Value,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
assumed_empty: &'a AHashSet<Arc<str>>,
) -> Result<Option<ParseOutput>, CanonicalizationError> {
parse_inner(value, ctx, resolver, Some(assumed_empty), Pruning::Keep)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Pruning {
Prune,
Keep,
}
fn parse_inner<'a>(
value: &'a Value,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
assumed_empty: Option<&AHashSet<Arc<str>>>,
pruning: Pruning,
) -> Result<Option<ParseOutput>, CanonicalizationError> {
let mut folded: AHashSet<Arc<str>> = assumed_empty.cloned().unwrap_or_default();
let mut tracks = spells_dynamic_reference(value, ctx.draft());
loop {
let attempt = parse_once(value, ctx, resolver, &folded, pruning, tracks)?;
if attempt.needs_dynamic_scope {
debug_assert!(!tracks, "a tracked parse never requests tracking");
tracks = true;
continue;
}
let Some(parsed) = attempt.output else {
return Ok(None);
};
let mut grew = false;
for (key, body) in &parsed.definitions {
if matches!(body.kind(), SchemaKind::False) {
grew |= folded.insert(Arc::clone(key));
}
}
if !grew {
return Ok(Some(parsed));
}
}
}
struct DocumentParse {
output: Option<ParseOutput>,
needs_dynamic_scope: bool,
}
fn parse_once<'a>(
value: &'a Value,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
assumed_empty: &AHashSet<Arc<str>>,
pruning: Pruning,
tracks_dynamic_scope: bool,
) -> Result<DocumentParse, CanonicalizationError> {
let mut state = ParseState::new(value, resolver.base_uri().as_str(), assumed_empty);
state.facts.merges_object_leaves =
matches!(ctx.draft(), Draft::Draft4) && merges_object_leaves(value);
if !tracks_dynamic_scope {
let mut spelling_scans = AHashMap::default();
spelling_scans.insert(Arc::clone(&state.root_base_uri), false);
state.dynamic_scope = DynamicScope::Untracked {
spelling_scans,
needs_tracking: false,
};
}
let parsed = parse_schema_in_scope(value, ctx, true, resolver, &mut state)?;
if state.facts.additional_schema && state.facts.pattern_properties {
return Ok(DocumentParse {
output: None,
needs_dynamic_scope: state.dynamic_scope.needs_tracking(),
});
}
let Some(root) = parsed else {
return Ok(DocumentParse {
output: None,
needs_dynamic_scope: state.dynamic_scope.needs_tracking(),
});
};
let needs_dynamic_scope = state.dynamic_scope.needs_tracking();
if pruning == Pruning::Prune {
prune_unreachable_definitions(&root, &mut state.definitions);
}
Ok(DocumentParse {
output: Some(ParseOutput {
root,
definitions: state.definitions,
has_references: state.facts.has_references,
}),
needs_dynamic_scope,
})
}
struct ParseState<'a> {
root: &'a Value,
root_base_uri: Arc<str>,
facts: DocumentFacts,
definitions: DefinitionMap,
in_progress: AHashSet<Arc<str>>,
sources: AHashMap<Arc<str>, &'a Value>,
assumed_empty: &'a AHashSet<Arc<str>>,
dynamic_scope: DynamicScope,
}
#[derive(Default)]
#[allow(clippy::struct_excessive_bools)]
struct DocumentFacts {
additional_schema: bool,
pattern_properties: bool,
merges_object_leaves: bool,
has_references: bool,
}
enum DynamicScope {
Tracked,
Untracked {
spelling_scans: AHashMap<Arc<str>, bool>,
needs_tracking: bool,
},
}
impl DynamicScope {
fn tracked(&self) -> bool {
matches!(self, DynamicScope::Tracked)
}
fn needs_tracking(&self) -> bool {
matches!(
self,
DynamicScope::Untracked {
needs_tracking: true,
..
}
)
}
}
impl<'a> ParseState<'a> {
fn new(root: &'a Value, root_base_uri: &str, assumed_empty: &'a AHashSet<Arc<str>>) -> Self {
Self {
root,
root_base_uri: Arc::from(root_base_uri),
facts: DocumentFacts::default(),
definitions: DefinitionMap::new(),
in_progress: AHashSet::new(),
sources: AHashMap::default(),
assumed_empty,
dynamic_scope: DynamicScope::Tracked,
}
}
fn assumes_empty(&self, key: &str) -> bool {
self.assumed_empty.contains(key)
}
}
fn parse_schema<'a>(
value: &Value,
ctx: &CanonicalizationContext,
is_root: bool,
resolver: &Resolver<'a>,
state: &mut ParseState<'a>,
) -> Result<Option<Schema>, CanonicalizationError> {
let resolver = resolver.in_subresource(ctx.draft().create_resource_ref(value))?;
parse_schema_in_scope(value, ctx, is_root, &resolver, state)
}
type DynamicEnv = Arc<[(Arc<str>, Arc<str>)]>;
const RECURSIVE_ANCHOR_NAME: &str = "$recursiveAnchor";
fn empty_environment() -> DynamicEnv {
Arc::from(Vec::new())
}
fn dynamic_scope_digest(
resolver: &Resolver<'_>,
draft: Draft,
) -> Result<DynamicEnv, CanonicalizationError> {
if matches!(draft, Draft::Draft201909) {
return recursive_chain_digest(resolver);
}
dynamic_anchor_digest(resolver)
}
fn dynamic_anchor_digest(resolver: &Resolver<'_>) -> Result<DynamicEnv, CanonicalizationError> {
let mut bindings: Vec<(Arc<str>, Arc<str>)> = Vec::new();
for uri in &resolver.dynamic_scope() {
let (contents, _, resource_draft) = resolver.lookup(uri.as_str())?.into_inner();
let mut names = Vec::new();
resource_dynamic_anchor_names(contents, resource_draft, &mut names);
for name in names {
let resource: Arc<str> = Arc::from(uri.as_str());
match bindings.iter_mut().find(|(bound, _)| *bound == name) {
Some(binding) => binding.1 = resource,
None => bindings.push((name, resource)),
}
}
}
if bindings.is_empty() {
return Ok(empty_environment());
}
bindings.sort_by(|left, right| left.0.cmp(&right.0));
Ok(Arc::from(bindings))
}
fn resource_dynamic_anchor_names(contents: &Value, draft: Draft, names: &mut Vec<Arc<str>>) {
if let Some(name) = contents
.as_object()
.and_then(|map| map.get("$dynamicAnchor"))
.and_then(Value::as_str)
{
names.push(Arc::from(name));
}
for subresource in draft.subresources_of(contents) {
if draft.create_resource_ref(subresource).id().is_none() {
resource_dynamic_anchor_names(subresource, draft, names);
}
}
}
fn recursive_chain_digest(resolver: &Resolver<'_>) -> Result<DynamicEnv, CanonicalizationError> {
let mut landing: Option<Arc<str>> = None;
for uri in &resolver.dynamic_scope() {
let (contents, _, _) = resolver.lookup(uri.as_str())?.into_inner();
if resource_root_has_recursive_anchor(contents) {
landing = Some(Arc::from(uri.as_str()));
} else {
break;
}
}
match landing {
Some(resource) => Ok(Arc::from(vec![(
Arc::from(RECURSIVE_ANCHOR_NAME),
resource,
)])),
None => Ok(empty_environment()),
}
}
fn resource_root_has_recursive_anchor(contents: &Value) -> bool {
contents
.as_object()
.and_then(|map| map.get("$recursiveAnchor"))
.and_then(Value::as_bool)
== Some(true)
}
fn spells_dynamic_reference(value: &Value, draft: Draft) -> bool {
if matches!(draft, Draft::Draft201909) {
return spells_key(value, "$recursiveRef");
}
if draft.is_known_keyword("$dynamicRef") {
return spells_key(value, "$dynamicRef");
}
false
}
fn spells_key(value: &Value, keyword: &str) -> bool {
match value {
Value::Object(map) => {
map.get(keyword).is_some_and(Value::is_string)
|| map.values().any(|entry| spells_key(entry, keyword))
}
Value::Array(items) => items.iter().any(|item| spells_key(item, keyword)),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => false,
}
}
fn specialized_key(key: &Arc<str>, env: &DynamicEnv) -> Arc<str> {
if env.is_empty() {
return Arc::clone(key);
}
let decoded = match key.strip_prefix(CANONICAL_REFERENCE_PREFIX) {
Some(encoded) => percent_encoding::percent_decode_str(encoded)
.decode_utf8_lossy()
.into_owned(),
None => key.to_string(),
};
let mut spelled = match Cow::from(percent_encoding::utf8_percent_encode(
&decoded,
SPECIALIZATION_COMPONENT,
)) {
Cow::Borrowed(_) => decoded,
Cow::Owned(escaped) => escaped,
};
for (name, resource) in env.iter() {
spelled.push_str("|dyn=");
spelled.extend(percent_encoding::utf8_percent_encode(
name,
SPECIALIZATION_COMPONENT,
));
spelled.push('@');
spelled.extend(percent_encoding::utf8_percent_encode(
resource,
SPECIALIZATION_COMPONENT,
));
}
let spelled =
percent_encoding::utf8_percent_encode(&spelled, percent_encoding::NON_ALPHANUMERIC);
let uri = format!("{CANONICAL_REFERENCE_PREFIX}{spelled}");
let uri = referencing::uri::from_str(&uri).expect("a percent-encoded canonical URI is valid");
Arc::from(uri.as_str())
}
const SPECIALIZATION_COMPONENT: &percent_encoding::AsciiSet =
&percent_encoding::CONTROLS.add(b'%').add(b'|').add(b'@');
fn parse_schema_in_scope<'a>(
value: &Value,
ctx: &CanonicalizationContext,
is_root: bool,
resolver: &Resolver<'a>,
state: &mut ParseState<'a>,
) -> Result<Option<Schema>, CanonicalizationError> {
let map = match value {
Value::Bool(true) => return Ok(Some(Schema::new(SchemaKind::True))),
Value::Bool(false) => return Ok(Some(Schema::new(SchemaKind::False))),
Value::Object(map) => map,
Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) => return Ok(None),
};
if has_unevaluated(map, ctx.draft()) {
let Some(degraded) = degrade_unevaluated(map, ctx.draft()) else {
return Ok(None);
};
return parse_schema_in_scope(°raded, ctx, is_root, resolver, state);
}
let mut references: Vec<Schema> = Vec::new();
if let Some(reference) = map.get("$ref").and_then(Value::as_str) {
let Some(schema) = resolve_reference(reference, ctx, resolver, state)? else {
return Ok(None);
};
references.push(schema);
}
if ctx.draft().is_known_keyword("$dynamicRef") {
if let Some(reference) = map.get("$dynamicRef").and_then(Value::as_str) {
debug_assert!(
state.dynamic_scope.tracked(),
"a dynamic reference is only resolved with the dynamic scope tracked"
);
let Some(schema) = resolve_reference(reference, ctx, resolver, state)? else {
return Ok(None);
};
references.push(schema);
}
}
if matches!(ctx.draft(), Draft::Draft201909) {
if let Some(reference) = map.get("$recursiveRef").and_then(Value::as_str) {
debug_assert!(
state.dynamic_scope.tracked(),
"a recursive reference is only resolved with the dynamic scope tracked"
);
if reference != "#" {
return Ok(None);
}
let Some(schema) = resolve_recursive_reference(ctx, resolver, state)? else {
return Ok(None);
};
references.push(schema);
}
}
if let Some(combined) = combine_references(references, ctx) {
if matches!(ctx.draft(), Draft::Draft4 | Draft::Draft6 | Draft::Draft7) {
return Ok(Some(combined));
}
if !ref_has_assertion_siblings(map, ctx.draft()) {
return Ok(Some(combined));
}
let mut siblings = map.clone();
siblings.remove("$ref");
siblings.remove("$dynamicRef");
siblings.remove("$recursiveRef");
siblings.remove("$id");
siblings.remove("id");
return Ok(
parse_schema(&Value::Object(siblings), ctx, is_root, resolver, state)?
.map(|siblings| algebra::intersect(combined, siblings, ctx)),
);
}
let mut type_set = None;
let mut enum_values = None;
let mut const_value = None;
let mut min_length: Option<BoundCardinality> = None;
let mut max_length: Option<BoundCardinality> = None;
let mut unique_items = false;
let mut min_items: Option<BoundCardinality> = None;
let mut max_items: Option<BoundCardinality> = None;
let mut items: Option<Schema> = None;
let mut contains_schema: Option<Schema> = None;
let mut min_contains: Option<BoundCardinality> = None;
let mut max_contains: Option<BoundCardinality> = None;
let mut item_prefix: Option<Vec<Schema>> = None;
let mut additional_items: Option<&Value> = None;
let mut required: Vec<Arc<str>> = Vec::new();
let mut property_names: Option<Schema> = None;
let mut properties: BTreeMap<Arc<str>, Schema> = BTreeMap::new();
let mut pattern_properties: BTreeMap<Arc<str>, Schema> = BTreeMap::new();
let mut forbid_unmatched_keys = false;
let mut additional_schema: Option<Schema> = None;
let mut min_properties: Option<BoundCardinality> = None;
let mut max_properties: Option<BoundCardinality> = None;
let mut patterns: Vec<Arc<str>> = Vec::new();
let mut formats: Vec<Arc<str>> = Vec::new();
let mut content_media_types: Vec<Arc<str>> = Vec::new();
let mut content_encodings: Vec<Arc<str>> = Vec::new();
let mut multiple_of = Divisors::default();
let mut real_minimum: Option<BoundNumber> = None;
let mut real_maximum: Option<BoundNumber> = None;
let mut draft4_exclusive_minimum = false;
let mut draft4_exclusive_maximum = false;
let mut if_schema: Option<Schema> = None;
let mut then_schema: Option<Schema> = None;
let mut else_schema: Option<Schema> = None;
let mut conjuncts: Vec<Schema> = Vec::new();
for (key, entry) in map {
match (key.as_str(), entry) {
("$schema", Value::String(uri)) => {
let declared = Draft::from_schema_uri(uri);
if matches!(declared, Draft::Unknown) || (!is_root && declared != ctx.draft()) {
return Ok(None);
}
}
("$id" | "id" | "$anchor" | "$dynamicAnchor" | "$recursiveRef", Value::String(_))
| ("$recursiveAnchor", Value::Bool(_))
| ("$defs" | "definitions", Value::Object(_)) => {}
("allOf", Value::Array(branches)) => {
for branch in branches {
match parse_schema(branch, ctx, false, resolver, state)? {
Some(schema) => conjuncts.push(schema),
None => return Ok(None),
}
}
}
("anyOf", Value::Array(items)) => {
let mut branches = Vec::new();
for branch in items {
match parse_schema(branch, ctx, false, resolver, state)? {
Some(schema) => branches.push(schema),
None => return Ok(None),
}
}
conjuncts.push(algebra::union(branches, ctx));
}
("oneOf", Value::Array(items)) => {
let mut branches = Vec::new();
for branch in items {
match parse_schema(branch, ctx, false, resolver, state)? {
Some(schema) => branches.push(schema),
None => return Ok(None),
}
}
match algebra::one_of(branches, ctx) {
Some(schema) => conjuncts.push(schema),
None => return Ok(None),
}
}
("type", value) => match parse_type_set(value) {
Some(set) => type_set = Some(set),
None => return Ok(None),
},
("enum", Value::Array(values)) if ctx.draft().is_known_keyword("enum") => {
if !values.iter().all(finite_value_spelling_is_exact) {
return Ok(None);
}
enum_values = Some(values);
}
("const", value) if ctx.draft().is_known_keyword("const") => {
if !finite_value_spelling_is_exact(value) {
return Ok(None);
}
const_value = Some(value);
}
("minLength", Value::Number(number)) if ctx.draft().is_known_keyword("minLength") => {
match BoundCardinality::from_number(number) {
Some(bound) => min_length = Some(bound),
None => return Ok(None),
}
}
("maxLength", Value::Number(number)) if ctx.draft().is_known_keyword("maxLength") => {
match BoundCardinality::from_number(number) {
Some(bound) => max_length = Some(bound),
None => return Ok(None),
}
}
("uniqueItems", Value::Bool(flag)) if ctx.draft().is_known_keyword("uniqueItems") => {
unique_items = *flag;
}
("minItems", Value::Number(number)) if ctx.draft().is_known_keyword("minItems") => {
match BoundCardinality::from_number(number) {
Some(bound) => min_items = Some(bound),
None => return Ok(None),
}
}
("maxItems", Value::Number(number)) if ctx.draft().is_known_keyword("maxItems") => {
match BoundCardinality::from_number(number) {
Some(bound) => max_items = Some(bound),
None => return Ok(None),
}
}
("items", value @ (Value::Object(_) | Value::Bool(_)))
if ctx.draft().is_known_keyword("items") =>
{
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => items = Some(schema),
None => return Ok(None),
}
}
("prefixItems", Value::Array(schemas))
if ctx.draft().is_known_keyword("prefixItems") =>
{
match parse_prefix(schemas, ctx, resolver, state)? {
Some(prefix) => item_prefix = Some(prefix),
None => return Ok(None),
}
}
("items", Value::Array(schemas))
if matches!(
ctx.draft(),
Draft::Draft4 | Draft::Draft6 | Draft::Draft7 | Draft::Draft201909
) =>
{
match parse_prefix(schemas, ctx, resolver, state)? {
Some(prefix) => item_prefix = Some(prefix),
None => return Ok(None),
}
}
("additionalItems", value @ (Value::Object(_) | Value::Bool(_)))
if matches!(
ctx.draft(),
Draft::Draft4 | Draft::Draft6 | Draft::Draft7 | Draft::Draft201909
) =>
{
additional_items = Some(value);
}
("contains", value @ (Value::Object(_) | Value::Bool(_)))
if ctx.draft().is_known_keyword("contains") =>
{
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => contains_schema = Some(schema),
None => return Ok(None),
}
}
("minContains", Value::Number(number))
if ctx.draft().is_known_keyword("minContains") =>
{
match BoundCardinality::from_number(number) {
Some(bound) => min_contains = Some(bound),
None => return Ok(None),
}
}
("maxContains", Value::Number(number))
if ctx.draft().is_known_keyword("maxContains") =>
{
match BoundCardinality::from_number(number) {
Some(bound) => max_contains = Some(bound),
None => return Ok(None),
}
}
("required", Value::Array(names))
if ctx.draft().is_known_keyword("required")
&& names.iter().all(Value::is_string) =>
{
required.extend(names.iter().filter_map(Value::as_str).map(Arc::from));
}
("properties", Value::Object(entries))
if ctx.draft().is_known_keyword("properties") =>
{
for (key, value) in entries {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => {
properties.insert(Arc::from(key.as_str()), schema);
}
None => return Ok(None),
}
}
}
("patternProperties", Value::Object(entries))
if ctx.draft().is_known_keyword("patternProperties") =>
{
for (pattern, value) in entries {
let pattern: Arc<str> = Arc::from(pattern.as_str());
if ctx.compile_regex(&pattern).is_none() {
return Err(CanonicalizationError::InvalidPattern {
pattern: pattern.to_string(),
});
}
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => {
pattern_properties.insert(pattern, schema);
}
None => return Ok(None),
}
}
}
("propertyNames", value) if ctx.draft().is_known_keyword("propertyNames") => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => property_names = Some(schema),
None => return Ok(None),
}
}
("additionalProperties", value @ (Value::Object(_) | Value::Bool(_)))
if ctx.draft().is_known_keyword("additionalProperties") =>
{
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) if matches!(schema.kind(), SchemaKind::True) => {}
Some(schema) if matches!(schema.kind(), SchemaKind::False) => {
forbid_unmatched_keys = true;
}
Some(schema) => {
state.facts.additional_schema = true;
additional_schema = Some(schema);
}
None => return Ok(None),
}
}
("minProperties", Value::Number(number))
if ctx.draft().is_known_keyword("minProperties") =>
{
match BoundCardinality::from_number(number) {
Some(bound) => min_properties = Some(bound),
None => return Ok(None),
}
}
("maxProperties", Value::Number(number))
if ctx.draft().is_known_keyword("maxProperties") =>
{
match BoundCardinality::from_number(number) {
Some(bound) => max_properties = Some(bound),
None => return Ok(None),
}
}
("pattern", Value::String(text)) if ctx.draft().is_known_keyword("pattern") => {
let pattern: Arc<str> = Arc::from(text.as_str());
if ctx.compile_regex(&pattern).is_none() {
return Err(CanonicalizationError::InvalidPattern {
pattern: pattern.to_string(),
});
}
if !pattern.is_empty() {
patterns.push(pattern);
}
}
("format", Value::String(name)) if ctx.draft().is_known_keyword("format") => {
if ctx.validate_formats() {
formats.push(Arc::from(name.as_str()));
}
}
("contentEncoding" | "contentMediaType" | "contentSchema", _)
if matches!(
ctx.draft(),
Draft::Draft201909 | Draft::Draft202012 | Draft::Unknown
) => {}
("contentMediaType", Value::String(name))
if matches!(ctx.draft(), Draft::Draft6 | Draft::Draft7)
&& !map.contains_key("contentEncoding") =>
{
content_media_types.push(Arc::from(name.as_str()));
}
("contentEncoding", Value::String(name))
if matches!(ctx.draft(), Draft::Draft6 | Draft::Draft7)
&& !map.contains_key("contentMediaType") =>
{
content_encodings.push(Arc::from(name.as_str()));
}
("multipleOf", Value::Number(number)) if ctx.draft().is_known_keyword("multipleOf") => {
match BoundRational::new(number) {
Some(step) => multiple_of = Divisors::one(step),
None => return Ok(None),
}
}
("minimum", Value::Number(number)) if ctx.draft().is_known_keyword("minimum") => {
real_minimum = tighter_real(real_minimum, number, true, Side::Lower);
}
("maximum", Value::Number(number)) if ctx.draft().is_known_keyword("maximum") => {
real_maximum = tighter_real(real_maximum, number, true, Side::Upper);
}
("exclusiveMinimum", Value::Number(number))
if !matches!(ctx.draft(), Draft::Draft4)
&& ctx.draft().is_known_keyword("exclusiveMinimum") =>
{
real_minimum = tighter_real(real_minimum, number, false, Side::Lower);
}
("exclusiveMaximum", Value::Number(number))
if !matches!(ctx.draft(), Draft::Draft4)
&& ctx.draft().is_known_keyword("exclusiveMaximum") =>
{
real_maximum = tighter_real(real_maximum, number, false, Side::Upper);
}
("exclusiveMinimum", Value::Bool(flag)) if matches!(ctx.draft(), Draft::Draft4) => {
draft4_exclusive_minimum = *flag;
}
("exclusiveMaximum", Value::Bool(flag)) if matches!(ctx.draft(), Draft::Draft4) => {
draft4_exclusive_maximum = *flag;
}
("if", value) if ctx.draft().is_known_keyword("if") => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => if_schema = Some(schema),
None => return Ok(None),
}
}
("then", value) if ctx.draft().is_known_keyword("then") => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => then_schema = Some(schema),
None => return Ok(None),
}
}
("else", value) if ctx.draft().is_known_keyword("else") => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => else_schema = Some(schema),
None => return Ok(None),
}
}
("dependencies", Value::Object(entries)) => {
for (key, entry) in entries {
match entry {
Value::Array(names) if names.iter().all(Value::is_string) => {
conjuncts.push(required_dependency(key, names, ctx));
}
value @ (Value::Object(_) | Value::Bool(_)) => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => {
conjuncts.push(schema_dependency(key, schema, ctx));
}
None => return Ok(None),
}
}
Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) => {
return Ok(None)
}
}
}
}
("dependentRequired", Value::Object(entries))
if ctx.draft().is_known_keyword("dependentRequired") =>
{
for (key, entry) in entries {
match entry {
Value::Array(names) if names.iter().all(Value::is_string) => {
conjuncts.push(required_dependency(key, names, ctx));
}
Value::Null
| Value::Bool(_)
| Value::Number(_)
| Value::String(_)
| Value::Array(_)
| Value::Object(_) => return Ok(None),
}
}
}
("dependentSchemas", Value::Object(entries))
if ctx.draft().is_known_keyword("dependentSchemas") =>
{
for (key, entry) in entries {
match entry {
value @ (Value::Object(_) | Value::Bool(_)) => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => {
conjuncts.push(schema_dependency(key, schema, ctx));
}
None => return Ok(None),
}
}
Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) => {
return Ok(None)
}
}
}
}
("not", Value::Object(inner))
if ctx.draft().is_known_keyword("not")
&& inner.len() == 1
&& inner.contains_key("not") =>
{
let body = inner
.get("not")
.expect("the double-complement guard found its body");
match parse_schema(body, ctx, false, resolver, state)? {
Some(schema) => conjuncts.push(schema),
None => return Ok(None),
}
}
("not", value) if ctx.draft().is_known_keyword("not") => {
match parse_schema(value, ctx, false, resolver, state)? {
Some(child) => match negate::negate(&child, ctx) {
Some(complement) => conjuncts.push(complement),
None => return Ok(None),
},
None => return Ok(None),
}
}
(other, _) if ctx.draft().is_known_keyword(other) => return Ok(None),
_ => {}
}
}
if draft4_exclusive_minimum {
real_minimum = real_minimum.map(BoundNumber::excluded);
}
if draft4_exclusive_maximum {
real_maximum = real_maximum.map(BoundNumber::excluded);
}
if min_length.as_ref().is_some_and(BoundCardinality::is_zero) {
min_length = None;
}
if min_length.is_some()
|| max_length.is_some()
|| !patterns.is_empty()
|| !formats.is_empty()
|| !content_media_types.is_empty()
|| !content_encodings.is_empty()
{
patterns.sort();
patterns.dedup();
formats.sort();
formats.dedup();
content_media_types.sort();
content_media_types.dedup();
content_encodings.sort();
content_encodings.dedup();
let leaf = StringLeaf {
lengths: LengthBounds {
minimum: min_length,
maximum: max_length,
},
patterns,
formats,
content_media_types,
content_encodings,
};
conjuncts.push(string_facet_schema(leaf, ctx));
}
if min_items.as_ref().is_some_and(BoundCardinality::is_zero) {
min_items = None;
}
let (prefix, tail) = match item_prefix {
Some(prefix)
if matches!(
ctx.draft(),
Draft::Draft4 | Draft::Draft6 | Draft::Draft7 | Draft::Draft201909
) =>
{
let tail = match additional_items {
Some(value) => match parse_schema(value, ctx, false, resolver, state)? {
Some(schema) => Some(schema),
None => return Ok(None),
},
None => None,
};
(prefix, tail)
}
Some(prefix) => (prefix, items),
None => (Vec::new(), items),
};
let contains: Vec<ContainsFacet> = contains_schema
.map(|schema| ContainsFacet {
schema,
minimum: min_contains,
maximum: max_contains,
})
.into_iter()
.collect();
if min_items.is_some()
|| max_items.is_some()
|| unique_items
|| !prefix.is_empty()
|| tail.is_some()
|| !contains.is_empty()
{
conjuncts.push(array_facet_schema(
ArrayLeaf {
lengths: LengthBounds {
minimum: min_items,
maximum: max_items,
},
unique: unique_items,
prefix,
items: tail,
contains,
},
ctx,
));
}
if min_properties
.as_ref()
.is_some_and(BoundCardinality::is_zero)
{
min_properties = None;
}
fold_finite_key_patterns(&mut pattern_properties, &mut properties, ctx);
if !pattern_properties.is_empty() {
state.facts.pattern_properties = true;
}
if forbid_unmatched_keys && !pattern_properties.is_empty() && state.facts.merges_object_leaves {
return Ok(None);
}
if forbid_unmatched_keys {
let mut allowed: Vec<Schema> =
Vec::with_capacity(properties.len() + pattern_properties.len());
for key in properties.keys() {
allowed.push(Schema::new(SchemaKind::Const(CanonicalJson::from_value(
&Value::String(key.to_string()),
))));
}
for pattern in pattern_properties.keys() {
allowed.push(algebra::string_leaf(
StringLeaf {
lengths: LengthBounds::default(),
patterns: vec![Arc::clone(pattern)],
formats: Vec::new(),
content_media_types: Vec::new(),
content_encodings: Vec::new(),
},
ctx,
));
}
let allowed = algebra::union(allowed, ctx);
property_names = Some(match property_names.take() {
Some(names) => algebra::intersect(names, allowed, ctx),
None => allowed,
});
}
if min_properties.is_some()
|| max_properties.is_some()
|| !required.is_empty()
|| property_names.is_some()
|| !properties.is_empty()
|| !pattern_properties.is_empty()
|| additional_schema.is_some()
{
required.sort();
conjuncts.push(object_facet_schema(
ObjectLeaf {
sizes: LengthBounds {
minimum: min_properties,
maximum: max_properties,
},
required,
property_names,
properties,
pattern_properties,
additional: additional_schema,
},
ctx,
));
}
if real_minimum.is_some() || real_maximum.is_some() || !multiple_of.is_empty() {
let leaf = NumberLeaf {
minimum: real_minimum,
maximum: real_maximum,
multiple_of,
};
let Some(bounds) = algebra::integer_bounds_within(&leaf) else {
return Ok(None);
};
if type_set == Some(JsonTypeSet::from(JsonType::Integer)) {
conjuncts.push(algebra::integer_leaf(
IntegerLeaf {
bounds,
multiple_of: leaf.multiple_of,
},
ctx,
));
} else {
conjuncts.push(number_facet_schema(leaf, ctx));
}
}
match (if_schema, then_schema, else_schema) {
(None, _, _) | (Some(_), None, None) => {}
(Some(condition), Some(then), None) => match negate::negate(&condition, ctx) {
Some(complement) => conjuncts.push(algebra::union(vec![complement, then], ctx)),
None => return Ok(None),
},
(Some(condition), None, Some(else_branch)) => {
conjuncts.push(algebra::union(vec![condition, else_branch], ctx));
}
(Some(condition), Some(then), Some(else_branch)) => match negate::negate(&condition, ctx) {
Some(complement) => {
let holds = algebra::intersect(condition, then, ctx);
let fails = algebra::intersect(complement, else_branch, ctx);
conjuncts.push(algebra::union(vec![holds, fails], ctx));
}
None => return Ok(None),
},
}
let base = match (type_set, admitted_values(enum_values, const_value)) {
(None, None) => Schema::new(SchemaKind::True),
(Some(set), None) => type_set_schema(set),
(None, Some(values)) => canonicalize_value_set(values),
(Some(set), Some(values)) => restrict_values_to_types(values, set, ctx),
};
Ok(Some(
conjuncts.into_iter().fold(base, |result, conjunct| {
algebra::intersect(result, conjunct, ctx)
}),
))
}
fn fold_finite_key_patterns(
pattern_properties: &mut BTreeMap<Arc<str>, Schema>,
properties: &mut BTreeMap<Arc<str>, Schema>,
ctx: &CanonicalizationContext,
) {
pattern_properties.retain(|pattern, schema| {
let Some(keys) = finite_pattern_keys(pattern) else {
return true;
};
for key in keys {
let merged = match properties.remove(&key) {
Some(existing) => algebra::intersect(existing, schema.clone(), ctx),
None => schema.clone(),
};
properties.insert(key, merged);
}
false
});
}
fn finite_pattern_keys(pattern: &str) -> Option<Vec<Arc<str>>> {
match jsonschema_regex::analyze_pattern(pattern)? {
jsonschema_regex::PatternAnalysis::Exact(key) => Some(vec![Arc::from(key.as_ref())]),
jsonschema_regex::PatternAnalysis::Alternation(keys) => {
Some(keys.iter().map(|key| Arc::from(key.as_str())).collect())
}
jsonschema_regex::PatternAnalysis::Prefix(_)
| jsonschema_regex::PatternAnalysis::NoWhitespace => None,
}
}
fn has_instance_dependent_applicator(map: &serde_json::Map<String, Value>) -> bool {
map.keys().any(|key| {
matches!(
key.as_str(),
"$dynamicRef"
| "$recursiveRef"
| "$ref"
| "anyOf"
| "dependencies"
| "dependentSchemas"
| "else"
| "if"
| "oneOf"
| "then"
)
})
}
#[derive(Default)]
struct PropertyCover {
everything: bool,
keys: Vec<String>,
patterns: Vec<String>,
}
fn hoist_cover(degraded: &mut serde_json::Map<String, Value>, cover: &PropertyCover) {
for (keyword, names) in [
("properties", &cover.keys),
("patternProperties", &cover.patterns),
] {
if names.is_empty() {
continue;
}
let Value::Object(entries) = degraded
.entry(keyword)
.or_insert_with(|| Value::Object(serde_json::Map::new()))
else {
continue;
};
for name in names {
entries.entry(name.as_str()).or_insert(Value::Bool(true));
}
}
}
fn property_cover(branch: &Value, cover: &mut PropertyCover) -> Option<()> {
let map = match branch {
Value::Bool(_) => return Some(()),
Value::Object(map) => map,
Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) => return None,
};
if has_instance_dependent_applicator(map) {
return None;
}
if map.contains_key("additionalProperties") || map.contains_key("unevaluatedProperties") {
cover.everything = true;
return Some(());
}
if let Some(Value::Object(properties)) = map.get("properties") {
cover.keys.extend(properties.keys().cloned());
}
if let Some(Value::Object(patterns)) = map.get("patternProperties") {
cover.patterns.extend(patterns.keys().cloned());
}
if let Some(Value::Array(nested)) = map.get("allOf") {
for branch in nested {
property_cover(branch, cover)?;
}
}
Some(())
}
#[derive(Default)]
struct ItemCover {
everything: bool,
prefix: usize,
}
fn pad_tuple(degraded: &mut serde_json::Map<String, Value>, prefix_items: bool, prefix: usize) {
if prefix == 0 {
return;
}
let keyword = if prefix_items { "prefixItems" } else { "items" };
let Value::Array(tuple) = degraded
.entry(keyword)
.or_insert_with(|| Value::Array(Vec::new()))
else {
return;
};
tuple.resize(tuple.len().max(prefix), Value::Bool(true));
}
fn item_cover(branch: &Value, draft: Draft, cover: &mut ItemCover) -> Option<()> {
let map = match branch {
Value::Bool(_) => return Some(()),
Value::Object(map) => map,
Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) => return None,
};
if has_instance_dependent_applicator(map) {
return None;
}
if map.contains_key("contains") {
return None;
}
if map.contains_key("unevaluatedItems") {
cover.everything = true;
return Some(());
}
let tuple_is_prefix_items = matches!(draft, Draft::Draft202012 | Draft::Unknown);
match map.get("items") {
Some(Value::Object(_) | Value::Bool(_)) => {
cover.everything = true;
return Some(());
}
Some(Value::Array(items)) if !tuple_is_prefix_items => {
cover.prefix = cover.prefix.max(items.len());
if map.contains_key("additionalItems") {
cover.everything = true;
return Some(());
}
}
Some(Value::Array(_) | Value::Null | Value::Number(_) | Value::String(_)) | None => {}
}
if tuple_is_prefix_items {
if let Some(Value::Array(prefix)) = map.get("prefixItems") {
cover.prefix = cover.prefix.max(prefix.len());
}
}
if let Some(Value::Array(nested)) = map.get("allOf") {
for branch in nested {
item_cover(branch, draft, cover)?;
}
}
Some(())
}
fn has_unevaluated(map: &serde_json::Map<String, Value>, draft: Draft) -> bool {
draft.is_known_keyword("unevaluatedProperties")
&& (map.contains_key("unevaluatedProperties") || map.contains_key("unevaluatedItems"))
}
fn degrade_unevaluated(map: &serde_json::Map<String, Value>, draft: Draft) -> Option<Value> {
if has_instance_dependent_applicator(map) {
return None;
}
let mut degraded = map.clone();
if let Some(value) = degraded
.remove("unevaluatedProperties")
.filter(|_| !degraded.contains_key("additionalProperties"))
{
if let Some(Value::Array(branches)) = map.get("allOf") {
let mut cover = PropertyCover::default();
for branch in branches {
property_cover(branch, &mut cover)?;
}
if !cover.everything {
hoist_cover(&mut degraded, &cover);
degraded.insert("additionalProperties".to_string(), value);
}
} else {
degraded.insert("additionalProperties".to_string(), value);
}
}
if let Some(value) = degraded.remove("unevaluatedItems") {
if map.contains_key("contains") {
return None;
}
let mut cover = ItemCover::default();
if let Some(Value::Array(branches)) = map.get("allOf") {
for branch in branches {
item_cover(branch, draft, &mut cover)?;
}
}
if !cover.everything {
let tuple_is_prefix_items = matches!(draft, Draft::Draft202012 | Draft::Unknown);
let tail = match (map.get("items"), tuple_is_prefix_items) {
(None, false) if cover.prefix > 0 => Some("additionalItems"),
(None, _) => Some("items"),
(Some(Value::Array(_)), false) => Some("additionalItems"),
(
Some(
Value::Object(_)
| Value::Bool(_)
| Value::Array(_)
| Value::Null
| Value::Number(_)
| Value::String(_),
),
_,
) => None,
};
if let Some(tail) = tail.filter(|tail| !degraded.contains_key(*tail)) {
pad_tuple(&mut degraded, tuple_is_prefix_items, cover.prefix);
degraded.insert(tail.to_string(), value);
}
}
}
Some(Value::Object(degraded))
}
fn ref_has_assertion_siblings(map: &serde_json::Map<String, Value>, draft: Draft) -> bool {
map.keys().any(|key| {
!matches!(
key.as_str(),
"$ref"
| "$dynamicRef"
| "$recursiveRef"
| "$schema"
| "$id"
| "id"
| "$anchor"
| "$dynamicAnchor"
| "$recursiveAnchor"
| "$defs"
| "definitions"
| "title"
| "description"
| "default"
| "examples"
) && draft.is_known_keyword(key)
})
}
fn combine_references(references: Vec<Schema>, ctx: &CanonicalizationContext) -> Option<Schema> {
let mut references = references.into_iter();
let first = references.next()?;
Some(references.fold(first, |left, right| algebra::intersect(left, right, ctx)))
}
fn resolve_recursive_reference<'a>(
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
state: &mut ParseState<'a>,
) -> Result<Option<Schema>, CanonicalizationError> {
let base_uri = resolver.base_uri();
let location = resolver.resolve_uri(&base_uri.borrow(), "#")?;
let resolved = resolver.lookup_recursive_ref()?;
reference_to_definition("#", location.as_str(), resolved, ctx, state)
}
fn resolve_reference<'a>(
reference: &str,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
state: &mut ParseState<'a>,
) -> Result<Option<Schema>, CanonicalizationError> {
state.facts.has_references = true;
let base_uri = resolver.base_uri();
let location = resolver.resolve_uri(&base_uri.borrow(), reference)?;
let resolved = resolver.lookup(reference)?;
reference_to_definition(reference, location.as_str(), resolved, ctx, state)
}
fn reference_to_definition<'a>(
reference: &str,
location: &str,
resolved: referencing::Resolved<'a>,
ctx: &CanonicalizationContext,
state: &mut ParseState<'a>,
) -> Result<Option<Schema>, CanonicalizationError> {
let (target, target_resolver, target_draft) = resolved.into_inner();
let env = if state.dynamic_scope.tracked() {
dynamic_scope_digest(&target_resolver, ctx.draft())?
} else {
empty_environment()
};
if std::ptr::eq(target, state.root)
&& env
.iter()
.all(|(_, resource)| *resource == state.root_base_uri)
{
if state.assumes_empty(ROOT_DEFINITION_KEY) {
return Ok(Some(Schema::new(SchemaKind::False)));
}
return Ok(Some(Schema::new(SchemaKind::Reference(Arc::from(
ROOT_DEFINITION_KEY,
)))));
}
if target_draft != ctx.draft() {
return Ok(None);
}
let raw_key = canonical_reference_uri(reference, location, &state.root_base_uri);
if !ensure_definition(&raw_key, target, ctx, &target_resolver, &env, state)? {
return Ok(None);
}
let key = specialized_key(&raw_key, &env);
debug_assert!(
state.definitions.contains_key(&key) || state.in_progress.contains(&key),
"a resolved reference target is complete or actively being canonicalized"
);
if state.assumes_empty(&key) {
return Ok(Some(Schema::new(SchemaKind::False)));
}
if !key.starts_with(CANONICAL_REFERENCE_PREFIX)
&& state
.definitions
.get(&key)
.is_some_and(|body| matches!(body.kind(), SchemaKind::False))
{
return Ok(Some(Schema::new(SchemaKind::False)));
}
Ok(Some(Schema::new(SchemaKind::Reference(key))))
}
fn canonical_reference_uri(reference: &str, location: &str, root_base_uri: &str) -> Arc<str> {
if let Some(uri) = canonical_definition_reference(reference) {
return uri;
}
if is_direct_definition_reference(reference)
&& resource_uri(location) == resource_uri(root_base_uri)
{
return Arc::from(reference);
}
if location.starts_with(CANONICAL_REFERENCE_PREFIX) {
return Arc::from(location);
}
let location =
percent_encoding::utf8_percent_encode(location, percent_encoding::NON_ALPHANUMERIC);
let uri = format!("{CANONICAL_REFERENCE_PREFIX}{location}");
let uri = referencing::uri::from_str(&uri).expect("a percent-encoded canonical URI is valid");
Arc::from(uri.as_str())
}
fn canonical_definition_reference(reference: &str) -> Option<Arc<str>> {
for prefix in ["#/$defs/", "#/definitions/"] {
let Some(encoded) = reference.strip_prefix(prefix) else {
continue;
};
let decoded = percent_encoding::percent_decode_str(encoded)
.decode_utf8()
.ok()?;
let uri = referencing::unescape_segment(&decoded);
if uri.starts_with(CANONICAL_REFERENCE_PREFIX) {
return Some(Arc::from(uri.as_ref()));
}
}
None
}
fn resource_uri(uri: &str) -> &str {
uri.split_once('#').map_or(uri, |(resource, _)| resource)
}
fn is_direct_definition_reference(reference: &str) -> bool {
for prefix in ["#/$defs/", "#/definitions/"] {
if let Some(name) = reference.strip_prefix(prefix) {
let Ok(decoded) = percent_encoding::percent_decode_str(name).decode_utf8() else {
return false;
};
return !decoded.is_empty() && !decoded.contains('/');
}
}
false
}
fn ensure_definition<'a>(
key: &Arc<str>,
target: &'a Value,
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
env: &DynamicEnv,
state: &mut ParseState<'a>,
) -> Result<bool, CanonicalizationError> {
if let DynamicScope::Untracked {
spelling_scans,
needs_tracking,
} = &mut state.dynamic_scope
{
let base = resolver.base_uri();
let spells = if let Some(&spells) = spelling_scans.get(base.as_str()) {
spells
} else {
let (contents, _, resource_draft) = resolver.lookup("#")?.into_inner();
let spells = spells_dynamic_reference(contents, resource_draft);
spelling_scans.insert(Arc::from(base.as_str()), spells);
spells
};
if spells {
*needs_tracking = true;
return Ok(false);
}
}
debug_assert!(
state.dynamic_scope.tracked() || env.is_empty(),
"an untracked parse keeps the digest empty"
);
let key = specialized_key(key, env);
if let Some(existing) = state.sources.get(&key) {
if !std::ptr::eq(*existing, target) {
return Ok(false);
}
}
if state.definitions.contains_key(&key) || state.in_progress.contains(&key) {
return Ok(true);
}
state.sources.insert(Arc::clone(&key), target);
let inserted = state.in_progress.insert(Arc::clone(&key));
debug_assert!(
inserted,
"a new definition target is not already in progress"
);
let parsed = parse_schema_in_scope(target, ctx, false, resolver, state);
let was_in_progress = state.in_progress.remove(&key);
debug_assert!(
was_in_progress,
"definition parsing balances its in-progress marker"
);
let Some(parsed) = parsed? else {
return Ok(false);
};
let previous = state.definitions.insert(key, parsed);
debug_assert!(
previous.is_none(),
"a canonical definition target is inserted once"
);
Ok(true)
}
fn prune_unreachable_definitions(root: &Schema, definitions: &mut DefinitionMap) {
let mut pending = Vec::new();
collect_live_definition_references(root, &mut pending);
let mut reachable = AHashSet::new();
while let Some(uri) = pending.pop() {
let Some((uri, schema)) = definitions.get_key_value(uri) else {
continue;
};
if reachable.insert(Arc::clone(uri)) {
collect_live_definition_references(schema, &mut pending);
}
}
drop(pending);
definitions.retain(|uri, _| reachable.contains(uri));
debug_assert!(
definitions.keys().all(|uri| reachable.contains(uri)),
"the retained definition map contains only targets reachable from the canonical root"
);
}
fn merges_object_leaves(value: &Value) -> bool {
match value {
Value::Object(map) => {
map.keys().any(|key| {
matches!(
key.as_str(),
"$dynamicRef"
| "$recursiveRef"
| "$ref"
| "allOf"
| "anyOf"
| "dependencies"
| "dependentSchemas"
| "else"
| "if"
| "not"
| "oneOf"
| "then"
)
}) || map.values().any(merges_object_leaves)
}
Value::Array(items) => items.iter().any(merges_object_leaves),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => false,
}
}
fn collect_live_definition_references<'a>(schema: &'a Schema, references: &mut Vec<&'a str>) {
let mut found = Vec::new();
emptiness::collect_classified_references(schema, emptiness::Position::InPlace, &mut found);
references.extend(found.into_iter().map(|(uri, _)| uri.as_ref()));
}
fn required_dependency(key: &str, names: &[Value], ctx: &CanonicalizationContext) -> Schema {
let mut required: Vec<Arc<str>> = names
.iter()
.filter_map(Value::as_str)
.map(Arc::from)
.collect();
required.push(Arc::from(key));
required.sort();
required.dedup();
dependency_conjunct(key, object_with_required(required, ctx), ctx)
}
fn schema_dependency(key: &str, schema: Schema, ctx: &CanonicalizationContext) -> Schema {
dependency_conjunct(key, schema, ctx)
}
fn dependency_conjunct(key: &str, consequent: Schema, ctx: &CanonicalizationContext) -> Schema {
let vacuous = type_set_schema(JsonTypeSet::all().remove(JsonType::Object));
let absent = algebra::object_leaf(
ObjectLeaf {
sizes: LengthBounds::default(),
required: Vec::new(),
property_names: None,
properties: BTreeMap::from([(Arc::from(key), Schema::new(SchemaKind::False))]),
pattern_properties: BTreeMap::new(),
additional: None,
},
ctx,
);
algebra::union(vec![vacuous, absent, consequent], ctx)
}
fn object_with_required(required: Vec<Arc<str>>, ctx: &CanonicalizationContext) -> Schema {
algebra::object_leaf(
ObjectLeaf {
sizes: LengthBounds::default(),
required,
property_names: None,
properties: BTreeMap::new(),
pattern_properties: BTreeMap::new(),
additional: None,
},
ctx,
)
}
fn admitted_values(
enum_values: Option<&Vec<Value>>,
const_value: Option<&Value>,
) -> Option<Vec<CanonicalJson>> {
let mut values: Option<Vec<CanonicalJson>> =
enum_values.map(|entries| entries.iter().map(CanonicalJson::from_value).collect());
if let Some(constant) = const_value {
let constant = CanonicalJson::from_value(constant);
values = Some(match values {
Some(members) => members
.into_iter()
.filter(|value| *value == constant)
.collect(),
None => vec![constant],
});
}
values
}
pub(crate) fn restrict_values_to_types(
values: Vec<CanonicalJson>,
set: JsonTypeSet,
ctx: &CanonicalizationContext,
) -> Schema {
let cover = SchemaKind::semantic_cover(set);
let filtered: Vec<CanonicalJson> = values
.into_iter()
.filter(|value| cover.contains(value.json_type()))
.collect();
if !keeps_draft4_integer_guard(set, ctx.draft()) {
return canonicalize_value_set(filtered);
}
let (integers, others): (Vec<_>, Vec<_>) = filtered
.into_iter()
.partition(|value| value.json_type() == JsonType::Integer);
let mut branches = Vec::new();
let integer_set = canonicalize_value_set(integers);
if !matches!(integer_set.kind(), SchemaKind::False) {
branches.push(typed_group(JsonType::Integer, integer_set));
}
let other_set = canonicalize_value_set(others);
if !matches!(other_set.kind(), SchemaKind::False) {
branches.push(other_set);
}
algebra::union(branches, ctx)
}
#[cfg(feature = "arbitrary-precision")]
fn finite_value_spelling_is_exact(value: &Value) -> bool {
match value {
Value::Number(number) => {
let canonical = crate::canonical::json::canonical_number(number.as_str());
let text = canonical.as_deref().unwrap_or(number.as_str());
!text.bytes().any(|byte| matches!(byte, b'e' | b'E'))
}
Value::Array(items) => items.iter().all(finite_value_spelling_is_exact),
Value::Object(map) => map.values().all(finite_value_spelling_is_exact),
Value::Null | Value::Bool(_) | Value::String(_) => true,
}
}
#[cfg(not(feature = "arbitrary-precision"))]
fn finite_value_spelling_is_exact(_value: &Value) -> bool {
true
}
fn parse_type_set(value: &Value) -> Option<JsonTypeSet> {
match value {
Value::String(name) => Some(JsonTypeSet::from(name.parse::<JsonType>().ok()?)),
Value::Array(names) => names.iter().try_fold(JsonTypeSet::empty(), |set, name| {
Some(set.insert(name.as_str()?.parse::<JsonType>().ok()?))
}),
Value::Null | Value::Bool(_) | Value::Number(_) | Value::Object(_) => None,
}
}
fn tighter_real(
current: Option<BoundNumber>,
limit: &serde_json::Number,
inclusive: bool,
side: Side,
) -> Option<BoundNumber> {
let bound = BoundNumber::new(limit, inclusive);
match current {
Some(current) if current.is_tighter_than(&bound, side) => Some(current),
_ => Some(bound),
}
}
fn number_facet_schema(leaf: NumberLeaf, ctx: &CanonicalizationContext) -> Schema {
let non_number = Schema::new(SchemaKind::MultiType(
JsonTypeSet::all()
.remove(JsonType::Number)
.remove(JsonType::Integer),
));
algebra::union(vec![non_number, algebra::number_leaf(leaf, ctx)], ctx)
}
fn string_facet_schema(leaf: StringLeaf, ctx: &CanonicalizationContext) -> Schema {
let non_string = Schema::new(SchemaKind::MultiType(
JsonTypeSet::all().remove(JsonType::String),
));
algebra::union(vec![non_string, algebra::string_leaf(leaf, ctx)], ctx)
}
fn parse_prefix<'a>(
schemas: &[Value],
ctx: &CanonicalizationContext,
resolver: &Resolver<'a>,
state: &mut ParseState<'a>,
) -> Result<Option<Vec<Schema>>, CanonicalizationError> {
let mut prefix = Vec::with_capacity(schemas.len());
for schema in schemas {
match parse_schema(schema, ctx, false, resolver, state)? {
Some(schema) => prefix.push(schema),
None => return Ok(None),
}
}
Ok(Some(prefix))
}
fn array_facet_schema(leaf: ArrayLeaf, ctx: &CanonicalizationContext) -> Schema {
let non_array = Schema::new(SchemaKind::MultiType(
JsonTypeSet::all().remove(JsonType::Array),
));
algebra::union(vec![non_array, algebra::array_leaf(leaf, ctx)], ctx)
}
fn object_facet_schema(leaf: ObjectLeaf, ctx: &CanonicalizationContext) -> Schema {
let non_object = Schema::new(SchemaKind::MultiType(
JsonTypeSet::all().remove(JsonType::Object),
));
algebra::union(vec![non_object, algebra::object_leaf(leaf, ctx)], ctx)
}
fn keeps_draft4_integer_guard(set: JsonTypeSet, draft: Draft) -> bool {
matches!(draft, Draft::Draft4)
&& set.contains(JsonType::Integer)
&& !set.contains(JsonType::Number)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn specialized_key_is_injective_for_delimiter_spelling_components() {
let key: Arc<str> = Arc::from("https://example.com/target");
let name_with_at: DynamicEnv =
Arc::from(vec![(Arc::<str>::from("a@r"), Arc::<str>::from("x"))]);
let resource_with_at: DynamicEnv =
Arc::from(vec![(Arc::<str>::from("a"), Arc::<str>::from("r@x"))]);
assert_ne!(
specialized_key(&key, &name_with_at),
specialized_key(&key, &resource_with_at)
);
let key_with_delimiter: Arc<str> = Arc::from("k|dyn=a@r");
let one_binding: DynamicEnv =
Arc::from(vec![(Arc::<str>::from("b"), Arc::<str>::from("s"))]);
let plain_key: Arc<str> = Arc::from("k");
let two_bindings: DynamicEnv = Arc::from(vec![
(Arc::<str>::from("a"), Arc::<str>::from("r")),
(Arc::<str>::from("b"), Arc::<str>::from("s")),
]);
assert_ne!(
specialized_key(&key_with_delimiter, &one_binding),
specialized_key(&plain_key, &two_bindings)
);
}
}