summary refs log tree commit diff
path: root/api/scripts/generate_openapi_schema.js
blob: eb979f144701fd90b24bd8557f96efd8fe3faad6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// 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) {
	var 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;
			obj.responses = {
				[status]: {
					...(route.test.response.body
						? {
								description: obj.responses[status].description || "",
								content: {
									"application/json": {
										schema: {
											$ref: `#/components/schemas/${route.test.response.body}`
										}
									}
								}
						  }
						: {})
				}
			}.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();