// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// One description of a customer, projected four ways. The same `Model` values
// reach every projection; nothing below is written twice.
///|
/// Everything a description can say, in one type.
///
/// Every variant of `Type` appears, and every field of `Check`, so the four
/// projections below each have to answer for all of them — including the
/// three none of them can carry.
fn customer() -> @moonmodel.Model {
@moonmodel.Model::new(
"Customer",
[
@moonmodel.Field::new(
"id",
Big,
doc="assigned at sign-up, never reused",
check=@moonmodel.Check::new(unique=true),
),
@moonmodel.Field::new(
"name",
Text,
doc="what to call them",
check=@moonmodel.Check::new(shortest=1, longest=64),
),
@moonmodel.Field::new(
"email",
Text,
check=@moonmodel.Check::new(format="email", pattern="^[^@]+@[^@]+$"),
),
@moonmodel.Field::new(
"age",
Int,
required=false,
check=@moonmodel.Check::new(least=0.0, under=150.0),
),
@moonmodel.Field::new(
"balance",
Real,
default=Json::number(0.0),
check=@moonmodel.Check::new(over=-1000.0, step=0.01),
),
@moonmodel.Field::new("active", Bool, default=Json::boolean(true)),
@moonmodel.Field::new("joined", Stamp),
@moonmodel.Field::new("avatar", Bytes, required=false, nullable=true),
@moonmodel.Field::new(
"tier",
Text,
default=Json::string("free"),
check=@moonmodel.Check::new(options=[
Json::string("free"),
Json::string("paid"),
Json::string("staff"),
]),
),
@moonmodel.Field::new(
"tags",
List(Text),
check=@moonmodel.Check::new(distinct=true, longest=10),
),
@moonmodel.Field::new("labels", Dict(Text)),
@moonmodel.Field::new("home", Ref("Address")),
@moonmodel.Field::new(
"billed_to",
Nest(
@moonmodel.Model::new("", [
@moonmodel.Field::new("name", Text),
@moonmodel.Field::new("vat", Text, required=false),
]),
),
),
@moonmodel.Field::new("notes", Any, required=false),
@moonmodel.Field::new(
"either",
Raw({ "oneOf": [{ "type": "string" }, { "type": "integer" }] }),
),
// A value a byte format writes itself: two bytes, no sign, most
// significant first. A document has no word for that and writes the
// width it can state — `format: uint16` — and nothing about the rest.
@moonmodel.Field::new(
"counter",
Wire(
@moonmodel.Layout::new(
Int,
bits=16,
unsigned=true,
order=@moonmodel.Order::Big,
),
),
doc="a count a byte format writes in two bytes",
),
],
doc="somebody with an account",
)
}
///|
fn address() -> @moonmodel.Model {
@moonmodel.Model::new("Address", [
@moonmodel.Field::new("line", Text),
@moonmodel.Field::new("city", Text),
@moonmodel.Field::new(
"country",
Text,
check=@moonmodel.Check::new(shortest=2, longest=2),
),
])
}
///|
fn heading(text : String) -> Unit {
println("")
println("── " + text + " " + "─".repeat(70 - text.length()))
}
///|
fn main {
let customer = customer()
let address = address()
// The description answers questions of its own, which is what makes it
// worth having rather than four hand-written schemas.
heading("the description")
println("model \{customer.name} — \{customer.doc}")
println("fields \{customer.fields.length()}")
println("required \{customer.required().join(", ")}")
println("mentions \{customer.mentions().join(", ")}")
match customer.field("email") {
Some(field) =>
println("email format \{field.check.format.unwrap_or("none")}")
None => println("email absent")
}
println("preset empty: \{@moonmodel.check.is_empty()}")
heading("JSON Schema 2020-12")
let order = @moonmodel.Model::new("Order", [
@moonmodel.Field::new("buyer", Ref("Customer")),
@moonmodel.Field::new("total", Real),
])
println(@json.of(order).stringify(indent=2))
// A document carries what it refers to. `known` is where the referenced
// descriptions come from; anything left unresolved stays a bare `$ref`.
heading("one document, references resolved")
println(@json.document(customer, known=[address]).stringify(indent=2))
// The three OpenAPI dialects differ in three places, and this probe hits
// all three: a nullable value, an open bound, and a reference.
heading("the three OpenAPI dialects")
let probe = @moonmodel.Model::new("Probe", [
@moonmodel.Field::new(
"score",
Real,
nullable=true,
check=@moonmodel.Check::new(over=0.0),
),
@moonmodel.Field::new("home", Ref("Address")),
])
for
one in [
("OpenAPI 3.1", @open.Dialect::OpenApi31),
("OpenAPI 3.0", @open.Dialect::OpenApi30),
("Swagger 2.0", @open.Dialect::Swagger20),
] {
let (label, dialect) = one
println("")
println("\{label} — references point at \{dialect.at()}")
println(@open.of(probe, dialect~).stringify(indent=2))
}
println("")
let envelope = @open.schemas([address][:], dialect=Swagger20)
println("2.0 keeps them under \{@open.Dialect::Swagger20.holder()}:")
println(envelope.stringify(indent=2))
// SQL loses the most, so it returns both halves: the columns it wrote and
// the fields it could not.
heading("SQL, three dialects")
for
one in [
("SQLite ", @sql.Dialect::Sqlite),
("Postgres", @sql.Dialect::Postgres),
("MySQL ", @sql.Dialect::Mysql),
] {
let (label, dialect) = one
let (columns, missing) = @sql.of(customer, dialect~)
println(
"\{label} \{columns.length()} columns, \{missing.length()} fields without one",
)
}
println("")
println(@sql.table(customer, dialect=Postgres, name="customers"))
println("")
let (_columns, unstorable) = @sql.of(customer, dialect=Postgres)
for one in unstorable {
println("no column for \{one.field}: \{one.why}")
}
// GraphQL marks non-null with `!`, which folds required and nullable into
// one mark, and has no type at all for two of these fields.
heading("GraphQL SDL")
let (text, untypeable) = @sdl.of(customer)
println(text)
println("")
for one in untypeable {
println("no type for \{one.field}: \{one.why}")
}
println("")
println("an argument type is the same shape under the other keyword:")
let (input, _) = @sdl.of(address, input=true)
println(input)
// The two scalars GraphQL lacks are declared once per document, and both
// names are parameters for a schema that chose others.
heading("a document declares its scalars")
println(@sdl.document([address, customer][:], stamp="Timestamp"))
println("the presets are \{@sdl.stamp} and \{@sdl.bytes}")
// The description is also what an instance is checked against: it is written
// out as JSON Schema and read by a validator that knows the specification,
// so there is one schema rather than a copy of it kept in step by hand.
heading("checking an instance")
let account = @moonmodel.Model::new("Account", [
@moonmodel.Field::new("name", Text, check=@moonmodel.Check::new(shortest=1)),
@moonmodel.Field::new(
"age",
Int,
required=false,
check=@moonmodel.Check::new(least=0.0, most=150.0),
),
])
// Read the description once and check as many instances as there are.
let checker = @verify.schema(account) catch {
error => abort("the description is not a schema: \{error}")
}
println(checker.valid({ "name": "ada", "age": Json::number(36.0) }))
let wrong : Json = { "age": Json::number(151.0) }
println(checker.valid(wrong))
for fault in checker.faults(wrong) {
println("\{fault.at}: \{fault.keyword} — \{fault.message}")
}
}