// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// Building the sheet: every field read four times, and the reason wherever a
// projection had nothing to say.
///|
struct Note {
letter : String
field : String
reading : String
why : String
}
///|
/// Record what a projection could not carry, and answer with the letter the
/// cell prints beside it.
fn note(
notes : Array[Note],
field : String,
reading : String,
why : String,
) -> String {
for existing in notes {
if existing.field == field && existing.why == why {
return existing.letter
}
}
let letter = if notes.length() < 26 {
(notes.length() + 97).unsafe_to_char().to_string()
} else {
"?"
}
notes.push({ letter, field, reading, why, })
letter
}
///|
fn properties_of(document : Json) -> Json? {
match document {
Object(map) => map.get("properties")
_ => None
}
}
///|
/// The `properties.` schema of a one-field model.
fn field_schema(properties : Json?, name : String) -> Json? {
match properties {
Some(Object(map)) => map.get(name)
_ => None
}
}
///|
/// The column lines of a `CREATE TABLE`, without the statement around them.
fn table_lines(statement : String, name : String) -> Array[String] {
let rows = statement.split("\n").collect()
let out = []
let mut seen = false
for row in rows {
let line = row.trim(chars=" ").to_owned()
if line.has_prefix(name + " ") {
seen = true
out.push(
if line.has_suffix(",") {
line.exact_view(end=line.length() - 1).to_owned()
} else {
line
},
)
continue
}
let continues = seen &&
!line.has_prefix(")") &&
(line.has_prefix("CHECK") || line.has_prefix("REFERENCES"))
if continues {
out.push(
if line.has_suffix(",") {
line.exact_view(end=line.length() - 1).to_owned()
} else {
line
},
)
}
}
let wrapped = []
for line in out {
for one in wrap(line, 34) {
wrapped.push(one)
}
}
wrapped
}
///|
/// The field's own line in a GraphQL type, and the type word on it.
fn sdl_line(text : String, name : String) -> (String, String) {
for row in text.split("\n") {
let line = row.trim(chars=" ").to_owned()
if line.has_prefix(name + ":") {
let rest = line
.exact_view(start=name.length() + 1)
.trim(chars=" ")
.to_owned()
return (line, rest)
}
}
("", "—")
}
///|
/// Whether a layout's numbers are wider than GraphQL's 32-bit `Int`.
fn too_wide_for_graphql(kind : @moonmodel.Type) -> Bool {
match kind {
Big => true
Wire(layout) =>
match layout.bits {
Some(bits) => bits > 32 || (layout.unsigned && bits >= 32)
None => false
}
_ => false
}
}
///|
fn boolean(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn lost_json(lost : String?) -> String {
match lost {
Some(letter) => quoted(letter)
None => "null"
}
}
///|
fn cell_json(cell : Cell) -> String {
"{\"lines\": " +
string_array(cell.lines) +
", \"spell\": " +
quoted(cell.spell) +
", \"lost\": " +
lost_json(cell.lost) +
"}"
}
///|
/// The whole sheet, as the JSON a page typesets.
fn sheet() -> String {
let model = user()
let notes : Array[Note] = []
let fields : Array[String] = []
let mut required = 0
for field in model.fields {
if field.required {
required = required + 1
}
let name = field.name
let one = @moonmodel.Model::new("", [field])
// --- JSON Schema ------------------------------------------------------
let json_cell = match field_schema(properties_of(@json.of(one)), name) {
Some(fragment) => {
let lost = if field.check.unique {
Some(
note(
notes, name, "json", "unique reaches SQL only: JSON Schema keeps that on the collection, not on the value.",
),
)
} else {
None
}
Cell::new(schema_lines(fragment), schema_spell(fragment), lost)
}
None => Cell::new([], "—", None)
}
// --- OpenAPI ----------------------------------------------------------
let openapi_cell = match
field_schema(properties_of(@open.of(one, dialect=@open.OpenApi30)), name) {
Some(fragment) => {
let lost = if field.check.unique {
Some(
note(
notes, name, "openapi", "unique reaches SQL only: JSON Schema keeps that on the collection, not on the value.",
),
)
} else {
None
}
Cell::new(schema_lines(fragment), schema_spell(fragment), lost)
}
None => Cell::new([], "—", None)
}
// --- SQL --------------------------------------------------------------
let (columns, skipped) = @sql.of(one, dialect=@sql.Postgres)
let sql_cell = if skipped.length() > 0 {
Cell::new([], "—", Some(note(notes, name, "sql", skipped[0].why)))
} else if columns.length() > 0 {
let column = columns[0]
let mut spell = column.kind
if column.unique {
spell = spell + " UNIQUE"
}
if column.check != "" {
spell = spell + " + CHECK"
}
Cell::new(
table_lines(@sql.table(one, dialect=@sql.Postgres), name),
spell,
None,
)
} else {
Cell::new([], "—", None)
}
// --- GraphQL SDL ------------------------------------------------------
let (sdl_text, sdl_skipped) = @sdl.of(one)
let sdl_cell = if sdl_skipped.length() > 0 {
Cell::new([], "—", Some(note(notes, name, "sdl", sdl_skipped[0].why)))
} else {
let (line, spell) = sdl_line(sdl_text, name)
let why = if too_wide_for_graphql(field.kind) {
Some(
"GraphQL's Int is 32 bits, so a 64-bit integer is carried as a String.",
)
} else if field.kind is Stamp {
Some(
"GraphQL has no date; the document declares the DateTime scalar once.",
)
} else if field.kind is Bytes {
Some("GraphQL has no byte string; it is carried as a String.")
} else if !field.check.options.is_empty() {
Some(
"a closed set of values has no GraphQL form here; the enum reaches JSON and SQL only.",
)
} else {
None
}
let lost = match why {
Some(text) => Some(note(notes, name, "sdl", text))
None => None
}
Cell::new([line], spell, lost)
}
// --- the station itself -----------------------------------------------
let station_lost = match field.kind {
Wire(layout) =>
if layout.order is @moonmodel.Order::Big || layout.reference {
Some(
note(
notes, name, "station", "the width of a layout becomes a format word; its byte order and its reference mark reach no document at all.",
),
)
} else {
None
}
_ => None
}
let row = StringBuilder()
row.write_string(" { \"name\": ")
row.write_string(quoted(name))
row.write_string(", \"type\": ")
row.write_string(quoted(display(field.kind, field.nullable)))
row.write_string(", \"doc\": ")
row.write_string(quoted(field.doc))
row.write_string(", \"required\": ")
row.write_string(boolean(field.required))
row.write_string(", \"station_lost\": ")
row.write_string(lost_json(station_lost))
row.write_string(", \"cells\": {\n")
row.write_string(" \"json\": " + cell_json(json_cell) + ",\n")
row.write_string(" \"openapi\": " + cell_json(openapi_cell) + ",\n")
row.write_string(" \"sql\": " + cell_json(sql_cell) + ",\n")
row.write_string(" \"sdl\": " + cell_json(sdl_cell) + " } }")
fields.push(row.to_string())
}
let note_rows = []
for item in notes {
let row = StringBuilder()
row.write_string(" { \"n\": " + quoted(item.letter))
row.write_string(", \"field\": " + quoted(item.field))
row.write_string(", \"reading\": " + quoted(item.reading))
row.write_string(", \"why\": " + quoted(item.why) + " }")
note_rows.push(row.to_string())
}
let out = StringBuilder()
out.write_string("{\n")
out.write_string(" \"model\": " + quoted(model.name) + ",\n")
out.write_string(" \"doc\": " + quoted(model.doc) + ",\n")
out.write_string(
" \"stat\": { \"fields\": " +
fields.length().to_string() +
", \"required\": " +
required.to_string() +
", \"lost\": " +
notes.length().to_string() +
" },\n",
)
out.write_string(" \"fields\": [\n")
out.write_string(fields.join(",\n"))
out.write_string("\n ],\n")
out.write_string(" \"notes\": [\n")
out.write_string(note_rows.join(",\n"))
out.write_string("\n ]\n")
out.write_string("}\n")
out.to_string()
}