// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The schema reference: what the endpoint knows, written down. A `GET` on the
/// endpoint serves it when the `Ide` is `reference`.
///
/// It is rendered from the schema the application holds, not from JSON the page
/// fetches: the types, their fields, the arguments each field takes, the
/// descriptions and the deprecations are already in memory, so the page is one
/// string with nothing to load. That is the difference between this and the four
/// IDEs beside it — they run operations, this one is read.
///
/// The page is the same material as the SDL, arranged for a reader: the root
/// types first, then every other type grouped by kind, each type a section with
/// an anchor a link can point at.
///|
/// Escape the five characters that would otherwise end a text run or an
/// attribute early.
fn html_escape(text : String) -> String {
let out = StringBuilder()
for ch in text {
match ch {
'<' => out.write_string("<")
'>' => out.write_string(">")
'&' => out.write_string("&")
'"' => out.write_string(""")
'\'' => out.write_string("'")
_ => out.write_char(ch)
}
}
out.to_string()
}
///|
/// A link to a type's section, or the name as text when the schema does not
/// declare the type (a built-in scalar, mostly).
fn type_link(schema : @schema.Schema, name : String) -> String {
let known = schema.type_by_name(name) is Some(_) ||
schema.enum_by_name(name) is Some(_) ||
schema.union_by_name(name) is Some(_) ||
schema.scalar_by_name(name) is Some(_)
if known {
"" +
html_escape(name) +
""
} else {
"" + html_escape(name) + ""
}
}
///|
/// A type as the SDL would write it — `[User!]!` — with every named part a link
/// and the brackets carried as punctuation, so a reader can click into the type
/// a field returns.
fn type_ref_html(schema : @schema.Schema, t : @schema.GqlType) -> String {
match t {
Scalar(name) => type_link(schema, name)
Named(name) => type_link(schema, name)
NonNull(inner) =>
type_ref_html(schema, inner) + "!"
ListOf(inner) =>
"[" +
type_ref_html(schema, inner) +
"]"
}
}
///|
/// The fields of an object, interface or input type: one row each, with the
/// arguments a field takes spelled out under its name.
fn fields_html(
schema : @schema.Schema,
fields : Array[@schema.Field],
) -> String {
if fields.is_empty() {
return "
No fields.
"
}
let out = StringBuilder()
out.write_string("
")
for f in fields {
out.write_string("
")
out.write_string("
")
out.write_string(html_escape(f.name))
out.write_string("")
if !f.args.is_empty() {
out.write_string("(")
let mut first = true
for a in f.args {
if !first {
out.write_string(", ")
}
first = false
out.write_string("")
out.write_string(html_escape(a.0))
out.write_string(": ")
out.write_string(type_ref_html(schema, a.1))
match f.arg_defaults.get(a.0) {
Some(d) => {
out.write_string(" = ")
out.write_string(html_escape(d.stringify()))
out.write_string("")
}
None => ()
}
}
out.write_string(")")
}
out.write_string(": ")
out.write_string(type_ref_html(schema, f.typ))
out.write_string("
")
match f.description {
Some(d) =>
out.write_string("
")
out.to_string()
}
///|
/// One type's section: the heading with its kind, the description, the
/// interfaces it implements, and the body each kind of type has.
fn type_section(
schema : @schema.Schema,
name : String,
kind : String,
description : String?,
implements : Array[String],
body : String,
) -> String {
let out = StringBuilder()
out.write_string("")
out.write_string("
")
match description {
Some(d) => out.write_string("
" + html_escape(d) + "
")
None => ()
}
if !implements.is_empty() {
out.write_string("
Implements ")
let mut first = true
for i in implements {
if !first {
out.write_string(", ")
}
first = false
out.write_string(type_link(schema, i))
}
out.write_string("
")
}
out.write_string(body)
out.write_string("")
out.to_string()
}
///|
/// The stylesheet. One set of variables, a serif for prose, a mono for
/// signatures, and every scrollable region — the page, the rail, a field row that
/// runs long — carrying the site's own bar rather than the browser's.
fn reference_style() -> String {
let css =
#|
css
}
///|
/// The script: the search that narrows the rail and the page together, and the
/// scrollbar that is only visible once the page has moved.
fn reference_script() -> String {
let script =
#|
script
}
///|
/// The documentation page for `schema`, served at `endpoint`.
pub fn reference_html(
schema : @schema.Schema,
endpoint~ : String = "/graphql",
) -> String {
let objects : Array[@schema.ObjectType] = []
let interfaces : Array[@schema.ObjectType] = []
let inputs : Array[@schema.ObjectType] = []
let mut field_count = 0
for t in schema.types {
field_count = field_count + t.fields.length()
match t.kind {
@schema.Object => objects.push(t)
@schema.Interface => interfaces.push(t)
@schema.Input => inputs.push(t)
}
}
let out = StringBuilder()
out.write_string("\n\n\n")
out.write_string("\n")
out.write_string(
"\n",
)
out.write_string("Schema reference\n")
out.write_string(
"\n",
)
out.write_string(
"\n\n",
)
out.write_string(
"\n",
)
out.write_string(reference_style())
out.write_string("\n\n")
out.write_string("\n")
out.write_string("
\n")
// The rail: the root types first, because they are what a caller starts from,
// then everything else by kind.
out.write_string("\n\n")
out.write_string("
")
out.write_string("
Everything this endpoint answers
")
out.write_string(
"
A rendering of the schema the server holds, in the order a caller works through it. Fields link to the type they return; deprecated ones say so. The same material is available as SDL from the schema itself.
\n")
// The root types, each with its fields.
for name in roots {
match schema.type_by_name(name) {
Some(t) =>
out.write_string(
type_section(
schema,
t.name,
"ROOT",
t.description,
t.interfaces,
fields_html(schema, t.fields),
),
)
None => ()
}
}
for t in sorted_objects(objects) {
if roots.contains(t.name) {
continue
}
out.write_string(
type_section(
schema,
t.name,
"OBJECT",
t.description,
t.interfaces,
fields_html(schema, t.fields),
),
)
}
for t in sorted_objects(interfaces) {
out.write_string(
type_section(
schema,
t.name,
"INTERFACE",
t.description,
t.interfaces,
fields_html(schema, t.fields),
),
)
}
for u in schema.unions {
let body = StringBuilder()
body.write_string("
")
for m in u.members {
body.write_string("" + type_link(schema, m) + "")
}
body.write_string("
")
out.write_string(
type_section(schema, u.name, "UNION", u.description, [], body.to_string()),
)
}
for e in schema.enums {
let body = StringBuilder()
body.write_string("
")
for v in e.values {
body.write_string("" + html_escape(v) + "")
}
body.write_string("
")
for v in e.values {
match e.value_descriptions.get(v) {
Some(d) =>
body.write_string(
"
" +
html_escape(v) +
"
" +
html_escape(d) +
"
",
)
None => ()
}
}
out.write_string(
type_section(schema, e.name, "ENUM", e.description, [], body.to_string()),
)
}
for t in sorted_objects(inputs) {
out.write_string(
type_section(
schema,
t.name,
"INPUT OBJECT",
t.description,
[],
fields_html(schema, t.fields),
),
)
}
for s in schema.scalars {
let body = match s.spec_url {
Some(u) =>
"
\n")
out.write_string(reference_script())
out.write_string("\n\n")
out.to_string()
}
///|
/// Names in dictionary order, the way the rail is meant to read.
fn dict_order(names : Array[String]) -> Array[String] {
let out = names.copy()
out.sort_by(fn(a, b) { a.compare(b) })
out
}
///|
/// The names of the three root types that exist, in the order a document
/// addresses them.
fn root_names(schema : @schema.Schema) -> Array[String] {
let out : Array[String] = [schema.query]
match schema.mutation {
Some(m) => out.push(m)
None => ()
}
match schema.subscription {
Some(s) => out.push(s)
None => ()
}
out
}
///|
/// The mutation and subscription roots the schema also declares, spelled out
/// under the query root's name.
fn root_note(schema : @schema.Schema) -> String {
let out = StringBuilder()
match schema.mutation {
Some(m) => out.write_string(" · mutation " + html_escape(m))
None => ()
}
match schema.subscription {
Some(s) => out.write_string(" · subscription " + html_escape(s))
None => ()
}
out.to_string()
}
///|
/// Object types in dictionary order, the order the sections are written in.
fn sorted_objects(
types : Array[@schema.ObjectType],
) -> Array[@schema.ObjectType] {
let out = types.copy()
out.sort_by(fn(a, b) { a.name.compare(b.name) })
out
}
///|
/// The page a reference request is answered with when the application did not
/// hand over a schema: the same chrome, and one sentence saying so instead of a
/// documentation page with nothing in it.
fn missing_schema_page(endpoint : String) -> String {
let out = StringBuilder()
out.write_string("\n\n\n")
out.write_string("\n")
out.write_string(
"\n",
)
out.write_string("Schema reference\n")
out.write_string(reference_style())
out.write_string(
"\n\n
This page is rendered from the schema the application holds, and none was passed to it. Serving it through the application's own endpoint (graphql_app or graphql_app_async) supplies one.