Skip to content

Instantly share code, notes, and snippets.

@tschoffelen
Created July 15, 2023 13:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tschoffelen/c90267addffc3526e39e61c1deed3aee to your computer and use it in GitHub Desktop.
Save tschoffelen/c90267addffc3526e39e61c1deed3aee to your computer and use it in GitHub Desktop.
import fs from "node:fs/promises";
import { createProcessor } from "@mdx-js/mdx";
const pipeline = createProcessor();
const flattenItems = (items) => {
return items.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {});
};
const flattenDescription = (items) => {
return (items || [])
.reduce((acc, item) => {
if (item.type === "text") {
acc.push(item.value);
} else if (item.type === "inlineCode") {
acc.push(`\`${item.value}\``);
} else if (item.type === "code") {
acc.push(` \`\`\`${item.value}\`\`\``);
} else if (item.name === "Response" || item.name === "Parameter") {
// do nothing
} else if (item.children) {
acc.push(...flattenDescription(item.children));
acc.push("\n");
} else {
console.log("unknown type", item.type);
}
return acc;
}, [])
.join("")
.replace("\n", " ");
};
const cleanType = (type) => {
if (type === "uuid" || type === "date") {
return "string";
}
if (type === "string[]") {
return "array";
}
return type.toLowerCase();
};
const payload = {
openapi: "3.0.0",
info: {
title: "My API",
version: "1.0.0",
},
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "API key",
},
},
},
security: [{ bearerAuth: [] }],
servers: [
{
url: "https://api.myapp.com",
description: "Production API",
},
],
paths: {},
};
const processTree = (tree) => {
for (const child of tree.children) {
if (child.name === "ApiMethod") {
const description = [];
const endpoint = {
...flattenItems(child.attributes),
responses: {},
requestBody: null,
parameters: [],
};
for (const subchild of child.children) {
if (subchild.name === "Parameter") {
const param = flattenItems(subchild.attributes);
if (param.in === "body") {
if (!endpoint.requestBody) {
endpoint.requestBody = {
type: "object",
properties: {},
required: [],
};
}
if (
"required" in param &&
param.required !== false &&
param.required !== "false"
) {
endpoint.requestBody.required.push(param.name);
}
const description = flattenDescription(subchild.children);
endpoint.requestBody.properties[param.name] = {
type: cleanType(param.type),
description,
};
} else {
endpoint.parameters.push({
name: param.name,
in: param.in,
description: flattenDescription(subchild.children),
required:
"required" in param &&
param.required !== false &&
param.required !== "false",
schema: {
type: cleanType(param.type),
},
});
}
}
if (subchild.name === "Response") {
const response = flattenItems(subchild.attributes);
let [status, description] = response.status.split(" ");
description = (
description +
" " +
flattenDescription(subchild.children)
).trim();
endpoint.responses[status.replace(":", "")] = {
description,
};
}
}
if (!payload.paths[endpoint.path]) {
payload.paths[endpoint.path] = {};
}
payload.paths[endpoint.path][endpoint.method] = {
summary: endpoint.summary,
description: flattenDescription(child.children),
responses: endpoint.responses,
parameters: endpoint.parameters,
requestBody: endpoint.requestBody
? {
content: {
"application/json": {
schema: endpoint.requestBody,
},
},
}
: undefined,
};
continue;
}
if (child.children?.length) {
processTree(child);
}
}
};
const loadTree = async (path) => {
const files = await fs.readdir(path);
for (const file of files) {
if (!file.includes(".")) {
await loadTree(`${path}/${file}`);
}
if (file.includes(".md")) {
const compiled = await pipeline.parse(
await fs.readFile(`${path}/${file}`)
);
processTree(compiled);
}
}
};
await loadTree("./pages");
await fs.mkdir("./public", { recursive: true });
await fs.writeFile(
"./public/openapi.json",
JSON.stringify(payload, null, 2)
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment