// Leaf and structural converters (upstream convert.rs).
///|
fn TypeSpace::convert_string(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
format : String?,
validation : @schema.StringValidation?,
) -> Converted raise TypifyError {
let display_from_str : Array[TypeSpaceImpl] = [Display, FromStr]
match format {
Some("uuid") => {
self.uses_uuid = true
(TypeEntry::new_native("::uuid::Uuid", display_from_str), metadata)
}
Some("date") => {
self.uses_chrono = true
(
TypeEntry::new_native("::chrono::naive::NaiveDate", display_from_str),
metadata,
)
}
Some("date-time") => {
self.uses_chrono = true
(
TypeEntry::new_native(
"::chrono::DateTime<::chrono::offset::Utc>", display_from_str,
),
metadata,
)
}
Some("ip") =>
(TypeEntry::new_native("::std::net::IpAddr", display_from_str), metadata)
Some("ipv4") =>
(
TypeEntry::new_native("::std::net::Ipv4Addr", display_from_str),
metadata,
)
Some("ipv6") =>
(
TypeEntry::new_native("::std::net::Ipv6Addr", display_from_str),
metadata,
)
_ =>
match validation {
None | Some({ max_length: None, min_length: None, pattern: None, }) =>
(TypeEntry::from_details(String), metadata)
Some(v) => {
if v.pattern is Some(pattern) {
ignore(
@regex.Regex::new(pattern) catch {
e =>
raise InvalidSchema(
type_name=type_name.into_option(),
reason="invalid pattern '\{pattern}' \{e}",
)
},
)
self.uses_regress = true
}
let type_id = self.assign_type(TypeEntry::from_details(String))
(
TypeEntryNewtype::from_metadata_with_string_validation(
self, type_name, metadata, type_id, v, original_schema,
),
metadata,
)
}
}
}
}
///|
fn TypeSpace::convert_enum_string(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
enum_values : Array[@serde_json.Value],
validation : @schema.StringValidation?,
) -> Converted raise TypifyError {
let mut has_null = false
let validator = StringValidator::new(type_name, validation)
let variants = []
for value in enum_values {
match value {
Null => has_null = true
String(variant_name) =>
if validator.is_valid(variant_name) {
variants.push(Variant::new(variant_name, None, Simple))
}
_ => raise BadValue("string", value)
}
}
if variants.is_empty() {
if has_null {
self.convert_null(metadata)
} else {
self.convert_never(type_name, original_schema)
}
} else {
let mut ty = TypeEntryEnum::from_metadata(
self,
type_name,
metadata,
External,
variants,
false,
original_schema,
)
if has_null {
ty = self.type_to_option(ty)
}
(ty, metadata)
}
}
///|
priv struct IntFormat {
format : String
ty : String
nz_ty : String
min : Double
max : Double
}
///|
/// Integer formats, from most to least restrictive. Bounds are the Rust
/// `T::MIN as f64` / `T::MAX as f64` values.
let int_formats : Array[IntFormat] = [
{
format: "int8",
ty: "i8",
nz_ty: "::std::num::NonZeroU8",
min: -128.0,
max: 127.0,
},
{
format: "uint8",
ty: "u8",
nz_ty: "::std::num::NonZeroU8",
min: 0.0,
max: 255.0,
},
{
format: "int16",
ty: "i16",
nz_ty: "::std::num::NonZeroU16",
min: -32768.0,
max: 32767.0,
},
{
format: "uint16",
ty: "u16",
nz_ty: "::std::num::NonZeroU16",
min: 0.0,
max: 65535.0,
},
{
format: "int",
ty: "i32",
nz_ty: "::std::num::NonZeroU32",
min: -2147483648.0,
max: 2147483647.0,
},
{
format: "int32",
ty: "i32",
nz_ty: "::std::num::NonZeroU32",
min: -2147483648.0,
max: 2147483647.0,
},
{
format: "uint",
ty: "u32",
nz_ty: "::std::num::NonZeroU32",
min: 0.0,
max: 4294967295.0,
},
{
format: "uint32",
ty: "u32",
nz_ty: "::std::num::NonZeroU32",
min: 0.0,
max: 4294967295.0,
},
{
format: "int64",
ty: "i64",
nz_ty: "::std::num::NonZeroU64",
min: (-0x7FFF_FFFF_FFFF_FFFFL - 1L).to_double(),
max: 0x7FFF_FFFF_FFFF_FFFFL.to_double(),
},
{
format: "uint64",
ty: "u64",
nz_ty: "::std::num::NonZeroU64",
min: 0.0,
max: 0xFFFF_FFFF_FFFF_FFFFUL.to_double(),
},
]
///|
/// `f64::EPSILON`
const F64_EPSILON : Double = 2.220446049250313e-16
///|
fn fmax(a : Double, b : Double) -> Double {
if a.is_nan() {
b
} else if b.is_nan() {
a
} else if a >= b {
a
} else {
b
}
}
///|
fn fmin(a : Double, b : Double) -> Double {
if a.is_nan() {
b
} else if b.is_nan() {
a
} else if a <= b {
a
} else {
b
}
}
///|
fn TypeSpace::convert_integer(
_self : TypeSpace,
metadata : @schema.Metadata?,
validation : @schema.NumberValidation?,
format : String?,
) -> Converted raise TypifyError {
let (min0, max0, multiple) = match validation {
None => (None, None, None)
Some(v) => {
let min = match (v.minimum, v.exclusive_minimum) {
(None, None) => None
(None, Some(e)) => Some(e + 1.0)
(Some(m), None) => Some(m)
(Some(m), Some(e)) => Some(fmax(m, e + 1.0))
}
let max = match (v.maximum, v.exclusive_maximum) {
(None, None) => None
(None, Some(e)) => Some(e - 1.0)
(Some(m), None) => Some(m)
(Some(m), Some(e)) => Some(fmin(m, e - 1.0))
}
(min, max, v.multiple_of)
}
}
let mut min = min0
let mut max = max0
let default = metadata_default(metadata)
if format is Some(format) {
match int_formats.search_by(f => f.format == format) {
Some(i) => {
let f = int_formats[i]
let valid_min = match min {
None => true
Some(fmin) => fmin >= f.min
}
let valid_max = match max {
None => true
Some(fmax) => fmax <= f.max
}
if multiple is None && valid_min && valid_max {
if default.bind(v => v.as_f64()) is Some(d) &&
(d < f.min || d > f.max) {
raise InvalidValue
}
return if min == Some(1.0) {
(TypeEntry::new_integer(f.nz_ty), metadata)
} else {
(TypeEntry::new_integer(f.ty), metadata)
}
}
if min is None {
min = Some(f.min)
}
if max is None {
max = Some(f.max)
}
}
None => ()
}
}
// Check the default against the bounds.
if default is Some(d) {
let ok = match (d.as_f64(), min, max) {
(Some(_), None, None) => true
(Some(v), None, Some(fmax)) => v <= fmax
(Some(v), Some(fmin), None) => v >= fmin
(Some(v), Some(fmin), Some(fmax)) => v >= fmin && v <= fmax
_ => false
}
if !ok {
raise InvalidValue
}
}
let rev = int_formats.rev()
let maybe_type = match (min, max) {
(None, Some(max)) =>
rev.search_by(f => (f.max - max).abs() <= F64_EPSILON).map(i => rev[i].ty)
(Some(min), None) => {
let mut found = None
for f in rev {
if min == 1.0 {
found = Some(f.nz_ty)
break
} else if (f.min - min).abs() <= F64_EPSILON {
found = Some(f.ty)
break
}
}
found
}
(Some(min), Some(max)) => {
let mut found = None
for f in rev {
if min == 1.0 {
found = Some(f.nz_ty)
break
} else if (f.max - max).abs() <= F64_EPSILON &&
(f.min - min).abs() <= F64_EPSILON {
found = Some(f.ty)
break
}
}
found
}
(None, None) => None
}
match maybe_type {
Some(ty) => (TypeEntry::new_integer(ty), metadata)
None => (TypeEntry::new_integer("i64"), metadata)
}
}
///|
fn TypeSpace::convert_number(
_self : TypeSpace,
metadata : @schema.Metadata?,
format : String?,
) -> Converted {
match format {
Some("float") => (TypeEntry::new_float("f32"), metadata)
_ => (TypeEntry::new_float("f64"), metadata)
}
}
///|
fn TypeSpace::convert_null(
_self : TypeSpace,
metadata : @schema.Metadata?,
) -> Converted {
(TypeEntry::from_details(Unit), metadata)
}
///|
fn can_handle_pattern_properties(v : @schema.ObjectValidation) -> Bool {
if !v.required.is_empty() || !v.properties.is_empty() {
return false
}
guard v.pattern_properties.first() is Some((_, first)) else { return false }
if !v.pattern_properties.values().iter().all(s => s == first) {
return false
}
if v.additional_properties is (Some(Bool(true)) | Some(Object(_))) {
return false
}
v.property_names is None
}
///|
fn TypeSpace::convert_object(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
validation : @schema.ObjectValidation?,
) -> Converted raise TypifyError {
match validation {
// Maps: no properties and non-false additionalProperties.
Some(v) if v.required.is_empty() &&
v.properties.is_empty() &&
v.pattern_properties.is_empty() &&
!(v.additional_properties is Some(Bool(false))) => {
let entry = self.make_map(
type_name.into_option(),
v.property_names,
v.additional_properties,
)
(entry, metadata)
}
Some(v) if can_handle_pattern_properties(v) => {
let pattern = v.pattern_properties.keys().join("|")
let property_names : @schema.Schema? = Some(
Object({
..@schema.SchemaObject::default(),
string: Some({
max_length: None,
min_length: None,
pattern: Some(pattern),
}),
}),
)
let additional = v.pattern_properties.first().map(p => p.1)
let entry = self.make_map(
type_name.into_option(),
property_names,
additional,
)
(entry, metadata)
}
None => (self.make_map(type_name.into_option(), None, None), metadata)
Some(v) => {
let tmp_type_name = get_type_name(type_name, metadata)
let (properties, deny_unknown_fields) = self.struct_members(
tmp_type_name, v,
)
(
TypeEntryStruct::from_metadata(
self, type_name, metadata, properties, deny_unknown_fields, original_schema,
),
None,
)
}
}
}
///|
fn TypeSpace::convert_reference(
self : TypeSpace,
metadata : @schema.Metadata?,
ref_name : String,
) -> Converted raise TypifyError {
if !ref_name.has_prefix("#") {
raise panic_with("external references are not supported: \{ref_name}")
}
match self.ref_to_id.get(ref_key(ref_name)) {
Some(type_id) => (TypeEntry::from_details(Reference(type_id)), metadata)
None => raise panic_with("$ref \{ref_name} is missing")
}
}
///|
fn TypeSpace::convert_all_of(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
subschemas : Array[@schema.Schema],
) -> Converted raise TypifyError {
if self.maybe_singleton_subschema(type_name, subschemas) is Some(ty) {
return (ty, metadata)
}
let merged = merge_all(subschemas, self.definitions)
if merged is Bool(false) {
return self.convert_never(type_name, original_schema)
}
let merged = merged.into_object()
if merged.metadata is Some(_) {
raise panic_with("assertion failed: merged_schema.metadata.is_none()")
}
let (type_entry, _) = self.convert_schema_object(type_name, original_schema, {
..merged,
metadata,
})
(type_entry, None)
}
///|
fn TypeSpace::convert_any_of(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
subschemas : Array[@schema.Schema],
) -> Converted raise TypifyError {
if subschemas.is_empty() {
let type_name = match get_type_name(type_name, metadata) {
Some(name) => Name::Required(name)
None => type_name
}
return self.convert_never(type_name, original_schema)
}
if self.maybe_option(type_name, metadata, subschemas) is Some(ty) {
return (ty, metadata)
}
if all_mutually_exclusive(subschemas, self.definitions) {
self.convert_one_of(type_name, original_schema, metadata, subschemas)
} else {
self.flattened_union_struct(
type_name, original_schema, metadata, subschemas, true,
)
}
}
///|
fn TypeSpace::convert_one_of(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
subschemas : Array[@schema.Schema],
) -> Converted raise TypifyError {
let ty = match self.maybe_option(type_name, metadata, subschemas) {
Some(ty) => ty
None =>
match
self.maybe_externally_tagged_enum(
type_name, original_schema, metadata, subschemas,
) {
Some(ty) => ty
None =>
match
self.maybe_adjacently_tagged_enum(
type_name, original_schema, metadata, subschemas,
) {
Some(ty) => ty
None =>
match
self.maybe_internally_tagged_enum(
type_name, original_schema, metadata, subschemas,
) {
Some(ty) => ty
None =>
match self.maybe_singleton_subschema(type_name, subschemas) {
Some(ty) => ty
None =>
self.untagged_enum(
type_name, original_schema, metadata, subschemas,
)
}
}
}
}
}
(ty, metadata)
}
///|
/// Infer an instance type from a JSON value (for `not`/untyped enums).
fn value_instance_type(v : @serde_json.Value) -> @schema.InstanceType {
match v {
Null => Null
Bool(_) => Boolean
Number(_) => Number
String(_) => String
Array(_) => Array
Object(_) => Object
}
}
///|
fn TypeSpace::convert_not(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
subschema : @schema.Schema,
) -> Converted raise TypifyError {
match subschema {
Bool(b) => {
let (entry, _) = self.convert_schema(type_name, Bool(!b))
(entry, metadata)
}
Object(o) if shape(o, any=F_META) => {
let (entry, _) = self.convert_schema(type_name, Bool(false))
(entry, metadata)
}
Object(
{ instance_type: Some(Single(_)), enum_values: Some(enum_values), .. } as o
) => {
let type_schema = { ..o, enum_values: None, }
let (entry, _) = self.convert_schema_object(
Unknown,
original_schema,
type_schema,
)
for value in enum_values {
ignore(entry.validate_value(self, value))
}
let type_id = self.assign_type(entry)
(
TypeEntryNewtype::from_metadata_with_deny_values(
self, type_name, metadata, type_id, enum_values, original_schema,
),
metadata,
)
}
Object(o) if shape(o, some=F_ENUM, any=F_META) => {
let enum_values = o.enum_values.unwrap()
let inner_metadata = o.metadata
let types : Array[@schema.InstanceType] = []
for v in enum_values {
let t = match v {
Bool(_) => @schema.InstanceType::Boolean
Number(_) => Number
String(_) => String
_ => raise panic_with("unhandled type for `not` construction: \{v}")
}
if !types.contains(t) {
types.push(t)
}
}
guard types is [instance_type] else {
raise panic_with("multiple implied types for an un-typed enum")
}
let typed_schema = {
..@schema.SchemaObject::default(),
instance_type: Some(Single(instance_type)),
}
let (entry, _) = self.convert_schema_object(
Unknown,
original_schema,
typed_schema,
)
for value in enum_values {
ignore(entry.validate_value(self, value))
}
let type_id = self.assign_type(entry)
(
TypeEntryNewtype::from_metadata_with_deny_values(
self, type_name, inner_metadata, type_id, enum_values, original_schema,
),
inner_metadata,
)
}
_ => raise panic_with("not yet implemented: unhandled not schema")
}
}
///|
fn TypeSpace::convert_array(
self : TypeSpace,
type_name : Name,
metadata : @schema.Metadata?,
validation : @schema.ArrayValidation,
) -> Converted raise TypifyError {
match validation {
// Tuples and fixed-length arrays.
{
items,
additional_items,
max_items: Some(max_items),
min_items: Some(min_items),
unique_items: None,
contains: None,
} if max_items == min_items && max_items > 0 => {
let max = max_items.reinterpret_as_int()
match items {
Some(Vec(items)) if items.length() < max => {
let rest_name = type_name.append("additional")
let rest_id = match additional_items {
Some(rest_schema) => self.id_for_schema(rest_name, rest_schema).0
None => self.id_for_schema(rest_name, Bool(true)).0
}
let types = []
for ii, item_schema in items {
types.push(
self.id_for_schema(type_name.append("item\{ii}"), item_schema).0,
)
}
for _ in items.length().. {
let types = []
for ii, item_schema in items {
if ii >= max {
break
}
types.push(
self.id_for_schema(type_name.append("item\{ii}"), item_schema).0,
)
}
(TypeEntry::from_details(Tuple(types)), metadata)
}
Some(Single(item_schema)) => {
let item_id = self.id_for_schema(
type_name.append("item"),
item_schema,
).0
(TypeEntry::from_details(Array(item_id, max)), metadata)
}
None => {
let any_id = self.id_for_schema(type_name.append("item"), Bool(true)).0
(TypeEntry::from_details(Array(any_id, max)), metadata)
}
}
}
// Arrays and sets.
{ items: Some(Single(item)), unique_items, contains: None, .. } => {
let item_type_name = match get_type_name(type_name, metadata) {
Some(s) => Name::Suggested("\{s}Item")
None => Unknown
}
let (type_id, _) = self.id_for_schema(item_type_name, item)
match unique_items {
Some(true) => (TypeEntry::from_details(Set(type_id)), metadata)
_ => (TypeEntry::from_details(Vec(type_id)), metadata)
}
}
// Arrays and sets with no specified items.
{ items: None, unique_items, contains: None, .. } => {
self.uses_serde_json = true
let type_id = self.assign_type(TypeEntry::from_details(JsonValue))
match unique_items {
Some(true) => (TypeEntry::from_details(Set(type_id)), metadata)
_ => (TypeEntry::from_details(Vec(type_id)), metadata)
}
}
_ =>
raise InvalidSchema(
type_name=type_name.into_option(),
reason="unhandled array validation \{Repr(validation)}",
)
}
}
///|
fn TypeSpace::convert_array_of_any(
self : TypeSpace,
metadata : @schema.Metadata?,
) -> Converted {
self.uses_serde_json = true
let type_id = self.assign_type(TypeEntry::from_details(JsonValue))
(TypeEntry::from_details(Vec(type_id)), metadata)
}
///|
fn TypeSpace::convert_bool(
_self : TypeSpace,
metadata : @schema.Metadata?,
) -> Converted {
(TypeEntry::from_details(Boolean), metadata)
}
///|
fn TypeSpace::convert_permissive(
self : TypeSpace,
metadata : @schema.Metadata?,
) -> Converted {
self.uses_serde_json = true
(TypeEntry::from_details(JsonValue), metadata)
}
///|
/// The "never" type: an enum with no variants.
fn TypeSpace::convert_never(
self : TypeSpace,
type_name : Name,
schema : @schema.Schema,
) -> Converted raise TypifyError {
let ty = TypeEntryEnum::from_metadata(
self,
type_name,
None,
External,
[],
true,
schema,
)
(ty, None)
}
///|
fn TypeSpace::convert_typed_enum(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
schema : @schema.SchemaObject,
enum_values : Array[@serde_json.Value],
) -> Converted raise TypifyError {
let type_schema = { ..schema, enum_values: None, }
let inner_type_name = match get_type_name(type_name, schema.metadata) {
Some(s) => Name::Suggested("\{s}Inner")
None => Unknown
}
let (entry, metadata) = self.convert_schema_object(
inner_type_name, original_schema, type_schema,
)
for value in enum_values {
ignore(entry.validate_value(self, value))
}
let type_id = self.assign_type(entry)
let newtype = TypeEntryNewtype::from_metadata_with_enum_values(
self, type_name, metadata, type_id, enum_values, original_schema,
)
(newtype, if metadata is Some(_) { schema.metadata } else { None })
}
///|
fn TypeSpace::convert_unknown_enum(
self : TypeSpace,
type_name : Name,
original_schema : @schema.Schema,
metadata : @schema.Metadata?,
enum_values : Array[@serde_json.Value],
) -> Converted raise TypifyError {
if enum_values.is_empty() {
return self.convert_never(type_name, original_schema)
}
let types : Array[@schema.InstanceType] = []
for v in enum_values {
let t = value_instance_type(v)
if !types.contains(t) {
types.push(t)
}
}
let has_null = types.contains(Null)
if has_null {
let non_null = enum_values.filter(v => !v.is_null())
if non_null.is_empty() {
return self.convert_null(metadata)
}
let (entry, metadata) = self.convert_unknown_enum(
type_name, original_schema, metadata, non_null,
)
return (self.type_to_option(entry), metadata)
}
match types {
[String] =>
self.convert_enum_string(
type_name,
original_schema,
metadata,
enum_values,
None,
)
[Boolean] => self.convert_bool(metadata)
[instance_type] => {
let typed_schema = {
..@schema.SchemaObject::default(),
instance_type: Some(Single(instance_type)),
}
let (entry, new_metadata) = self.convert_typed_enum(
type_name, original_schema, typed_schema, enum_values,
)
(entry, if new_metadata is Some(_) { metadata } else { None })
}
_ => raise panic_with("multiple implied types for an un-typed enum")
}
}
///|
fn TypeSpace::convert_option(
self : TypeSpace,
type_name : Name,
metadata : @schema.Metadata?,
schema : @schema.Schema,
) -> Converted raise TypifyError {
let (ty, _) = self.convert_schema(type_name, schema)
(self.type_to_option(ty), metadata)
}
///|
/// A single subschema is just an annotation layer.
fn TypeSpace::maybe_singleton_subschema(
self : TypeSpace,
type_name : Name,
subschemas : Array[@schema.Schema],
) -> TypeEntry? raise TypifyError {
match subschemas {
[subschema] => ok(() => self.convert_schema(type_name, subschema).0)
_ => None
}
}