///|
/// A reference that could not be written into the schema, and why.
///
/// Reported rather than dropped or guessed at: a body schema silently missing
/// a field is a validation hole that shows up as accepted-but-wrong data.
pub(all) struct Unresolved {
/// The field that holds the reference.
field : String
/// The model it points at.
model : String
why : String
} derive(Eq)
///|
pub extend Unresolved with Eq::{equal, not_equal}
///|
/// A moonmodel description as a schema.
///
/// One description, written once, drives request validation and the OpenAPI
/// document here, and the DDL and the SDL in their own libraries. `known` is
/// where a `Ref` is looked up: a reference to a model that is not in it, or
/// one that leads back to a model already being written, is reported instead
/// — a schema is a finite value, so a cycle cannot be written out, and the
/// name alone does not say what the thing looks like.
pub fn Schema::of_model(
model : @moonmodel.Model,
known~ : Array[@moonmodel.Model] = [],
) -> (Schema, Array[Unresolved]) {
let missing = []
let schema = object_of(model, known, [], missing)
(schema, missing)
}
///|
/// `open~` is the names currently being written, which is how a reference
/// leading back into one of them is caught before it is followed.
fn object_of(
model : @moonmodel.Model,
known : Array[@moonmodel.Model],
open : Array[String],
missing : Array[Unresolved],
) -> Schema {
let fields = []
let within = if model.name == "" { open } else { [..open, model.name] }
for one in model.fields {
match kind_of(one.kind, known, within, one.name, missing) {
Some(kind) => fields.push(described(one, kind))
None => ()
}
}
SObject({ name: model.name, fields, description: model.doc, })
}
///|
fn described(one : @moonmodel.Field, kind : Schema) -> Field {
// A field that is nullable and one that may be absent are two questions
// there, and two different things here: the first is a wrapper on the
// value, the second is whether the name appears in `required`.
let kind = if one.nullable { SNullable(kind) } else { kind }
let kind = match one.check.format {
Some(format) => SFormat(kind, format)
None => kind
}
let kind = if one.check.options.length() > 0 {
SEnum(kind, one.check.options)
} else {
kind
}
Field::new(
one.name,
kind,
required=one.required,
description=one.doc,
constraints=bounds_of(one.check, one.kind),
default?=one.default,
)
}
///|
fn kind_of(
kind : @moonmodel.Type,
known : Array[@moonmodel.Model],
open : Array[String],
field : String,
missing : Array[Unresolved],
) -> Schema? {
match kind {
Bool => Some(SBool)
// Both are OpenAPI integers; which one is said by the format, which
// `field_of` writes from `Check::format` only when the description asked
// for one, so the width is carried by `int32`/`int64` below.
Int => Some(SFormat(SInt, "int32"))
Big => Some(SFormat(SInt, "int64"))
Real => Some(SFloat)
Text => Some(SStr)
Bytes => Some(SFormat(SStr, "byte"))
Stamp => Some(SFormat(SStr, "date-time"))
List(inner) =>
match kind_of(inner, known, open, field, missing) {
Some(inner) => Some(SArray(inner))
None => None
}
Dict(inner) =>
match kind_of(inner, known, open, field, missing) {
Some(inner) => Some(SMap(inner))
None => None
}
Nest(model) => Some(object_of(model, known, open, missing))
Ref(name) => {
if open.contains(name) {
missing.push({
field,
model: name,
why: "a reference back into a model being written cannot be a finite schema",
})
return None
}
for one in known {
if one.name == name {
return Some(object_of(one, known, open, missing))
}
}
missing.push({
field,
model: name,
why: "not among the known models, and a name alone does not say what it looks like",
})
None
}
Any => Some(SAny)
Raw(written) => Some(Schema::json(written))
}
}
///|
/// The bounds, under the names this library gives them.
///
/// `shortest` and `longest` bound a string's characters and a list's items
/// alike in a description; here they are two different constraints, and the
/// type says which is meant.
fn bounds_of(
check : @moonmodel.Check,
kind : @moonmodel.Type,
) -> Array[Constraint] {
let out = []
match check.least {
Some(value) => out.push(Minimum(value))
None => ()
}
match check.most {
Some(value) => out.push(Maximum(value))
None => ()
}
match check.over {
Some(value) => out.push(ExclusiveMinimum(value))
None => ()
}
match check.under {
Some(value) => out.push(ExclusiveMaximum(value))
None => ()
}
match check.step {
Some(value) => out.push(MultipleOf(value))
None => ()
}
let listed = kind is List(_)
match check.shortest {
Some(value) =>
out.push(if listed { MinItems(value) } else { MinLength(value) })
None => ()
}
match check.longest {
Some(value) =>
out.push(if listed { MaxItems(value) } else { MaxLength(value) })
None => ()
}
match check.pattern {
Some(value) => out.push(Pattern(value))
None => ()
}
out
}