Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Building a Tree of the Program with XQuery

A code generator is a program that writes text, and the most direct way to write text in XQuery is a string constructor. It starts with ``[ and ends with ]``. Everything between is text, kept as it is, line breaks included, except for expressions written between `{ and }`, whose values are put in:

$ sxml xquery --method=text --expression='
    let $name := "FlagKey"
    return ``[export function check`{ $name }`(value: unknown): boolean { return true; }]``'
export function checkFlagKey(value: unknown): boolean { return true; }

Braces, quotes and backticks are ordinary text in a string constructor, so TypeScript can be written in it as it looks. For a line or two of text with a fixed shape, that is the simplest tool there is. For whole files it gets hard to follow. Each piece of the generator then does three jobs at once: it decides what the program says (a check for each facet), how TypeScript writes it (keywords, punctuation, quotes and escapes), and how it is laid out (indentation, line breaks, empty lines). Each piece has to get all three right, and the indentation of a line depends on pieces far away. What comes out is text, which can be read but not asked anything.

So we'll split the work in two. The generator, in XQuery, builds a tree of the TypeScript program: an XML document with an element for each declaration, type, statement and expression. It decides what the program says, and nothing else. A printer, an XSLT stylesheet that the next section shows, turns the tree into text, with one template rule for each kind of element. Between the two steps, the tree is a document like any other: we can look at it, query it and test it.

A vocabulary for TypeScript

The tree needs element names. The Source Code as XML chapter has some already: sxml:parse-code turns TypeScript into a tree, with an element for each node of tree-sitter's grammar. Here is a line of code from the generated checks:

$ sxml xpath --method=xml 'sxml:parse-code("const count = problems.length;", "typescript", { "highlights": false() })'
<program xmlns:sxml="urn:sxml"><lexical_declaration><sxml:token field="kind">const</sxml:token> <variable_declarator><identifier field="name">count</identifier> = <member_expression field="value"><identifier field="object">problems</identifier>.<property_identifier field="property">length</property_identifier></member_expression></variable_declarator>;</lexical_declaration></program>

That tree is concrete: every token and every space is in it, so its string value is the code. That is what makes it good for reading code, and it is why it is a poor thing to write: to write such a tree, you write the text anyway. A generator needs an abstract tree, which has only the parts that vary, and leaves the keywords, punctuation and layout to the printer. So we'll use a small vocabulary of our own, in no namespace, named after TypeScript's own words. The same line is

<const name="count"><member name="length"><name>problems</name></member></const>

and the whole vocabulary fits in a table:

ElementsTypeScript
module (with file)a file; its children are its declarations
comment, doc// text and /** text */
import (with from, type-only)import type { A, B } from "./types.ts";
type-aliasexport type State = "stable" | "paused";
interface, property (with optional), methodexport interface FlagStatus { flag: FlagKey; retryAfter?: Seconds; }
function (with export, async), parameter, returns, bodyexport async function getFlags(input: FlagsRequest): Promise<FlagSet> { ... }
type, array-type, union, predicatePromise<FlagSet>, string[], "a" | "b", value is FlagSet
statement, const, return, if with then and else, for-ofstatements
name, string, number, boolean, regexvalue, "text", 100, false, /^[a-z0-9-]*$/u
call (with function, or method of its first child), member, not, binary (with operator), await, array, object with entry, arrowexpressions

A name of something, such as a function, is an attribute; the parts of it are children. A string holds the text of the string, not the TypeScript literal: how quotes and escapes are written is the printer's business. An operator has a name, such as less, not a symbol, so no &lt; is needed in the generator.

The hand-written part

Not everything should be generated. The code that is the same for every API is written once, by hand, and the generated code calls it:

runtime.ts:

import type { IncomingMessage, ServerResponse } from "node:http";

// The hand-written part: what every generated file shares.
// The generated checks call these for the built-in XSD types.

/** A check adds a problem for each way the value is wrong. */
export type Check = (value: unknown, path: string, problems: string[]) => void;

