// Schema merging for `allOf` (upstream merge.rs). A merge that is
// unsatisfiable returns `None` (upstream `Err(())`); upstream panics
// (`unimplemented!`, failed assertions) raise `Panic`.
///|
/// Merge all schemas; an unsatisfiable result is `false`.
fn merge_all(
schemas : Array[@schema.Schema],
defs : Defs,
) -> @schema.Schema raise TypifyError {
match try_merge_all(schemas, defs) {
Some(s) => s
None => Bool(false)
}
}
///|
fn try_merge_all(
schemas : Array[@schema.Schema],
defs : Defs,
) -> @schema.Schema? raise TypifyError {
match schemas {
[] =>
raise panic_with(
"we should not be trying to merge an empty array of schemas",
)
[only] => Some(only)
[first, second, .. rest] => {
guard try_merge_schema(first, second, defs) is Some(out) else {
return None
}
let mut out = out
for schema in rest {
guard try_merge_schema(out, schema, defs) is Some(next) else {
return None
}
out = next
}
Some(out)
}
}
}
///|
fn merge_additional_items(
a : @schema.Schema?,
b : @schema.Schema?,
defs : Defs,
) -> @schema.Schema? raise TypifyError {
match (a, b) {
(None, None) => Some(Bool(true))
_ => merge_additional_properties(a, b, defs)
}
}
///|
fn merge_additional_properties(
a : @schema.Schema?,
b : @schema.Schema?,
defs : Defs,
) -> @schema.Schema? raise TypifyError {
match (a, b) {
(None, other) | (other, None) => other
(Some(aa), Some(bb)) => Some(merge_schema(aa, bb, defs))
}
}
///|
fn merge_schema(
a : @schema.Schema,
b : @schema.Schema,
defs : Defs,
) -> @schema.Schema raise TypifyError {
match try_merge_schema(a, b, defs) {
Some(s) => s
None => Bool(false)
}
}
///|
fn try_merge_schema(
a : @schema.Schema,
b : @schema.Schema,
defs : Defs,
) -> @schema.Schema? raise TypifyError {
match (a, b) {
(Bool(false), _) | (_, Bool(false)) => return None
(Bool(true), other) | (other, Bool(true)) => return Some(other)
(Object({ reference: Some(ar), .. }), Object({ reference: Some(br), .. })) if ar ==
br => return Some(Object(@schema.SchemaObject::new_ref(ar)))
_ => ()
}
// Resolve a reference before merging (left alternative first).
let ref_case = match a {
Object({ reference: Some(r), .. }) => Some((a, r, b))
_ =>
match b {
Object({ reference: Some(r), .. }) => Some((b, r, a))
_ => None
}
}
if ref_case is Some((ref_schema, ref_name, other)) {
guard defs.get(ref_key(ref_name)) is Some(resolved) else {
raise panic_with("unresolved reference: \{ref_name}")
}
guard try_merge_schema(resolved, other, defs) is Some(merged) else {
return None
}
return if roughly(merged, resolved) {
Some(ref_schema)
} else {
Some(merged)
}
}
guard (a, b) is (Object(aa), Object(bb)) else { abort("unreachable") }
merge_schema_object(aa, bb, defs).map(o => @schema.Schema::Object(o))
}
///|
fn merge_schema_object(
a : @schema.SchemaObject,
b : @schema.SchemaObject,
defs : Defs,
) -> @schema.SchemaObject? raise TypifyError {
if a.reference is Some(_) || b.reference is Some(_) {
raise panic_with("assertion failed: reference.is_none()")
}
guard merge_so_instance_type(a.instance_type, b.instance_type)
is Some(instance_type) else {
return None
}
guard merge_so_format(a.format, b.format) is Some(format) else { return None }
guard merge_so_number(a.number, b.number) is Some(number) else { return None }
guard merge_so_string(a.string, b.string) is Some(string) else { return None }
guard merge_so_array(a.array, b.array, defs) is Some(array) else {
return None
}
guard merge_so_object(a.object, b.object, defs) is Some(object) else {
return None
}
guard merge_so_enum_values(
a.enum_values,
a.const_value,
b.enum_values,
b.const_value,
)
is Some(enum_values) else {
return None
}
let merged : @schema.SchemaObject = {
..@schema.SchemaObject::default(),
instance_type,
format,
enum_values,
number,
string,
array,
object,
}
guard try_merge_with_subschemas(merged, a.subschemas, defs) is Some(merged) else {
return None
}
guard try_merge_with_subschemas(merged, b.subschemas, defs) is Some(merged) else {
return None
}
if merged == @schema.Schema::Bool(false).into_object() {
raise panic_with("assertion failed: merged schema is false")
}
// Weed out enumerated values that no longer validate.
match merged.enum_values {
Some(values) => {
let wrapped : @schema.Schema = Object({ ..merged, enum_values: None, })
let kept = values.filter(v => schema_value_validate(wrapped, v) is None)
Some({ ..merged, enum_values: Some(kept), })
}
None => Some(merged)
}
}
///|
/// Returns `None` when unsatisfiable, `Some(result)` otherwise.
fn merge_so_enum_values(
a_enum : Array[@serde_json.Value]?,
a_const : @serde_json.Value?,
b_enum : Array[@serde_json.Value]?,
b_const : @serde_json.Value?,
) -> Array[@serde_json.Value]?? raise TypifyError {
let side = (e : Array[@serde_json.Value]?, c : @serde_json.Value?) => {
match (e, c) {
(None, None) => None
(Some(values), None) => Some(values)
(None, Some(v)) => Some([v])
(Some(_), Some(_)) => raise panic_with("not implemented")
}
}
match (side(a_enum, a_const), side(b_enum, b_const)) {
(None, None) => Some(None)
(None, Some(values)) | (Some(values), None) => Some(Some(values))
(Some(aa), Some(bb)) => {
let values = aa.filter(v => bb.contains(v))
if values.is_empty() {
None
} else {
Some(Some(values))
}
}
}
}
///|
/// Merge a schema with subschema validation; `None` if unsatisfiable.
fn try_merge_with_subschemas(
schema_object : @schema.SchemaObject,
maybe_subschemas : @schema.SubschemaValidation?,
defs : Defs,
) -> @schema.SchemaObject? raise TypifyError {
guard maybe_subschemas is Some(s) else { return Some(schema_object) }
if s.if_schema is Some(_) ||
s.then_schema is Some(_) ||
s.else_schema is Some(_) {
raise panic_with("not implemented: if/then/else schemas are not supported")
}
let mut schema_object = schema_object
if s.all_of is Some(all_of) {
let mut merged : @schema.Schema = Object(schema_object)
for other in all_of {
guard try_merge_schema(merged, other, defs) is Some(m) else {
return None
}
merged = m
}
if merged is Bool(false) {
raise panic_with("assertion failed: merged_schema != false")
}
schema_object = merged.into_object()
}
if s.not is Some(not) {
guard try_merge_schema_not(schema_object, not, defs) is Some(o) else {
return None
}
schema_object = o
}
if s.any_of is Some(_) && s.one_of is Some(_) {
raise panic_with("assertion failed: any_of.is_none() || one_of.is_none()")
}
if s.any_of is Some(any_of) {
let merged = try_merge_with_each_subschema(schema_object, any_of, defs)
match merged {
[] => return None
[only] => schema_object = only.into_object()
_ =>
schema_object = {
..@schema.SchemaObject::default(),
metadata: schema_object.metadata,
subschemas: Some({
..@schema.SubschemaValidation::default(),
any_of: Some(merged),
}),
}
}
}
if s.one_of is Some(one_of) {
let merged = try_merge_with_each_subschema(schema_object, one_of, defs)
match merged {
[] => return None
[only] => schema_object = only.into_object()
_ =>
schema_object = {
..@schema.SchemaObject::default(),
metadata: schema_object.metadata,
subschemas: Some({
..@schema.SubschemaValidation::default(),
one_of: Some(merged),
}),
}
}
}
Some(schema_object)
}
///|
fn try_merge_with_each_subschema(
schema_object : @schema.SchemaObject,
subschemas : Array[@schema.Schema],
defs : Defs,
) -> Array[@schema.Schema] raise TypifyError {
let schema : @schema.Schema = Object(schema_object)
let out = []
for ii, other in subschemas {
guard try_merge_schema(schema, other, defs) is Some(merged) else {
continue
}
if roughly(merged, schema) {
out.push(schema)
} else if roughly(merged, other) {
out.push(other)
} else {
let joined = [schema, other]
for jj, not_schema_item in subschemas {
if jj != ii {
joined.push(not_schema(not_schema_item))
}
}
out.push(all_of_schema(joined))
}
}
out
}
///|
fn merge_schema_not(
schema : @schema.Schema,
not_schema : @schema.Schema,
defs : Defs,
) -> @schema.Schema raise TypifyError {
match (schema, not_schema) {
(_, Bool(true)) | (Bool(false), _) => Bool(false)
(any, Bool(false)) => any
(Bool(true), Object(_)) => raise panic_with("not yet implemented")
(Object(o), any_not) =>
match try_merge_schema_not(o, any_not, defs) {
Some(o) => Object(o)
None => Bool(false)
}
}
}
///|
/// "Subtract" the `not` schema from the schema object.
fn try_merge_schema_not(
schema_object : @schema.SchemaObject,
not_schema : @schema.Schema,
defs : Defs,
) -> @schema.SchemaObject? raise TypifyError {
match not_schema {
Bool(true) => None
Bool(false) => Some(schema_object)
Object(not_object) =>
try_merge_schema_object_not(schema_object, not_object, defs)
}
}
///|
fn try_merge_with_subschemas_not(
schema_object : @schema.SchemaObject,
not_subschemas : @schema.SubschemaValidation,
defs : Defs,
) -> @schema.SchemaObject? raise TypifyError {
let m = sub_present(not_subschemas)
if m == S_ANY {
// not(anyOf) == allOf(not)
let all_of = not_subschemas.any_of.unwrap().map(not_schema)
let new_other = only_subschemas({
..@schema.SubschemaValidation::default(),
all_of: Some(all_of),
})
merge_schema_object(schema_object, new_other, defs)
} else if m == S_NOT {
try_merge_schema(Object(schema_object), not_subschemas.not.unwrap(), defs).map(s => {
s.into_object()
},
)
} else if m == S_ONE || m == 0 {
Some(schema_object)
} else if m == S_ALL {
match try_merge_all(not_subschemas.all_of.unwrap(), defs) {
Some(merged_not) => try_merge_schema_not(schema_object, merged_not, defs)
None => Some(schema_object)
}
} else {
raise panic_with("not yet implemented: not subschemas")
}
}
///|
fn try_merge_schema_object_not(
schema_object : @schema.SchemaObject,
not_object : @schema.SchemaObject,
defs : Defs,
) -> @schema.SchemaObject? raise TypifyError {
let mut schema_object = schema_object
match (schema_object.enum_values, not_object.enum_values) {
(Some(values), Some(not_values)) => {
let kept = values.filter(v => !not_values.contains(v))
if kept.is_empty() {
return None
}
schema_object = { ..schema_object, enum_values: Some(kept), }
}
_ => ()
}
match (schema_object.object, not_object.object) {
(Some(obj), Some(not_obj)) => {
let properties = @collections.StrMap::new()
for name, prop_schema in obj.properties {
match not_obj.properties.get(name) {
Some(not_prop) =>
properties.set(name, merge_schema_not(prop_schema, not_prop, defs))
None => properties.set(name, prop_schema)
}
}
for name in not_obj.properties.keys() {
if !obj.properties.contains(name) {
properties.set(name, @schema.Schema::Bool(false))
}
}
for not_required in not_obj.required.iter() {
if !not_obj.properties.contains(not_required) {
properties.set(not_required, @schema.Schema::Bool(false))
}
}
for required in obj.required.iter() {
if properties.get(required) is Some(Bool(false)) {
return None
}
}
schema_object = { ..schema_object, object: Some({ ..obj, properties, }), }
}
_ => ()
}
match not_object.subschemas {
Some(not_subschemas) =>
try_merge_with_subschemas_not(schema_object, not_subschemas, defs)
None => Some(schema_object)
}
}
///|
fn merge_so_instance_type(
a : @schema.SingleOrVec[@schema.InstanceType]?,
b : @schema.SingleOrVec[@schema.InstanceType]?,
) -> @schema.SingleOrVec[@schema.InstanceType]?? {
match (a, b) {
(None, None) => Some(None)
(None, Some(_) as other) | (Some(_) as other, None) => Some(other)
(Some(Single(aa)), Some(Single(bb))) =>
if aa == bb {
Some(Some(Single(aa)))
} else {
None
}
(Some(Vec(types)), Some(Single(it)))
| (Some(Single(it)), Some(Vec(types))) =>
if types.contains(it) {
Some(Some(Single(it)))
} else {
None
}
(Some(Vec(aa)), Some(Vec(bb))) => {
let types = aa.filter(t => bb.contains(t))
types.sort()
types.dedup()
match types {
[] => None
[t] => Some(Some(Single(t)))
_ => Some(Some(Vec(types)))
}
}
}
}
///|
fn merge_so_format(a : String?, b : String?) -> String?? {
match (a, b) {
(None, other) | (other, None) => Some(other)
(Some("ip"), Some("ipv4" | "ipv6" as r))
| (Some("ipv4" | "ipv6" as r), Some("ip")) => Some(Some(r))
(Some(aa), Some(bb)) if aa == bb => Some(Some(aa))
_ => None
}
}
///|
fn[T] choose_value(a : T?, b : T?, prefer : (T, T) -> T) -> T? {
match (a, b) {
(None, other) | (other, None) => other
(Some(aa), Some(bb)) => Some(prefer(aa, bb))
}
}
///|
/// Rust `f64 % f64` (truncated remainder).
fn frem(x : Double, y : Double) -> Double {
x - (x / y).trunc() * y
}
///|
fn merge_so_number(
a : @schema.NumberValidation?,
b : @schema.NumberValidation?,
) -> @schema.NumberValidation?? raise TypifyError {
match (a, b) {
(None, other) | (other, None) => return Some(other)
(Some(a), Some(b)) if a == b => return Some(Some(a))
_ => ()
}
guard (a, b) is (Some(a), Some(b)) else { abort("unreachable") }
let maximum = choose_value(a.maximum, b.maximum, fmin)
let exclusive_maximum = choose_value(
a.exclusive_maximum,
b.exclusive_maximum,
fmin,
)
let minimum = choose_value(a.minimum, b.minimum, fmax)
let exclusive_minimum = choose_value(
a.exclusive_minimum,
b.exclusive_minimum,
fmax,
)
let multiple_of = choose_value(a.multiple_of, b.multiple_of, (a, b) => {
let mut x = a
let mut y = b
while y != 0.0 {
let t = frem(x, y)
x = y
y = t
}
a / x * b
})
let (minimum, exclusive_minimum) = match (minimum, exclusive_minimum) {
(Some(inc), Some(exc)) if exc >= inc => (None, Some(exc))
(Some(inc), Some(_)) => (Some(inc), None)
pair => pair
}
let (maximum, exclusive_maximum) = match (maximum, exclusive_maximum) {
(Some(inc), Some(exc)) if exc <= inc => (None, Some(exc))
(Some(inc), Some(_)) => (Some(inc), None)
pair => pair
}
match (minimum, exclusive_minimum, maximum, exclusive_maximum) {
(Some(min), None, Some(max), None) if min > max => return None
(Some(min), None, None, Some(xmax)) if min >= xmax => return None
(None, Some(xmin), Some(max), None) if xmin >= max => return None
(None, Some(xmin), None, Some(xmax)) if xmin >= xmax => return None
(Some(_), Some(_), _, _) | (_, _, Some(_), Some(_)) =>
raise panic_with("internal error: entered unreachable code")
_ => ()
}
Some(
Some({
multiple_of,
maximum,
exclusive_maximum,
minimum,
exclusive_minimum,
}),
)
}
///|
fn merge_so_string(
a : @schema.StringValidation?,
b : @schema.StringValidation?,
) -> @schema.StringValidation?? {
match (a, b) {
(None, other) | (other, None) => return Some(other)
(Some(a), Some(b)) if a == b => return Some(Some(a))
_ => ()
}
guard (a, b) is (Some(a), Some(b)) else { abort("unreachable") }
let max_length = choose_value(a.max_length, b.max_length, (x, y) => {
if x <= y {
x
} else {
y
}
})
let min_length = choose_value(a.min_length, b.min_length, (x, y) => {
if x >= y {
x
} else {
y
}
})
let pattern = match (a.pattern, b.pattern) {
(None, v) | (v, None) => v
(Some(x), Some(y)) if x == y => Some(x)
// Combine distinct patterns with lookaheads.
(Some(x), Some(y)) if x.has_prefix("(?=") => Some("\{x}(?=\{y})")
(Some(x), Some(y)) => Some("(?=\{x})(?=\{y})")
}
if (min_length, max_length) is (Some(min), Some(max)) && min > max {
return None
}
Some(Some({ max_length, min_length, pattern, }))
}
///|
fn umin(x : UInt, y : UInt) -> UInt {
if x <= y {
x
} else {
y
}
}
///|
fn umax(x : UInt, y : UInt) -> UInt {
if x >= y {
x
} else {
y
}
}
///|
fn merge_so_array(
a : @schema.ArrayValidation?,
b : @schema.ArrayValidation?,
defs : Defs,
) -> @schema.ArrayValidation?? raise TypifyError {
match (a, b) {
(None, other) | (other, None) => return Some(other)
_ => ()
}
guard (a, b) is (Some(aa), Some(bb)) else { abort("unreachable") }
let max_items = choose_value(aa.max_items, bb.max_items, umin)
let min_items = choose_value(aa.min_items, bb.min_items, umax)
let unique_items = choose_value(aa.unique_items, bb.unique_items, (x, y) => {
x || y
})
let contains = match (aa.contains, bb.contains) {
(None, other) | (other, None) => other
(Some(x), Some(y)) if x == y => Some(x)
_ => return None
}
if (min_items, max_items) is (Some(min), Some(max)) && min > max {
return None
}
let (items, additional_items, max_items) : (
@schema.SingleOrVec[@schema.Schema]?,
@schema.Schema?,
UInt?,
) = match ((aa.items, aa.additional_items), (bb.items, bb.additional_items)) {
((None, _), (None, _)) => (None, None, max_items)
((None, _), (Some(Single(item)), _))
| ((Some(Single(item)), _), (None, _)) =>
(Some(Single(item)), None, max_items)
((None, _), (Some(Vec(items)), additional))
| ((Some(Vec(items)), additional), (None, _)) =>
match max_items {
Some(max) if items.length() >= max.reinterpret_as_int() =>
(
Some(Vec(items[:max.reinterpret_as_int()].to_owned())),
None,
max_items,
)
_ => (Some(Vec(items)), additional, max_items)
}
((Some(Single(a_single)), _), (Some(Single(b_single)), _)) => {
guard try_merge_schema(a_single, b_single, defs) is Some(merged) else {
return None
}
(Some(Single(merged)), None, max_items)
}
((Some(Single(single)), _), (Some(Vec(items)), additional))
| ((Some(Vec(items)), additional), (Some(Single(single)), _)) => {
guard merge_items_array(
items.map(i => (i, single)),
min_items,
max_items,
defs,
)
is Some((items, allow_additional)) else {
return None
}
if allow_additional {
let additional = match additional {
None => single
Some(additional_schema) =>
match try_merge_schema(additional_schema, single, defs) {
Some(s) => s
None => return None
}
}
(Some(Vec(items)), Some(additional), max_items)
} else {
(Some(Vec(items)), None, Some(items.length().reinterpret_as_uint()))
}
}
((Some(Vec(a_items)), a_additional), (Some(Vec(b_items)), b_additional)) => {
let items_len = a_items.length().max(b_items.length())
let pairs = []
for i in 0.. s
None => a_additional.unwrap_or(Bool(true))
}
let y = match b_items.get(i) {
Some(s) => s
None => b_additional.unwrap_or(Bool(true))
}
pairs.push((x, y))
}
guard merge_items_array(pairs, min_items, max_items, defs)
is Some((items, allow_additional)) else {
return None
}
if allow_additional {
let additional = merge_additional_items(
a_additional, b_additional, defs,
)
(Some(Vec(items)), additional, max_items)
} else {
(Some(Vec(items)), None, Some(items.length().reinterpret_as_uint()))
}
}
}
Some(
Some({
items,
additional_items,
max_items,
min_items,
unique_items,
contains,
}),
)
}
///|
/// Merge item pairs; stops at `max_items` or at the first unsatisfiable
/// item. Returns the items and whether additional items remain possible.
fn merge_items_array(
pairs : Array[(@schema.Schema, @schema.Schema)],
min_items : UInt?,
max_items : UInt?,
defs : Defs,
) -> (Array[@schema.Schema], Bool)? raise TypifyError {
let items = []
for pair in pairs {
match try_merge_schema(pair.0, pair.1, defs) {
Some(schema) => {
items.push(schema)
if max_items is Some(max) && items.length() == max.reinterpret_as_int() {
return Some((items, false))
}
}
None => {
if items.length().reinterpret_as_uint() < min_items.unwrap_or(1) {
return None
}
return Some((items, false))
}
}
}
Some((items, true))
}
///|
fn merge_so_object(
a : @schema.ObjectValidation?,
b : @schema.ObjectValidation?,
defs : Defs,
) -> @schema.ObjectValidation?? raise TypifyError {
match (a, b) {
(None, other) | (other, None) => return Some(other)
_ => ()
}
guard (a, b) is (Some(aa), Some(bb)) else { abort("unreachable") }
let required = aa.required.union(bb.required)
let additional_properties = merge_additional_properties(
aa.additional_properties,
bb.additional_properties,
defs,
)
// Properties of a, then those only in b, resolved and checked in order
// (upstream short-circuits on the first unsatisfiable required property).
let properties = @collections.StrMap::new()
let add = (name : String, resolved : @schema.Schema) => {
match resolved {
Bool(false) if required.contains(name) => false
Bool(false) => {
if !(additional_properties is Some(Bool(false))) {
properties.set(name, resolved)
}
true
}
schema => {
properties.set(name, schema)
true
}
}
}
for name, a_schema in aa.properties {
let resolved = match bb.properties.get(name) {
Some(b_schema) => merge_schema(a_schema, b_schema, defs)
None => filter_prop(name, a_schema, bb, defs)
}
if !add(name, resolved) {
return None
}
}
for name, b_schema in bb.properties {
if !aa.properties.contains(name) {
if !add(name, filter_prop(name, b_schema, aa, defs)) {
return None
}
}
}
let max_properties = choose_value(aa.max_properties, bb.max_properties, umin)
let min_properties = choose_value(aa.min_properties, bb.min_properties, umax)
if (min_properties, max_properties) is (Some(min), Some(max)) && min > max {
return None
}
Some(
Some({
required,
properties,
additional_properties,
max_properties,
min_properties,
pattern_properties: @collections.StrMap::new(),
property_names: None,
}),
)
}
///|
fn filter_prop(
name : String,
prop_schema : @schema.Schema,
object_schema : @schema.ObjectValidation,
_defs : Defs,
) -> @schema.Schema raise TypifyError {
if object_schema.properties.contains(name) {
raise panic_with(
"assertion failed: !object_schema.properties.contains_key(name)",
)
}
if object_schema.property_names is Some(_) {
raise panic_with("assertion failed: object_schema.property_names.is_none()")
}
if !object_schema.pattern_properties.is_empty() {
raise panic_with(
"assertion failed: object_schema.pattern_properties.is_empty()",
)
}
match object_schema.additional_properties {
Some(Bool(true)) | None => prop_schema
Some(Bool(false)) => Bool(false)
Some(additional) => all_of_schema([additional, prop_schema])
}
}