// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// The sheet, as data. One model is projected four times; what comes out is the
// text of each cell, the one-word summary a reader compares across a row, and
// what each projection had to leave out — so a page can typeset all of it and
// reflow when a field is added.
///|
/// The model the sheet prints. It is the one in `src/examples/annotations`,
/// written out as calls, so the sheet and the example cannot drift.
fn user() -> @moonmodel.Model {
@moonmodel.Model::new(
"User",
[
@moonmodel.Field::new(
"id",
@moonmodel.Big,
doc="assigned by the database",
check=@moonmodel.Check::new(unique=true),
),
@moonmodel.Field::new(
"name",
@moonmodel.Text,
check=@moonmodel.Check::new(shortest=1, longest=64),
),
@moonmodel.Field::new(
"email",
@moonmodel.Text,
required=false,
nullable=true,
check=@moonmodel.Check::new(format="email"),
),
@moonmodel.Field::new(
"age",
@moonmodel.Int,
required=false,
nullable=true,
check=@moonmodel.Check::new(least=0.0, most=150.0),
),
@moonmodel.Field::new(
"role",
@moonmodel.Text,
check=@moonmodel.Check::new(options=[
Json::string("Admin"),
Json::string("Member"),
Json::string("Guest"),
]),
),
@moonmodel.Field::new(
"weight",
@moonmodel.Real,
required=false,
default=Json::number(1.0),
check=@moonmodel.Check::new(over=0.0),
),
@moonmodel.Field::new("home", @moonmodel.Ref("Address")),
@moonmodel.Field::new(
"tags",
@moonmodel.List(@moonmodel.Text),
check=@moonmodel.Check::new(longest=8, distinct=true),
),
@moonmodel.Field::new("joined", @moonmodel.Stamp),
@moonmodel.Field::new(
"count",
@moonmodel.Wire(
@moonmodel.Layout::new(
@moonmodel.Int,
bits=16,
unsigned=true,
order=@moonmodel.Order::Big,
reference=true,
),
),
),
],
doc="somebody with an account",
)
}
///|
/// One cell: the lines as they are printed, the one-word summary, and the note
/// letter when the projection left something out.
struct Cell {
lines : Array[String]
spell : String
lost : String?
}
///|
fn Cell::new(lines : Array[String], spell : String, lost : String?) -> Cell {
{ lines, spell, lost, }
}
///|
/// A quoted JSON string.
fn quoted(text : String) -> String {
let out = StringBuilder()
out.write_char('"')
for c in text {
if c == '"' {
out.write_string("\\\"")
} else if c == '\\' {
out.write_string("\\\\")
} else if c == '\n' {
out.write_string("\\n")
} else {
out.write_char(c)
}
}
out.write_char('"')
out.to_string()
}
///|
fn string_array(items : Array[String]) -> String {
"[" + items.map(quoted).join(", ") + "]"
}
///|
/// A value on one line, which is how a cell prints it.
fn inline(json : Json) -> String {
match json {
Null => "null"
True => "true"
False => "false"
Number(n, ..) => n.to_string()
String(s) => quoted(s)
Array(items) => "[" + items.map(inline).join(", ") + "]"
Object(map) if map.is_empty() => "{}"
Object(map) => {
let pairs = map
.iter()
.map(p => quoted(p.0) + ": " + inline(p.1))
.collect()
"{ " + pairs.join(", ") + " }"
}
}
}
///|
/// The order a schema's keys are read in: what a value is, then what it is
/// checked against, then its name. Any other key follows alphabetically, so two
/// runs produce the same sheet.
fn key_order(map : Map[String, Json]) -> Array[String] {
let priority = [
"type", "format", "$ref", "items", "enum", "minLength", "maxLength", "minimum",
"maximum", "exclusiveMinimum", "minItems", "maxItems", "uniqueItems", "default",
"nullable", "description",
]
let out = []
for key in priority {
if map.contains(key) {
out.push(key)
}
}
let rest = map.keys().filter(k => !out.contains(k)).collect()
rest.sort()
out..append(rest)
}
///|
/// A line broken at spaces, the continuation indented, so a cell never runs
/// into the column beside it.
fn wrap(text : String, width : Int32) -> Array[String] {
let out = []
let mut line = ""
for word in text.split(" ").collect() {
let w = word.to_owned()
let next = if line == "" { w } else { line + " " + w }
if next.length() > width && line != "" {
out.push(line)
line = " " + w
} else {
line = next
}
}
if line != "" {
out.push(line)
}
out
}
///|
/// The entries of a schema object as printed lines, a trailing comma on every
/// one but the last. A description is an ellipsis: the text belongs on the
/// spine, and a cell is thirty characters wide.
fn schema_lines(fragment : Json) -> Array[String] {
let out = []
if fragment is Object(map) {
let keys = key_order(map)
for i, key in keys {
let shown = match map.get(key) {
Some(String(text)) if key == "$ref" => quoted(last_segment(text))
Some(String(text)) if text.length() > 18 => "\"…\""
Some(value) => inline(value)
None => "\"\""
}
let comma = if i == keys.length() - 1 { "" } else { "," }
out.push(quoted(key) + ": " + shown + comma)
}
}
let wrapped = []
for line in out {
for one in wrap(line, 34) {
wrapped.push(one)
}
}
wrapped
}
///|
/// The one-word summary of a schema: what a reader compares across a row.
fn number_text(n : Double) -> String {
let whole = n.to_int()
if n == whole.to_double() {
whole.to_string()
} else {
n.to_string()
}
}
///|
fn last_segment(path : String) -> String {
match path.rev_find("/") {
Some(at) => "…" + path.exact_view(start=at).to_owned()
None => path
}
}
///|
fn number_at(map : Map[String, Json], key : String) -> String? {
match map.get(key) {
Some(Number(n, ..)) => Some(number_text(n))
_ => None
}
}
///|
/// The one-word summary of a schema: what a reader compares across a row.
fn schema_spell(fragment : Json) -> String {
let out = StringBuilder()
if fragment is Object(map) {
if map.get("$ref") is Some(String(path)) {
out.write_string(
"ref " + last_segment(path).exact_view(start=2).to_owned(),
)
return out.to_string()
}
let mut nullable = false
let names = []
match map.get("type") {
Some(String(one)) => names.push(one)
Some(Array(items)) =>
for item in items {
match item {
String(one) =>
if one == "null" {
nullable = true
} else {
names.push(one)
}
_ => ()
}
}
_ => ()
}
out.write_string(names.join("|"))
if map.get("format") is Some(String(one)) {
out.write_string(" " + one)
}
let least = number_at(map, "minimum")
let most = number_at(map, "maximum")
let shortest = number_at(map, "minLength")
let longest = number_at(map, "maxLength")
if least is Some(one) {
if most is Some(two) {
out.write_string(" " + one + "…" + two)
} else {
out.write_string(" ≥" + one)
}
} else if most is Some(two) {
out.write_string(" ≤" + two)
}
if shortest is Some(one) {
if longest is Some(two) {
out.write_string(" " + one + "…" + two)
} else {
out.write_string(" ≥" + one + " chars")
}
} else if longest is Some(two) {
out.write_string(" ≤" + two)
}
if map.get("exclusiveMinimum") is Some(Number(n, ..)) {
out.write_string(" >" + number_text(n))
} else if map.get("exclusiveMinimum") is Some(True) {
out.write_string(" open")
}
match map.get("enum") {
Some(Array(items)) =>
out.write_string(" enum " + items.length().to_string())
_ => ()
}
match map.get("default") {
Some(value) => out.write_string(" =" + inline(value))
_ => ()
}
if map.get("nullable") is Some(True) {
nullable = true
}
if nullable {
out.write_string("|null")
}
}
out.to_string()
}
///|
/// The type as the spine writes it: `Big`, `Text?`, `List[Text]`, a layout's own
/// word.
fn display(kind : @moonmodel.Type, nullable : Bool) -> String {
let name = match kind {
Bool => "Bool"
Int => "Int"
Big => "Big"
Real => "Real"
Text => "Text"
Bytes => "Bytes"
Stamp => "Stamp"
Any => "Any"
List(inner) => "List[" + display(inner, false) + "]"
Dict(inner) => "Map[String, " + display(inner, false) + "]"
Ref(_) => "Ref"
Nest(_) => "Nest"
Wire(layout) => layout_word(layout)
Raw(_) => "Raw"
}
if nullable {
name + "?"
} else {
name
}
}
///|
fn layout_word(layout : @moonmodel.Layout) -> String {
@moonmodel.Type::Wire(layout).word().unwrap_or("wire")
}