/** The problems of a value that failed its check. */
export class InvalidValue extends Error {
  problems: string[];
  constructor(problems: string[]) {
    super(problems.join("; "));
    this.problems = problems;
  }
}

/** Returns the value when it passes the check, and throws otherwise. */
export function expect<T>(value: unknown, check: Check): T {
  const problems: string[] = [];
  check(value, "input", problems);
  if (problems.length > 0) throw new InvalidValue(problems);
  return value as T;
}

export function checkString(value: unknown, path: string, problems: string[]): value is string {
  if (typeof value === "string") return true;
  problems.push(`${path}: expected a string`);
  return false;
}

export function checkInteger(value: unknown, path: string, problems: string[]): value is number {
  if (Number.isInteger(value)) return true;
  problems.push(`${path}: expected an integer`);
  return false;
}

export function checkDecimal(value: unknown, path: string, problems: string[]): value is number {
  if (Number.isFinite(value)) return true;
  problems.push(`${path}: expected a number`);
  return false;
}

export function checkBoolean(value: unknown, path: string, problems: string[]): value is boolean {
  if (typeof value === "boolean") return true;
  problems.push(`${path}: expected true or false`);
  return false;
}

export function checkDate(value: unknown, path: string, problems: string[]): value is string {
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return true;
  problems.push(`${path}: expected a date such as 2026-10-20`);
  return false;
}

export function checkObject(value: unknown, path: string, problems: string[]): value is Record<string, unknown> {
  if (typeof value === "object" && value !== null && !Array.isArray(value)) return true;
  problems.push(`${path}: expected an object`);
  return false;
}

/** XSD counts characters, not UTF-16 code units. */
export function characterCount(value: string): number {
  return [...value].length;
}

// The client side.

/** Where the client sends its calls; a browser page keeps "/api". */
export const settings = { endpoint: "/api" };

/** Calls a remote function: a query with GET, a command with POST. */
export async function call(name: string, kind: "query" | "command", input: unknown): Promise<unknown> {
  const url = `${settings.endpoint}/${name}`;
  const response = kind === "query"
    ? await fetch(`${url}?input=${encodeURIComponent(JSON.stringify(input))}`)
    : await fetch(url, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      });
  const body = await response.json();
  if (!response.ok) throw new InvalidValue(body.problems ?? [`${name}: ${response.status}`]);
  return body;
}

// The server side.

/** One remote function, as the server sees it. */
export interface Route {
  kind: "query" | "command";
  checkInput: Check;
  checkOutput: Check;
  run: (input: any) => Promise<unknown>;
}

interface Reply {
  status: number;
  body: unknown;
}

/** Answers one request: finds the route, checks the input, runs it, checks the output. */
export async function answer(routes: Record<string, Route>, method: string, url: URL, body: string): Promise<Reply> {
  const name = url.pathname.startsWith("/api/") ? url.pathname.slice(5) : "";
  const route = Object.hasOwn(routes, name) ? routes[name] : undefined;
  if (route === undefined) return { status: 404, body: { problems: [`no function ${url.pathname}`] } };
  const expected = route.kind === "query" ? "GET" : "POST";
  if (method !== expected) return { status: 405, body: { problems: [`use ${expected}`] } };
  let input: unknown;
  try {
    input = JSON.parse(route.kind === "query" ? url.searchParams.get("input") ?? "" : body);
  } catch {
    return { status: 400, body: { problems: ["input: expected JSON"] } };
  }
  const problems: string[] = [];
  route.checkInput(input, "input", problems);
  if (problems.length > 0) return { status: 400, body: { problems } };
  const output = await route.run(input);
  route.checkOutput(output, "output", problems);
  if (problems.length > 0) return { status: 500, body: { problems } };
  return { status: 200, body: output };
}

/** A Node.js request listener for the routes. */
export function serve(routes: Record<string, Route>) {
  return async (request: IncomingMessage, response: ServerResponse) => {
    let body = "";
    for await (const chunk of request) body += chunk;
    const url = new URL(request.url ?? "/", "http://localhost");
    let reply: Reply;
    try {
      reply = await answer(routes, request.method ?? "GET", url, body);
    } catch (error) {
      reply = { status: 500, body: { problems: [String(error)] } };
    }
    response.writeHead(reply.status, { "content-type": "application/json" });
    response.end(JSON.stringify(reply.body));
  };
}

