summary refs log tree commit diff
path: root/scripts/generate_openapi.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/generate_openapi.js')
-rw-r--r--scripts/generate_openapi.js137
1 files changed, 137 insertions, 0 deletions
diff --git a/scripts/generate_openapi.js b/scripts/generate_openapi.js
new file mode 100644
index 00000000..9624a5b9
--- /dev/null
+++ b/scripts/generate_openapi.js
@@ -0,0 +1,137 @@
+// https://mermade.github.io/openapi-gui/#
+// https://editor.swagger.io/
+const getRouteDescriptions = require("../jest/getRouteDescriptions");
+const path = require("path");
+const fs = require("fs");
+require("missing-native-js-functions");
+
+const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
+const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json");
+const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
+const specification = JSON.parse(fs.readFileSync(openapiPath, { encoding: "utf8" }));
+
+function combineSchemas(schemas) {
+	let definitions = {};
+
+	for (const name in schemas) {
+		definitions = {
+			...definitions,
+			...schemas[name].definitions,
+			[name]: { ...schemas[name], definitions: undefined, $schema: undefined }
+		};
+	}
+
+	for (const key in definitions) {
+		specification.components.schemas[key] = definitions[key];
+		delete definitions[key].additionalProperties;
+		delete definitions[key].$schema;
+		const definition = definitions[key];
+
+		if (typeof definition.properties === "object") {
+			for (const property of Object.values(definition.properties)) {
+				if (Array.isArray(property.type)) {
+					if (property.type.includes("null")) {
+						property.type = property.type.find((x) => x !== "null");
+						property.nullable = true;
+					}
+				}
+			}
+		}
+	}
+
+	return definitions;
+}
+
+function getTag(key) {
+	return key.match(/\/([\w-]+)/)[1];
+}
+
+function apiRoutes() {
+	const routes = getRouteDescriptions();
+
+	const tags = Array.from(routes.keys()).map((x) => getTag(x));
+	specification.tags = [...specification.tags.map((x) => x.name), ...tags].unique().map((x) => ({ name: x }));
+
+	routes.forEach((route, pathAndMethod) => {
+		const [p, method] = pathAndMethod.split("|");
+		const path = p.replace(/:(\w+)/g, "{$1}");
+
+		let obj = specification.paths[path]?.[method] || {};
+		if (!obj.description) {
+			const permission = route.permission ? `##### Requires the \`\`${route.permission}\`\` permission\n` : "";
+			const event = route.test?.event ? `##### Fires a \`\`${route.test?.event}\`\` event\n` : "";
+			obj.description = permission + event;
+		}
+		if (route.body) {
+			obj.requestBody = {
+				required: true,
+				content: {
+					"application/json": {
+						schema: { $ref: `#/components/schemas/${route.body}` }
+					}
+				}
+			}.merge(obj.requestBody);
+		}
+		if (!obj.responses) {
+			obj.responses = {
+				default: {
+					description: "not documented"
+				}
+			};
+		}
+		if (route.test?.response) {
+			const status = route.test.response.status || 200;
+			let schema = {
+				allOf: [
+					{
+						$ref: `#/components/schemas/${route.test.response.body}`
+					},
+					{
+						example: route.test.body
+					}
+				]
+			};
+			if (!route.test.body) schema = schema.allOf[0];
+
+			obj.responses = {
+				[status]: {
+					...(route.test.response.body
+						? {
+								description: obj.responses[status].description || "",
+								content: {
+									"application/json": {
+										schema: schema
+									}
+								}
+						  }
+						: {})
+				}
+			}.merge(obj.responses);
+			delete obj.responses.default;
+		}
+		if (p.includes(":")) {
+			obj.parameters = p.match(/:\w+/g)?.map((x) => ({
+				name: x.replace(":", ""),
+				in: "path",
+				required: true,
+				schema: { type: "string" },
+				description: x.replace(":", "")
+			}));
+		}
+		obj.tags = [...(obj.tags || []), getTag(p)].unique();
+
+		specification.paths[path] = { ...specification.paths[path], [method]: obj };
+	});
+}
+
+function main() {
+	combineSchemas(schemas);
+	apiRoutes();
+
+	fs.writeFileSync(
+		openapiPath,
+		JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number")
+	);
+}
+
+main();