Skip to content

Commit

Permalink
Add command to print simplified visualization of schemas (#103)
Browse files Browse the repository at this point in the history
* Add printer

* Wrap long descriptions in the printer

* Add a debug print command
  • Loading branch information
zephraph authored Jan 26, 2025
1 parent 90db804 commit 342cb9d
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 0 deletions.
9 changes: 9 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ depends = ["gen:rust"]
sources = ["schemas/*", "scripts/generate-schema.ts"]
outputs = ["src/clients/deno/schemas.ts"]

## Debug

[tasks."print-schema"]
description = "Prints a simplified version of the schema"
usage = '''
arg "[schema]" help="The schema to print; prints all if not provided"
'''
run = "deno run -A scripts/generate-schema/debug.ts {{arg(name=\"schema\")}}"

## Publishing

[tasks."verify-publish:deno"]
Expand Down
34 changes: 34 additions & 0 deletions scripts/generate-schema/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { parseSchema } from "./parser.ts";
import { printDocIR } from "./printer.ts";

const schemaFiles = [
"WebViewOptions.json",
"WebViewRequest.json",
"WebViewResponse.json",
"WebViewMessage.json",
];

async function main() {
const targetSchema = Deno.args[0];
const filesToProcess = targetSchema
? [targetSchema.endsWith(".json") ? targetSchema : `${targetSchema}.json`]
: schemaFiles;

for (const schemaFile of filesToProcess) {
if (!schemaFiles.includes(schemaFile)) {
console.error(`Invalid schema file: ${schemaFile}`);
console.error(`Available schemas: ${schemaFiles.join(", ")}`);
Deno.exit(1);
}

console.log(`Schema: ${schemaFile}`);

const schema = JSON.parse(await Deno.readTextFile(`schemas/${schemaFile}`));
const doc = parseSchema(schema);
console.log(printDocIR(doc));
}
}

if (import.meta.main) {
main().catch(console.error);
}
219 changes: 219 additions & 0 deletions scripts/generate-schema/printer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { match } from "npm:ts-pattern";
import { Doc, Node } from "./parser.ts";
import {
blue,
bold,
dimmed,
type Formatter,
green,
mix,
yellow,
} from "jsr:@coven/terminal";

const comment = mix(yellow, dimmed);
const string = green;
const type = blue;
const kind = bold;

function wrapText(
text: string,
maxLength: number,
indent: string,
formatter?: Formatter,
): string[] {
const words = text.split(" ");
const lines: string[] = [];
let currentLine = "";

for (const word of words) {
if (currentLine.length + word.length + 1 <= maxLength) {
currentLine += (currentLine.length === 0 ? "" : " ") + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
if (currentLine.length > 0) {
lines.push(currentLine);
}
return lines
.map((line) => formatter ? formatter`${line}` : line)
.map((line, i) => i === 0 ? line : indent + line);
}

function printNodeIR(
node: Node,
prefix: string = "",
isLast: boolean = true,
): string {
const marker = isLast ? "└── " : "├── ";
const childPrefix = prefix + (isLast ? " " : "│ ");

return prefix + marker + match(node)
.with({ type: "reference" }, ({ name }) => kind`${name}\n`)
.with({ type: "descriminated-union" }, ({ descriminator, members }) => {
let output = kind`discriminated-union` + ` (by ${descriminator})\n`;
Object.entries(members).forEach(([name, properties], index) => {
output += printNodeIR(
{ type: "object", name, properties },
childPrefix,
index === Object.values(members).length - 1,
);
});
return output;
})
.with({ type: "intersection" }, ({ members }) => {
let output = kind`intersection\n`;
members.forEach((member, index) => {
output += printNodeIR(
member,
childPrefix,
index === members.length - 1,
);
});
return output;
})
.with({ type: "union" }, ({ members }) => {
let output = kind`union\n`;
members.forEach((member, index) => {
output += printNodeIR(
member,
childPrefix,
index === members.length - 1,
);
});
return output;
})
.with({ type: "object" }, ({ name, properties }) => {
let output = kind`${name ?? "object"}\n`;
properties.forEach(({ key, required, value, description }, index) => {
description = description?.split("\n")[0];
const propDesc = `${key}${required ? "" : "?"}: `;

if (description) {
const wrappedDesc = wrapText(
description,
96,
childPrefix + "│ ",
comment,
)
.join("\n");
output += childPrefix + "│ " + `${wrappedDesc}\n`;
output += childPrefix +
(index === properties.length - 1 ? "└── " : "├── ");
} else {
output += childPrefix +
(index === properties.length - 1 ? "└── " : "├── ");
}

const valueStr = match(value)
.with(
{ type: "boolean" },
({ optional }) => type`boolean${optional ? "?" : ""}`,
)
.with(
{ type: "string" },
({ optional }) => type`string${optional ? "?" : ""}`,
)
.with({ type: "literal" }, ({ value }) => string`"${value}"`)
.with(
{ type: "int" },
({ minimum, maximum }) =>
type`int${minimum !== undefined ? ` min(${minimum})` : ""}${
maximum !== undefined ? ` max(${maximum})` : ""
}`,
)
.with(
{ type: "float" },
({ minimum, maximum }) =>
type`float${minimum !== undefined ? ` min(${minimum})` : ""}${
maximum !== undefined ? ` max(${maximum})` : ""
}`,
)
.with(
{ type: "record" },
({ valueType }) => type`record<string, ${valueType}>`,
)
.with({ type: "unknown" }, () => "unknown")
.with({ type: "reference" }, ({ name }) => name)
.otherwise(() => {
output += propDesc + "\n";
return printNodeIR(
value,
childPrefix + (index === properties.length - 1 ? " " : "│ "),
true,
);
});

if (
value.type === "union" || value.type === "intersection" ||
value.type === "descriminated-union" || value.type === "object"
) {
output += valueStr;
} else {
output += propDesc + valueStr + "\n";
}
});
return output;
})
.with({ type: "enum" }, ({ members }) => {
return kind`enum` + `[${members.join(",")}]\n`;
})
.with(
{ type: "record" },
({ valueType }) => `record<string, ${valueType}>\n`,
)
.with(
{ type: "boolean" },
({ optional }) => `boolean${optional ? "?" : ""}\n`,
)
.with(
{ type: "string" },
({ optional }) => `string${optional ? "?" : ""}\n`,
)
.with({ type: "literal" }, ({ value }) => `"${value}"\n`)
.with(
{ type: "int" },
({ minimum, maximum }) =>
`int${minimum !== undefined ? ` min(${minimum})` : ""}${
maximum !== undefined ? ` max(${maximum})` : ""
}\n`,
)
.with(
{ type: "float" },
({ minimum, maximum }) =>
`float${minimum !== undefined ? ` min(${minimum})` : ""}${
maximum !== undefined ? ` max(${maximum})` : ""
}\n`,
)
.with({ type: "unknown" }, () => "unknown\n")
.exhaustive();
}

export function printDocIR(doc: Doc): string {
let result = `${doc.title}\n`;
const description = doc.description?.split("\n")[0];
if (description) {
const wrappedDesc = wrapText(description, 96, "│ description: ", comment)
.join("\n");
result += "│ description: " + `${wrappedDesc}\n`;
}
result += printNodeIR(doc.root, "", true);
for (const [name, definition] of Object.entries(doc.definitions)) {
result += `${name}\n`;
const description = definition.description?.split("\n")[0];
if (description) {
const wrappedDesc = wrapText(
description,
96,
"│ description: ",
comment,
).join(
"\n",
);
result += "│ description: " + `${wrappedDesc}\n`;
}
result += printNodeIR(definition, "", true);
}
return result;
}

0 comments on commit 342cb9d

Please sign in to comment.