It has three groups of functions:

  • Checks. A check looks at a value that came from JSON, so its type is unknown, and adds a message to problems for each way it is wrong. path says where the value is, such as input.percentage, so the messages can say it too. The checks of the built-in XSD types are here: an xs:integer must be a whole number, an xs:date a string such as 2026-10-20. characterCount counts characters as XSD does: JavaScript's length counts UTF-16 code units, which differ for characters outside the Basic Multilingual Plane.
  • The client. call sends a query as a GET request, with the input as JSON in the URL, and a command as a POST request, with the input as the body.
  • The server. answer finds the function that a request names, checks the method, the input and then the output, and serve makes a request listener for Node.js's http module from it.

The generator

The generator is a library module, with one function for each kind of thing it builds. Each returns elements of the vocabulary:

typescript.xqm:

(:~ Turns an XSD schema and an API description into the tree of a TypeScript program. :)
module namespace ts = "https://example.org/typescript";

declare namespace xs = "http://www.w3.org/2001/XMLSchema";
declare namespace api = "https://example.org/api";

(:~ The built-in XSD types the generator knows: their TypeScript type and check. :)
declare variable $ts:built-in := {
  "xs:string":  { "type": "string",  "check": "checkString" },
  "xs:integer": { "type": "number",  "check": "checkInteger" },
  "xs:decimal": { "type": "number",  "check": "checkDecimal" },
  "xs:boolean": { "type": "boolean", "check": "checkBoolean" },
  "xs:date":    { "type": "string",  "check": "checkDate" }
};

