Newer
Older
percord / api / scripts / generate_openapi_schema.ts
// https://mermade.github.io/openapi-gui/#
// https://editor.swagger.io/
import path from "path";
import fs from "fs";
import * as TJS from "typescript-json-schema";
import "missing-native-js-functions";

const settings: TJS.PartialArgs = {
	required: true,
	ignoreErrors: true,
	excludePrivate: true,
	defaultNumberType: "integer",
	noExtraProps: true,
	defaultProps: false
};
const compilerOptions: TJS.CompilerOptions = {
	strictNullChecks: false
};
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
var specification = JSON.parse(fs.readFileSync(openapiPath, { encoding: "utf8" }));

async function generateSchemas() {
	const program = TJS.getProgramFromFiles([path.join(__dirname, "..", "..", "util", "src", "index.ts")], compilerOptions);
	const generator = TJS.buildGenerator(program, settings);

	const schemas = [
		"Application",
		"Attachment",
		"Message",
		"AuditLog",
		"Ban",
		"Channel",
		"Emoji",
		"Guild",
		"Invite",
		"ReadState",
		"Recipient",
		"Relationship",
		"Role",
		"Sticker",
		"Team",
		"TeamMember",
		"Template",
		"VoiceState",
		"Webhook",
		"User",
		"UserPublic"
	];

	// @ts-ignore
	const definitions = combineSchemas({ schemas, generator, program });

	for (const key in definitions) {
		specification.components.schemas[key] = definitions[key];
		delete definitions[key].additionalProperties;
	}
}

function combineSchemas(opts: { program: TJS.Program; generator: TJS.JsonSchemaGenerator; schemas: string[] }) {
	var definitions: any = {};

	for (const name of opts.schemas) {
		const part = TJS.generateSchema(opts.program, name, settings, [], opts.generator as TJS.JsonSchemaGenerator);
		if (!part) continue;

		definitions = { ...definitions, ...part.definitions, [name]: { ...part, definitions: undefined, $schema: undefined } };
	}

	return definitions;
}

function generateBodies() {
	const program = TJS.getProgramFromFiles([path.join(__dirname, "..", "src", "schema", "index.ts")], compilerOptions);
	const generator = TJS.buildGenerator(program, settings);

	const schemas = [
		"BanCreateSchema",
		"DmChannelCreateSchema",
		"ChannelModifySchema",
		"ChannelGuildPositionUpdateSchema",
		"ChannelGuildPositionUpdateSchema",
		"EmojiCreateSchema",
		"GuildCreateSchema",
		"GuildUpdateSchema",
		"GuildTemplateCreateSchema",
		"GuildUpdateWelcomeScreenSchema",
		"InviteCreateSchema",
		"MemberCreateSchema",
		"MemberNickChangeSchema",
		"MemberChangeSchema",
		"MessageCreateSchema",
		"RoleModifySchema",
		"TemplateCreateSchema",
		"TemplateModifySchema",
		"UserModifySchema",
		"UserSettingsSchema",
		"WidgetModifySchema"
	];

	// @ts-ignore
	const definitions = combineSchemas({ schemas, generator, program });

	for (const key in definitions) {
		specification.components.requestBodies[key] = {
			content: {
				"application/json": { schema: definitions[key] }
			},
			description: ""
		};

		delete definitions[key].additionalProperties;
		delete definitions[key].$schema;
	}
}

function addDefaultResponses() {
	Object.values(specification.paths).forEach((path: any) =>
		Object.values(path).forEach((request: any) => {
			if (!request.responses?.["401"]) {
				request.responses["401"] = {
					description: "Unauthorized",
					content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
				};
			}
			if (!request.responses?.["429"]) {
				request.responses["429"] = {
					description: "Rate limit exceeded",
					content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } },
					headers: {
						"X-RateLimit-Bucket": {
							description:
								"A unique string denoting the rate limit being encountered (non-inclusive of major parameters in the route path)",
							schema: { type: "string" }
						},
						"X-Rate-Limit-Limit": {
							description: "The number of allowed requests in the current period",
							schema: {
								type: "integer"
							}
						},
						"X-Rate-Limit-Remaining": {
							description: "The number of remaining requests in the current period",
							schema: {
								type: "integer"
							}
						},
						"X-Rate-Limit-Reset": {
							description: "Date when current period is over in seconds since the Unix epoch",
							schema: {
								type: "integer"
							}
						},
						"X-Rate-Limit-Reset-After": {
							description: "Number of seconds when current period will reset (can have decimal)",
							schema: {
								type: "number"
							}
						},
						"Retry-After": {
							description: "Same as X-Rate-Limit-Reset-After but an integer",
							schema: {
								type: "integer"
							}
						},
						"X-RateLimit-Global": {
							description: "Indicates whether or not all requests from your ip are rate limited",
							schema: {
								type: "boolean"
							}
						}
					}
				};
			}
		})
	);
}

function main() {
	addDefaultResponses();
	generateSchemas();
	specification = JSON.parse(JSON.stringify(specification).replaceAll("#/definitions", "#/components/schemas"));

	generateBodies();

	fs.writeFileSync(
		openapiPath,
		JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/requestBodies").replaceAll("bigint", "number")
	);
}

main();