// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// What a value is, in the smallest set all four projections recognise.
///
/// `Ref` names another model rather than nesting it, so two models may point
/// at each other and a projection decides for itself whether to inline or to
/// emit a reference — `$ref` in JSON Schema, a foreign key in SQL, a named
/// type in SDL.
pub(all) enum Type {
Bool
/// A 32-bit integer.
Int
/// A 64-bit integer. Separate from `Int` because SQL and GraphQL both care
/// which one, and because JSON numbers past 2^53 do not survive a double.
Big
Real
Text
Bytes
/// A moment in time. Only a format that defines one reads into this; see
/// `Check::format` for the other case, where a date is a string being
/// checked rather than a date being parsed.
Stamp
List(Type)
/// A mapping with text keys. SQL and GraphQL have no such thing, and say so
/// in their own way.
Dict(Type)
/// Another model, by name.
Ref(String)
/// Another model, written here. For the one that has no name of its own and
/// so cannot be referred to; a named one belongs in `Ref` so that the
/// projections can hoist it and point at it once.
Nest(Model)
/// Anything at all: JSON Schema's empty schema.
Any
/// A schema written out in full, for what this vocabulary cannot say —
/// `allOf`, a vocabulary of one's own, a dialect's extension. Each
/// projection either passes it through as it stands or reports that it
/// cannot carry it. Reach for it last: nothing reads inside it.
Raw(Json)
} derive(Eq, Debug)
///|
pub extend Type with Eq::{equal, not_equal}
///|
pub extend Type with Debug::{to_repr}
///|
/// The bounds a value must satisfy.
///
/// The names avoid `min` and `max`, which mean different things in the four
/// targets: `least` and `most` are the closed bounds, `under` and `over` the
/// open ones (JSON Schema's `exclusiveMinimum` and `exclusiveMaximum`).
/// `shortest` and `longest` bound a string's characters and a list's items
/// alike, which is how every one of the four spells it too.
pub(all) struct Check {
least : Double?
most : Double?
over : Double?
under : Double?
/// A multiple the value must be one of (`multipleOf`).
step : Double?
shortest : Int32?
longest : Int32?
pattern : String?
/// A named shape a string is checked against — `date-time`, `email`, `uuid`.
/// Checked, never converted: see the README on when a string becomes a date.
format : String?
/// The values allowed, if the set is closed. Json rather than text because
/// a closed set of numbers is just as ordinary as one of names.
options : Array[Json]
/// Whether a list's items must differ (`uniqueItems`).
distinct : Bool
/// Whether no two records may carry the same value.
///
/// Separate from `distinct`, which is about one list's own items: this one
/// is about the collection the records live in, and only SQL can state it.
/// The other three projections drop it.
unique : Bool
} derive(Eq, Debug)
///|
pub extend Check with Eq::{equal, not_equal}
///|
pub extend Check with Debug::{to_repr}
///|
/// No bound at all, which is what most fields carry.
pub let check : Check = {
least: None,
most: None,
over: None,
under: None,
step: None,
shortest: None,
longest: None,
pattern: None,
format: None,
options: [],
distinct: false,
unique: false,
}
///|
/// A `Check` by name, so a caller states the one bound it means.
pub fn Check::new(
least? : Double,
most? : Double,
over? : Double,
under? : Double,
step? : Double,
shortest? : Int32,
longest? : Int32,
pattern? : String,
format? : String,
options~ : Array[Json] = [],
distinct~ : Bool = false,
unique~ : Bool = false,
) -> Check {
{
least,
most,
over,
under,
step,
shortest,
longest,
pattern,
format,
options,
distinct,
unique,
}
}
///|
/// Whether anything is bounded at all, which is how a projection decides to
/// write nothing rather than an empty object.
pub fn Check::is_empty(self : Check) -> Bool {
self == check
}
///|
/// One field of a model.
///
/// `required` and `nullable` are two questions, not one. The first asks
/// whether the key has to be there, the second whether its value may be null.
/// JSON Schema keeps them apart (`required` is a list on the object, `null` is
/// a type), SQL's `NOT NULL` is the second alone, and GraphQL's `!` is both at
/// once. Folding them into one field would leave at least one projection
/// guessing.
pub(all) struct Field {
name : String
kind : Type
required : Bool
nullable : Bool
doc : String
default : Json?
check : Check
} derive(Eq, Debug)
///|
pub extend Field with Eq::{equal, not_equal}
///|
pub extend Field with Debug::{to_repr}
///|
/// A field. Required by default, because a type whose fields are all optional
/// describes almost nothing, and because every one of the four targets treats
/// "present" as the ordinary case.
pub fn Field::new(
name : String,
kind : Type,
required~ : Bool = true,
nullable~ : Bool = false,
doc~ : String = "",
default? : Json,
check~ : Check = check,
) -> Field {
{ name, kind, required, nullable, doc, default, check, }
}
///|
/// A named type and its fields.
pub(all) struct Model {
name : String
doc : String
fields : Array[Field]
} derive(Eq, Debug)
///|
pub extend Model with Eq::{equal, not_equal}
///|
pub extend Model with Debug::{to_repr}
///|
pub fn Model::new(
name : String,
fields : Array[Field],
doc~ : String = "",
) -> Model {
{ name, doc, fields, }
}
///|
/// One field by name, for a projection that needs to look one up.
pub fn Model::field(self : Model, name : String) -> Field? {
for one in self.fields {
if one.name == name {
return Some(one)
}
}
None
}
///|
/// The names of the fields that must be present, in declaration order —
/// JSON Schema's `required` array, and the test every projection applies.
pub fn Model::required(self : Model) -> Array[String] {
let out = []
for one in self.fields {
if one.required {
out.push(one.name)
}
}
out
}
///|
/// Every model named by a `Ref` anywhere in this one, in the order met and
/// without repeats.
///
/// A projection that writes a document rather than a fragment needs this: it
/// has to emit the models this one points at, and the ones those point at.
pub fn Model::mentions(self : Model) -> Array[String] {
let out = []
for one in self.fields {
names(one.kind, out)
}
out
}
///|
fn names(kind : Type, out : Array[String]) -> Unit {
match kind {
Ref(name) => if !out.contains(name) { out.push(name) }
List(inner) | Dict(inner) => names(inner, out)
// A nested model has no name of its own, but what it refers to counts.
Nest(model) =>
for one in model.fields {
names(one.kind, out)
}
_ => ()
}
}