declare function ts:built-in($name as xs:string) as map(*) {
  $ts:built-in($name)
  otherwise error(#ts:unsupported, "The generator has no mapping for " || $name)
};

(:~ The TypeScript type of an XSD type name. :)
declare function ts:type($name as xs:string) as element(type) {
  <type name="{ if (starts-with($name, "xs:")) then ts:built-in($name)?type else $name }"/>
};

(:~ The name of the check function of an XSD type name. :)
declare function ts:check($name as xs:string) as xs:string {
  if (starts-with($name, "xs:"))
  then ts:built-in($name)?check
  else "check" || $name
};

(:~ The statement problems.push(a + b + ...). :)
declare function ts:push($message as element()+) as element(statement) {
  <statement>
    <call method="push"><name>problems</name><binary operator="plus">{ $message }</binary></call>
  </statement>
};

(:~ A statement that adds a problem when the condition holds. :)
declare function ts:problem($condition as element(), $text as xs:string) as element(if) {
  <if>{ $condition }<then>{ ts:push((<name>path</name>, <string>: { $text }</string>)) }</then></if>
};

(:~ The comparison of an expression with the value of a facet. :)
declare function ts:compare($left as element(), $operator as xs:string, $value as xs:string)
    as element(binary) {
  <binary operator="{ $operator }">{ $left }<number>{ $value }</number></binary>
};

(:~ The check of one facet, XSD 1.1 Part 2, 4.3. :)
declare function ts:facet($facet as element()) as element(if) {
  let $value := string($facet/@value)
  let $characters := <call function="characterCount"><name>value</name></call>
  return switch (local-name($facet)) {
    case "pattern" return ts:problem(
      <not><call method="test"><regex flags="u">^(?:{ $value })$</regex><name>value</name></call></not>,
      "must match " || $value)
    case "length" return ts:problem(
      ts:compare($characters, "not-equal", $value), "must have " || $value || " characters")
    case "minLength" return ts:problem(
      ts:compare($characters, "less", $value), "must have at least " || $value || " characters")
    case "maxLength" return ts:problem(
      ts:compare($characters, "greater", $value), "must have at most " || $value || " characters")
    case "minInclusive" return ts:problem(
      ts:compare(<name>value</name>, "less", $value), "must be at least " || $value)
    case "maxInclusive" return ts:problem(
      ts:compare(<name>value</name>, "greater", $value), "must be at most " || $value)
    case "minExclusive" return ts:problem(
      ts:compare(<name>value</name>, "at-most", $value), "must be more than " || $value)
    case "maxExclusive" return ts:problem(
      ts:compare(<name>value</name>, "at-least", $value), "must be less than " || $value)
    default return error(#ts:unsupported,
      "The generator has no check for the facet " || local-name($facet))
  }
};

(:~ A check function: it checks the base type first, then adds a problem for each rule that fails. :)
declare function ts:check-function($name as xs:string, $base as xs:string, $checks as element()*)
    as element(function) {
  <function name="check{ $name }" export="true">
    <parameter name="value"><type name="unknown"/></parameter>
    <parameter name="path"><type name="string"/></parameter>
    <parameter name="problems"><array-type><type name="string"/></array-type></parameter>
    <returns><predicate parameter="value"><type name="{ $name }"/></predicate></returns>
    <body>
      <if>
        <not><call function="{ $base }"><name>value</name><name>path</name><name>problems</name></call></not>
        <then><return><boolean>false</boolean></return></then>
      </if>
      <const name="count"><member name="length"><name>problems</name></member></const>
      { $checks }
      <return>
        <binary operator="equal"><member name="length"><name>problems</name></member><name>count</name></binary>
      </return>
    </body>
  </function>
};

(:~ A simple type: a type alias and a check of its facets. :)
declare function ts:simple-type($type as element(xs:simpleType)) as element()+ {
  let $name := string($type/@name)
  let $base := string($type/xs:restriction/@base)
  let $values := $type/xs:restriction/xs:enumeration/@value ! string()
  let $facets := $type/xs:restriction/(* except xs:enumeration)
  return (
    <type-alias name="{ $name }" export="true">{
      if (exists($values))
      then <union>{ $values ! <string>{ . }</string> }</union>
      else ts:type($base)
    }</type-alias>,
    ts:check-function($name, ts:check($base), (
      if (exists($values)) {
        ts:problem(
          <not><call method="includes"><array>{ $values ! <string>{ . }</string> }</array><name>value</name></call></not>,
          "must be one of " || string-join($values, ", "))
      },
      $facets ! ts:facet(.)
    ))
  )
};

(:~ A property of a complex type: one element of its sequence. :)
declare function ts:property($element as element(xs:element)) as element(property) {
  let $many := $element/@maxOccurs = "unbounded"
  return <property name="{ $element/@name }">{
    if ($element/@minOccurs = "0" and not($many)) { attribute optional { "true" } },
    if ($many)
    then <array-type>{ ts:type($element/@type) }</array-type>
    else ts:type($element/@type)
  }</property>
};

(:~ The check of a property: required, optional, or an array. :)
declare function ts:property-check($element as element(xs:element)) as element() {
  let $name := string($element/@name)
  let $check := ts:check($element/@type)
  let $property := <member name="{ $name }"><name>value</name></member>
  let $path := <binary operator="plus"><name>path</name><string>.{ $name }</string></binary>
  return
    if ($element/@maxOccurs = "unbounded") then
      <if>
        <call method="isArray"><name>Array</name>{ $property }</call>
        <then>
          <statement>
            <call method="forEach">{ $property }
              <arrow>
                <parameter name="item"/>
                <parameter name="index"/>
                <call function="{ $check }">
                  <name>item</name>
                  <binary operator="plus">
                    <name>path</name><string>.{ $name }[</string><name>index</name><string>]</string>
                  </binary>
                  <name>problems</name>
                </call>
              </arrow>
            </call>
          </statement>
        </then>
        <else>{ ts:push((<name>path</name>, <string>.{ $name }: expected an array</string>)) }</else>
      </if>
    else if ($element/@minOccurs = "0") then
      <if>
        <binary operator="not-equal">{ $property }<name>undefined</name></binary>
        <then>
          <statement><call function="{ $check }">{ $property, $path }<name>problems</name></call></statement>
        </then>
      </if>
    else
      <statement><call function="{ $check }">{ $property, $path }<name>problems</name></call></statement>
};

(:~ A complex type: an interface and a check of its properties. :)
declare function ts:complex-type($type as element(xs:complexType)) as element()+ {
  let $name := string($type/@name)
  let $elements := $type/xs:sequence/xs:element
  return (
    <interface name="{ $name }" export="true">{ $elements ! ts:property(.) }</interface>,
    ts:check-function($name, "checkObject", (
      $elements ! ts:property-check(.),
      <for-of name="key">
        <call method="keys"><name>Object</name><name>value</name></call>
        <body>
          <if>
            <not>
              <call method="includes">
                <array>{ $elements/@name ! <string>{ string(.) }</string> }</array>
                <name>key</name>
              </call>
            </not>
            <then>{
              ts:push((<name>path</name>, <string>.</string>, <name>key</name>, <string>: is not allowed</string>))
            }</then>
          </if>
        </body>
      </for-of>
    ))
  )
};

(:~ A module: a comment, the imports its declarations need, and the declarations.
    A name is imported from types.ts when that module declares it,
    and a called function that no module of the program declares from runtime.ts. :)
declare function ts:module($file as xs:string, $comment as xs:string,
    $declarations as element()*, $types as element(module)?) as element(module) {
  let $declared := $declarations/@name
  let $from-types := function($names) {
    distinct-values($names[. = $types/*/@name][not(. = $declared)])
  }
  let $type-names := $from-types($declarations//type/@name)
  let $value-names := $from-types($declarations//(call/@function | name))
  let $runtime-names := sort(distinct-values(
    $declarations//call/@function[not(. = ($declared, $types/*/@name))]))
  return
    <module file="{ $file }">
      <comment>{ $comment }</comment>
      {
        if (exists($runtime-names)) {
          <import from="../runtime.ts">{ $runtime-names ! <name>{ . }</name> }</import>
        },
        if (exists($type-names)) {
          <import from="./types.ts" type-only="true">{ $type-names ! <name>{ . }</name> }</import>
        },
        if (exists($value-names)) {
          <import from="./types.ts">{ $value-names ! <name>{ . }</name> }</import>
        },
        $declarations
      }
    </module>
};

