use std::sync::Arc;
use referencing::Draft;
use serde_json::Value;
use crate::{
canonical::{
algebra,
context::CanonicalizationContext,
ir::{
tighter, BoundCardinality, BoundInteger, CanonicalJson, IntegerBounds, LengthBounds,
Schema, SchemaKind, StringLeaf,
},
CanonicalizationError,
},
JsonType, JsonTypeSet,
};
pub(crate) fn parse(
value: &Value,
ctx: &CanonicalizationContext,
) -> Result<Option<Schema>, CanonicalizationError> {
parse_schema(value, ctx, true)
}
fn parse_schema(
value: &Value,
ctx: &CanonicalizationContext,
is_root: bool,
) -> 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,
_ => return Ok(None),
};
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 patterns: Vec<Arc<str>> = Vec::new();
let mut minimum: Option<BoundInteger> = None;
let mut maximum: Option<BoundInteger> = None;
let mut draft4_exclusive_minimum = false;
let mut draft4_exclusive_maximum = false;
let mut conjuncts: Vec<Schema> = Vec::new();
for (key, entry) in map {
match (key.as_str(), entry) {
("$schema", _) if !is_root => return Ok(None),
("$schema", Value::String(uri)) => {
if matches!(Draft::from_schema_uri(uri), Draft::Unknown) {
return Ok(None);
}
}
("allOf", Value::Array(branches)) => {
for branch in branches {
match parse_schema(branch, ctx, false)? {
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)? {
Some(schema) => branches.push(schema),
None => return Ok(None),
}
}
conjuncts.push(algebra::union(branches, ctx));
}
("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),
}
}
("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(),
});
}
patterns.push(pattern);
}
("minimum", Value::Number(number)) if ctx.draft().is_known_keyword("minimum") => {
match BoundInteger::from_number(number) {
Some(bound) => minimum = tighter(minimum, Some(bound), Ord::max),
None => return Ok(None),
}
}
("maximum", Value::Number(number)) if ctx.draft().is_known_keyword("maximum") => {
match BoundInteger::from_number(number) {
Some(bound) => maximum = tighter(maximum, Some(bound), Ord::min),
None => return Ok(None),
}
}
("exclusiveMinimum", Value::Number(number))
if !matches!(ctx.draft(), Draft::Draft4)
&& ctx.draft().is_known_keyword("exclusiveMinimum") =>
{
match BoundInteger::from_number(number).and_then(BoundInteger::checked_increment) {
Some(bound) => minimum = tighter(minimum, Some(bound), Ord::max),
None => return Ok(None),
}
}
("exclusiveMaximum", Value::Number(number))
if !matches!(ctx.draft(), Draft::Draft4)
&& ctx.draft().is_known_keyword("exclusiveMaximum") =>
{
match BoundInteger::from_number(number).and_then(BoundInteger::checked_decrement) {
Some(bound) => maximum = tighter(maximum, Some(bound), Ord::min),
None => return Ok(None),
}
}
("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;
}
(other, _) if ctx.draft().is_known_keyword(other) => return Ok(None),
_ => {}
}
}
if draft4_exclusive_minimum {
if let Some(bound) = minimum.take() {
match bound.checked_increment() {
Some(next) => minimum = Some(next),
None => return Ok(None),
}
}
}
if draft4_exclusive_maximum {
if let Some(bound) = maximum.take() {
match bound.checked_decrement() {
Some(next) => maximum = Some(next),
None => return Ok(None),
}
}
}
if matches!(ctx.draft(), Draft::Draft4)
&& (enum_values.is_some() || const_value.is_some())
&& type_set.is_some_and(|set| {
set.contains(JsonType::Integer) && set != JsonTypeSet::from(JsonType::Integer)
})
{
return Ok(None);
}
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() {
patterns.sort();
patterns.dedup();
let leaf = StringLeaf {
lengths: LengthBounds {
minimum: min_length,
maximum: max_length,
},
patterns,
};
conjuncts.push(string_facet_schema(leaf, ctx));
}
if minimum.is_some() || maximum.is_some() {
if type_set == Some(JsonTypeSet::from(JsonType::Integer)) {
conjuncts.push(algebra::integer_leaf(
IntegerBounds { minimum, maximum },
ctx,
));
} else {
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 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(Schema::new(SchemaKind::TypedGroup {
ty: JsonType::Integer,
body: 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),
_ => 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()?))
}),
_ => None,
}
}
pub(crate) fn type_set_schema(set: JsonTypeSet) -> Schema {
let set = SchemaKind::canonical_type_set(set);
if SchemaKind::semantic_cover(set) == JsonTypeSet::all() {
return Schema::new(SchemaKind::True);
}
if set == JsonTypeSet::from(JsonType::Null) {
return Schema::new(SchemaKind::Const(CanonicalJson::from_value(&Value::Null)));
}
if set == JsonTypeSet::from(JsonType::Boolean) {
return Schema::new(SchemaKind::Enum(vec![
CanonicalJson::from_value(&Value::Bool(false)),
CanonicalJson::from_value(&Value::Bool(true)),
]));
}
Schema::new(SchemaKind::MultiType(set))
}
pub(crate) fn canonicalize_value_set(mut members: Vec<CanonicalJson>) -> Schema {
members.sort();
members.dedup();
match members.len() {
0 => Schema::new(SchemaKind::False),
1 => Schema::new(SchemaKind::Const(
members.into_iter().next().expect("len == 1"),
)),
_ => {
if let Some(type_set) = SchemaKind::finite_values_saturated_domain(&members) {
if type_set.len() >= 2 {
return Schema::new(SchemaKind::MultiType(type_set));
}
}
Schema::new(SchemaKind::Enum(members))
}
}
}
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)
}
fn keeps_draft4_integer_guard(set: JsonTypeSet, draft: Draft) -> bool {
matches!(draft, Draft::Draft4)
&& set.contains(JsonType::Integer)
&& !set.contains(JsonType::Number)
}