Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: use autodoc inventory for automatic typegen #1

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
*.*map
node_modules

yarn-error.log
yarn-error.log

docs/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Discord API Types
# Eludris API Types

[![GitHub](https://img.shields.io/github/license/eludris-community/eludris-api-types?style=flat-square)](https://github.com/eludris-community/eludris-api-types/blob/main/LICENSE.md)
[![npm](https://img.shields.io/npm/v/eludris-api-types?color=crimson&logo=npm&style=flat-square)](https://www.npmjs.com/package/eludris-api-types)
[![docs](https://img.shields.io/badge/docs-eludris--api--types-black?style=flat-square&logo=vercel)](https://next-eludris-api-types.vercel.app/)


Simple type definitions for the [Eludris API](https://eludris.github.io/docs).
Simple type definitions and routes for the [Eludris API](https://eludevs.pages.dev/).

## Installation

Expand All @@ -14,4 +14,4 @@ Install with [npm](https://www.npmjs.com/) / [yarn](https://yarnpkg.com) / [pnpm
npm install eludris-api-types
yarn add eludris-api-types
pnpm add eludris-api-types
```
```
344 changes: 344 additions & 0 deletions autotype/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
import { stat } from "fs/promises";
import { createInterface } from "readline/promises";
import {
Project,
SourceFile,
} from "ts-morph";
import {
EnumInfo,
Item,
ItemInfo,
ItemType,
RouteInfo,
StructInfo,
VariantType,
} from "./types";

import { snakeCase } from "lodash";

import { argv } from "process";

main(argv[2], argv[3]);

const converters: Map<RegExp, (str: string) => string> = new Map([
[/^String$/, (_) => "string"],
[/^str$/, (_) => "string"],
[/^(u|i)(size|\d\d)$/, (str) => "number"],
[
/^Option<.+>/,
(str) =>
convertType(str.replace(/^(?:Option<)+([^<>]+)>*$/, "$1")) + " | null",
],
[/^bool$/, (_) => "boolean"],
[/^TempFile$/, (_) => "unknown"],
[/^Box<.*>/, (str) => convertType(str.replace(/^Box<(.+)>$/, "$1"))],
[/^IpAddr$/, (_) => "string"],
[/^Vec<.*>/, (str) => convertType(str.replace(/^Vec<(.+)>$/, "$1[]"))],
]);

function convertType(type: string): string {
for (const [regex, converter] of converters) {
if (regex.test(type)) {
return converter(type);
}
}
return type;
}

function switchCase(content: string, newCase: string | null): string {
if (newCase === "SCREAMING_SNAKE_CASE") {
return `"${snakeCase(content).toUpperCase()}"`;
} else if (newCase === "UPPERCASE") {
return content.toUpperCase();
}
return content;
}

async function main(inventoryIndex: string, output: string) {
const inventory: {
version: string;
items: string[];
} = await (await fetch(`${inventoryIndex}/index.json`)).json();

let exists = true;
await stat(`${output}/v${inventory["version"]}.ts`,
).catch(() => {
exists = false;
});


if (exists) {
console.log(
`[INFO] v${inventory["version"]}.ts already exists, do you want to overwrite it?`
);
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await rl.question("Overwrite? (y/n) ");
rl.close();
if (answer !== "y") {
return;
}
}

const project = new Project();
const sourceFile = project.createSourceFile(
`${output}/v${inventory["version"]}.ts`,
undefined,
{
overwrite: true,
}
);

let items: ItemInfo[] = await Promise.all(
inventory.items.sort().map(async (url) => {
const response = await fetch(`${inventoryIndex}/${url}`);
return await response.json();
})
);

const builder = new Builder(sourceFile, items);
builder.buildTypes();

sourceFile.formatText({
indentSize: 2,
convertTabsToSpaces: true,
ensureNewLineAtEndOfFile: true,
});

sourceFile.organizeImports();
await sourceFile.save();
await project.save();
}

class Builder {
sourceFile: SourceFile;
routes: ItemInfo<Item>[] = [];
itemInfos: ItemInfo[] = [];

constructor(sourceFile: SourceFile, itemInfos: ItemInfo[]) {
this.sourceFile = sourceFile;
this.itemInfos = itemInfos;
}

convertDoc(doc: string) {
for (const route of this.routes) {
doc = doc.replace(route.name, convertToCamelCase(route.name));
}
return doc.replace(/\[`(.+?)`\]/g, "{@link $1}");
}

buildTypes() {
for (const itemInfo of this.itemInfos) {
if (itemInfo.item.type === ItemType.Route) {
this.routes.push(itemInfo);
}
}

for (const itemInfo of this.itemInfos) {
if (itemInfo.item.type === ItemType.Struct) {
this.handleStruct(itemInfo as ItemInfo<StructInfo>);
} else if (itemInfo.item.type === ItemType.Enum) {
this.handleEnum(itemInfo as ItemInfo<EnumInfo>);
} else if (itemInfo.item.type === ItemType.Route) {
this.handleRoute(itemInfo as ItemInfo<RouteInfo>);
}
}

this.sourceFile.addStatements((writer) => {
writer.writeLine("export const ROUTES = {");
for (const route of this.routes) {
//docs
writer.writeLine(formatDocString(route.doc));

writer.writeLine(`${convertToCamelCase(route.name)},`);
}
writer.writeLine("};");
});
}

handleStruct(itemInfo: ItemInfo<StructInfo>) {
const { item, name, doc } = itemInfo;
const struct = this.sourceFile.addInterface({
name,
isExported: false,
docs: [this.convertDoc(doc)],
extends: item.fields.filter((f) => f.flattened).map((f) => f.field_type),
});
struct.setIsExported(true);

for (const field of item.fields.filter((f) => !f.flattened)) {
const { name, doc, field_type } = field;

const property = struct.addProperty({
name,
type: convertType(field_type),
hasQuestionToken: field.ommitable,
});

if (doc) {
property.addJsDoc(this.convertDoc(doc));
}
}
}

handleEnum(itemInfo: ItemInfo<EnumInfo>) {
const { item, name, doc } = itemInfo;
const union = this.sourceFile.addTypeAlias({
name,
isExported: true,
docs: [this.convertDoc(doc)],
type: item.variants.map((v) => name + v.name).join(" | "),
});
union.setIsExported(true);

for (const variant of item.variants) {
const variantInterface = this.sourceFile.addInterface({
name: name + variant.name,
});
variantInterface.setIsExported(true);

if (variant.type === VariantType.Unit) {
if (item.tag) {
const property = variantInterface.addProperty({
name: item.tag,
type: switchCase(`"${variant.name}"`, item.rename_all),
});

if (variant.doc) {
property.addJsDoc(this.convertDoc(variant.doc));
}
} else {
variantInterface.remove();
const typeAlias = this.sourceFile.addTypeAlias({
name: name + variant.name,
type: switchCase(`"${variant.name}"`, item.rename_all),
});
if (variant.doc) {
typeAlias.addJsDoc(this.convertDoc(variant.doc));
}

continue;
}
} else if (variant.type === VariantType.Tuple) {
if (!item.tag) {
continue;
}

const property = variantInterface.addProperty({
name: item.tag,
type: switchCase(`"${variant.name}"`, item.rename_all),
});

if (variant.doc) {
property.addJsDoc(this.convertDoc(variant.doc));
}

if (item.content) {
variantInterface.addProperty({
name: item.content,
type: convertType(variant.field_type),
});
}
} else if (variant.type === VariantType.Struct) {
if (!item.tag) {
continue;
}

const property = variantInterface.addProperty({
name: item.tag,
type: switchCase(`"${variant.name}"`, item.rename_all),
});

if (variant.doc) {
property.addJsDoc(this.convertDoc(variant.doc));
}
let toExtend: string[] = [];
variantInterface.addMember((writer) => {
writer.writeLine(`${item.content}: {`);
for (const field of variant.fields) {
if (field.flattened) {
toExtend.push(convertType(field.field_type));
} else {
if (field.doc) {
writer.writeLine(formatDocString(field.doc));
}

writer.write(`${field.name}`);

if (field.ommitable) {
writer.write("?");
}

writer.write(": ");

writer.write(convertType(field.field_type));
writer.write(";\n");
}
}
writer.writeLine("}");
});
for (const type of toExtend) {
variantInterface.addExtends(type);
}

}
}
}

handleRoute(itemInfo: ItemInfo<RouteInfo>) {
let params = [
{
name: "baseUrl",
param_type: "string",
},
];
params = params.concat(itemInfo.item.path_params);
params = params.concat(itemInfo.item.query_params);
const camelCaseName = itemInfo.name.replace(/_(\w)/g, (_, c) =>
c.toUpperCase()
);

const routeFunction = this.sourceFile.addFunction({
name: camelCaseName,
parameters: params.map((p) => ({
name: convertToCamelCase(p.name),
type: convertType(p.param_type),
})),

returnType: "string",
});

routeFunction.addStatements((writer) => {
writer.write("return `${baseUrl}");
const route = itemInfo.item.route.replace(/\/?\??<.+>/g, "");
writer.write(`${route}/`);

for (const pathParam of itemInfo.item.path_params) {
const camelCaseParamName = convertToCamelCase(pathParam.name);
writer.write(`\${${camelCaseParamName}}/`);
}

if (itemInfo.item.query_params.length > 0) {
writer.write("?");
}

let queryParams = itemInfo.item.query_params.map(
(p) => `\${${convertToCamelCase(p.name)}}`
);
writer.write(queryParams.join("&"));

writer.write("`;");
});
}
}

function convertToCamelCase(str: string) {
return str.replace(/_(\w)/g, (_, c) => c.toUpperCase());
}

function formatDocString(doc: string): string {
const formattedDoc = " * " + doc.split("\n").join("\n * ").replace(/\* \n/g, "*\n");
return `/**\n${formattedDoc}\n */`;
}
Loading