(:~ types.ts: every type of the schema, in the schema's order. :)
declare function ts:types($schema as document-node()) as element(module) {
  ts:module("types.ts", "Generated from the XSD schema by program.xq and print.xsl. Do not edit.",
    $schema/xs:schema/(xs:simpleType | xs:complexType) !
      (if (self::xs:simpleType) then ts:simple-type(.) else ts:complex-type(.)),
    ())
};

(:~ The remote functions of an API description, after checking that their types exist. :)
declare function ts:functions($api as document-node()) as element()* {
  let $schema := doc(resolve-uri($api/api:api/@types, base-uri($api)))
  let $names := $schema/xs:schema/xs:complexType/@name ! string()
  for $function in $api/api:api/(api:query | api:command)
  for $type in ($function/@input, $function/@output)
  where not($type = $names)
  return error(#ts:unknown-type,
    "The function " || $function/@name || " uses the type " || $type
    || ", which the schema doesn't declare"),
  $api/api:api/(api:query | api:command)
};

(:~ The names of the clients that the API description offers, sorted. :)
declare function ts:client-names($api as document-node()) as xs:string* {
  sort(distinct-values($api/api:api/*/@clients ! tokenize(.)))
};

(:~ The module of one client, such as browser.ts: one async function for
    each remote function that the client offers. :)
declare function ts:client($api as document-node(), $client as xs:string, $types as element(module))
    as element(module) {
  ts:module($client || ".ts", "Generated from the API description by program.xq and print.xsl. Do not edit.",
    for $function in ts:functions($api)[tokenize(@clients) = $client]
    let $input := string($function/@input)
    let $output := string($function/@output)
    return
      <function name="{ $function/@name }" export="true" async="true">
        <doc>{ normalize-space($function) }</doc>
        <parameter name="input"><type name="{ $input }"/></parameter>
        <returns><type name="Promise"><type name="{ $output }"/></type></returns>
        <body>
          <statement>
            <call function="expect"><type name="{ $input }"/><name>input</name><name>{ ts:check($input) }</name></call>
          </statement>
          <const name="output">
            <await>
              <call function="call">
                <string>{ string($function/@name) }</string>
                <string>{ local-name($function) }</string>
                <name>input</name>
              </call>
            </await>
          </const>
          <return>
            <call function="expect"><type name="{ $output }"/><name>output</name><name>{ ts:check($output) }</name></call>
          </return>
        </body>
      </function>,
    $types)
};

(:~ server.ts: the interface the service implements, and a request handler
    that checks every function, whichever client calls it. :)
declare function ts:server($api as document-node(), $types as element(module)) as element(module) {
  let $functions := ts:functions($api)
  return ts:module("server.ts", "Generated from the API description by program.xq and print.xsl. Do not edit.", (
    <interface name="Implementation" export="true">
      <doc>What the service does: one method for each remote function.</doc>
      {
        for $function in $functions
        return
          <method name="{ $function/@name }">
            <doc>{ normalize-space($function) }</doc>
            <parameter name="input"><type name="{ $function/@input }"/></parameter>
            <returns><type name="Promise"><type name="{ $function/@output }"/></type></returns>
          </method>
      }
    </interface>,
    <function name="createHandler" export="true">
      <doc>A Node.js request listener that checks every input and output.</doc>
      <parameter name="implementation"><type name="Implementation"/></parameter>
      <body>
        <return>
          <call function="serve">
            <object>{
              for $function in $functions
              return
                <entry name="{ $function/@name }">
                  <object>
                    <entry name="kind"><string>{ local-name($function) }</string></entry>
                    <entry name="checkInput"><name>{ ts:check($function/@input) }</name></entry>
                    <entry name="checkOutput"><name>{ ts:check($function/@output) }</name></entry>
                    <entry name="run">
                      <arrow>
                        <parameter name="input"/>
                        <call method="{ $function/@name }"><name>implementation</name><name>input</name></call>
                      </arrow>
                    </entry>
                  </object>
                </entry>
            }</object>
          </call>
        </return>
      </body>
    </function>
  ), $types)
};

Let's read it from the top.

  • $ts:built-in maps the built-in XSD types that the API uses to their TypeScript type and their check. ts:type makes a type element for a type name, and ts:check gives the name of its check; a name of the schema keeps its name. A built-in type that isn't in the map is an error, not a guess.
  • ts:push and ts:problem build the statement that every rule ends in: problems.push(...), with the message, inside an if.
  • ts:facet turns one facet into one if. A pattern becomes a regular expression with ^(?: and )$ around it, because an XSD pattern must match the whole value (XSD 1.1 Part 2, appendix G). Length facets count characters. A facet the generator doesn't know is an error: a generator that left a rule out without saying so would make code that accepts what the schema refuses.
  • ts:check-function builds the shape that every check shares: check the base type, remember the number of problems, run the rules, and return whether none was added. ts:simple-type gives it the facets of an xs:simpleType, after a type alias; with enumeration facets, the type is a union of string literals, so the TypeScript compiler knows the values too.
  • ts:property and ts:property-check handle one element of a sequence: required, optional, or an array. ts:complex-type builds an interface and a check, which also refuses properties that the schema doesn't have.
  • ts:module puts declarations into a module, with the imports they need, which it finds by asking the declarations: a type or a name that types.ts declares is imported from there, and a function that is called but declared nowhere in the program comes from runtime.ts. Nobody keeps a list of imports by hand.
  • ts:types builds types.ts: every type of the schema, in the order of the schema. ts:functions returns the functions of the description, after it has checked that their types are complex types of the schema. ts:client-names lists the names in the clients attributes, once each and sorted, and ts:client builds the module of one of them, such as browser.ts, with the functions that name it. ts:server builds server.ts, with every function: whichever client sends a request, the service checks it.

The XSD types are read as XML, with paths such as xs:restriction/@base. This is the schema as a document, not the compiled schema that sxml validate uses: a generator needs to know how the types are written, and paths are the simplest way to ask.

Trying the pieces

As with any library module, try the functions on their own first. One facet becomes one if, with the comparison and the message:

$ sxml xquery --method=xml --expression='
    import module namespace ts = "https://example.org/typescript" at "typescript.xqm";
    declare namespace xs = "http://www.w3.org/2001/XMLSchema";
    declare option output:indent "yes";
    ts:facet(<xs:maxInclusive value="100"/>)'
<if>
  <binary operator="greater">
    <name>value</name>
    <number>100</number>
  </binary>
  <then>
    <statement>
      <call method="push">
        <name>problems</name>
        <binary operator="plus">
          <name>path</name>
          <string>: must be at most 100</string>
        </binary>
      </call>
    </statement>
  </then>
</if>

The tree is longer than the line of code it stands for, but each part has a name, so a test can ask for a part instead of comparing text. The operator and the number of a maxLength facet:

$ sxml xquery --expression='
    import module namespace ts = "https://example.org/typescript" at "typescript.xqm";
    declare namespace xs = "http://www.w3.org/2001/XMLSchema";
    ts:facet(<xs:maxLength value="64"/>)/binary ! (@operator, number) ! string()'
greater 64

Given to the printer of the next section, which reads a document from standard input when its input is -, the tree of a facet is one line of TypeScript:

$ sxml xquery --method=xml --expression='
    import module namespace ts = "https://example.org/typescript" at "typescript.xqm";
    declare namespace xs = "http://www.w3.org/2001/XMLSchema";
    ts:facet(<xs:pattern value="[a-z0-9][a-z0-9-]*"/>)' | sxml transform print.xsl -
if (!/^(?:[a-z0-9][a-z0-9-]*)$/u.test(value)) problems.push(path + ": must match [a-z0-9][a-z0-9-]*");

A simple type is a type alias and a check function. Put into a module, the printer writes them as a file would have them:

$ sxml xquery --method=xml --expression='
    import module namespace ts = "https://example.org/typescript" at "typescript.xqm";
    declare namespace xs = "http://www.w3.org/2001/XMLSchema";
    <module>{ ts:simple-type(doc("vane-api.xsd")//xs:simpleType[@name = "Percentage"]) }</module>' | sxml transform print.xsl -
export type Percentage = number;

export function checkPercentage(value: unknown, path: string, problems: string[]): value is Percentage {
  if (!checkInteger(value, path, problems)) return false;
  const count = problems.length;
  if (value < 0) problems.push(path + ": must be at least 0");
  if (value > 100) problems.push(path + ": must be at most 100");
  return problems.length === count;
}

The facets minInclusive and maxInclusive became the two if statements. The checks are type predicates: when checkInteger returns true, TypeScript knows that value is a number in the lines after it. A complex type becomes an interface:

$ sxml xquery --method=xml --expression='
    import module namespace ts = "https://example.org/typescript" at "typescript.xqm";
    declare namespace xs = "http://www.w3.org/2001/XMLSchema";
    <module>{ ts:complex-type(doc("vane-api.xsd")//xs:complexType[@name = "FlagStatus"]) }</module>' | sxml transform print.xsl -
export interface FlagStatus {
  flag: FlagKey;
  state: State;
  failedChecks: FailureCount;
  retryAfter?: Seconds;
}

export function checkFlagStatus(value: unknown, path: string, problems: string[]): value is FlagStatus {
  if (!checkObject(value, path, problems)) return false;
  const count = problems.length;
  checkFlagKey(value.flag, path + ".flag", problems);
  checkState(value.state, path + ".state", problems);
  checkFailureCount(value.failedChecks, path + ".failedChecks", problems);
  if (value.retryAfter !== undefined) checkSeconds(value.retryAfter, path + ".retryAfter", problems);
  for (const key of Object.keys(value)) {
    if (!["flag", "state", "failedChecks", "retryAfter"].includes(key)) problems.push(path + "." + key + ": is not allowed");
  }
  return problems.length === count;
}

And a facet without a translation stops the generator:

$ sxml xquery --expression='
    import module namespace ts = "https://example.org/typescript" at "typescript.xqm";
    ts:facet(<xs:totalDigits value="5"/>)'
typescript.xqm:73:20: ts:unsupported: The generator has no check for the facet totalDigits

The tree of the whole program

A main module puts it together. It reads the description and its schema, and returns a program with a module for each file:

program.xq:

(:~ The tree of the TypeScript program for an API description and its schema:
    the types, one module for each client, and the server. :)
import module namespace ts = "https://example.org/typescript" at "typescript.xqm";

declare option output:method "xml";
declare option output:indent "yes";

declare variable $api-file as xs:string external := "api.xml";

let $api := doc($api-file)
let $types := ts:types(doc(resolve-uri($api/*/@types, base-uri($api))))
return <program>{
  $types,
  ts:client-names($api) ! ts:client($api, ., $types),
  ts:server($api, $types)
}</program>

The output options in the prolog, as in Writing Results, write the tree as indented XML, so it is easy to read and to compare. The modules are built in the order of the files that use each other: types.ts first, because ts:module looks into it to find the imports of the others. The clients come next, one for each name, and the server last.

$ sxml xquery --output=program.xml program.xq
$ sxml xpath --context-item=program.xml '/program/module ! `{ @file }: { count(descendant::*) } elements`'
"types.ts: 1427 elements"
"browser.ts: 62 elements"
"console.ts: 62 elements"
"node.ts: 62 elements"
"server.ts: 138 elements"

The tree is a document, so everything in this book about querying documents applies to the program. Every exported function, by file:

$ sxml xpath --context-item=program.xml '/program/module ! `{ @file }: { string-join(function[@export]/@name, ", ") }`'
"types.ts: checkFlagKey, checkOwner, checkEnvironment, checkState, checkPercentage, checkFailureCount, checkSeconds, checkRule, checkFlagConfig, checkFlagsRequest, checkFlagSet, checkAttribute, checkEvaluationContext, checkEvaluation, checkEvaluations, checkStatusRequest, checkFlagStatus, checkRolloutRequest, checkKillRequest, checkKillResult"
"browser.ts: evaluateFlags, getStatus"
"console.ts: rollOutFlag, killFlag"
"node.ts: getFlags, getStatus"
"server.ts: createHandler"

The checks that use checkFlagKey, which are the ones a change to the type of a flag key reaches:

$ sxml xpath --context-item=program.xml '//function[.//(call/@function | name) = "checkFlagKey"]/@name ! string()'
"checkFlagConfig"
"checkEvaluation"
"checkStatusRequest"
"checkFlagStatus"
"checkRolloutRequest"
"checkKillRequest"
"checkKillResult"

And the imports of the frontend SDK's client, which ts:module found in the tree:

$ sxml xpath --context-item=program.xml '/program/module[@file = "browser.ts"]/import ! `{ @from }: { string-join(name, ", ") }`'
"../runtime.ts: call, expect"
"./types.ts: EvaluationContext, Evaluations, StatusRequest, FlagStatus"
"./types.ts: checkEvaluationContext, checkEvaluations, checkStatusRequest, checkFlagStatus"

None of these needs to know how TypeScript writes a function or an import; asked of the text, each would need a parser.

The rule the schema couldn't check

ts:functions checks that each function's types exist. With a typo in a type name, the generator stops with its own error, and writes no file, because --output writes its file only when the whole query succeeds:

$ sed 's/output="KillResult"/output="KillResults"/' api.xml > typo.xml
$ sxml xquery --param=api-file=typo.xml --output=typo-program.xml program.xq
typescript.xqm:248:10: ts:unknown-type: The function killFlag uses the type KillResults, which the schema doesn't declare
$ test -e typo-program.xml || echo "no file written"
no file written

Next, let's print the tree.