Skip to content
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
70 changes: 70 additions & 0 deletions openapi-gen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import path from 'path';
import YAML from 'yaml';
import { OpenAPIV3_1 } from 'openapi-types'
import { Endpoint, extractSpec } from './tsxParser/openapi-extractor';
import React from 'react';
import { ApiTemplate } from './templates/ApiTemplate';
import { renderToMDX } from './tsxParser/renderToMDX';
import { mkdir, writeFile } from "fs/promises"

(async function main() {
const inputUrl = process.argv[2]
if (!inputUrl) {
console.error('Usage: ts-node index.ts <openapi-url>');
process.exit(1);
}
const outputDir = process.argv[3] || path.join('src', 'pages', 'ipa', 'resources');
try {
const yml = await readYaml(inputUrl)

const mdxFile: Record<string, Endpoint[]> = {}
const customFlags: string[] = []
const endpoints = extractSpec(yml)

endpoints.forEach(endpoint => {
if (!endpoint.tag) {
console.error('No tag for this endpoint:', endpoint.path)
return
}

if (!mdxFile[endpoint.tag]) {
mdxFile[endpoint.tag] = []
}

mdxFile[endpoint.tag].push(endpoint)
const ymlTag = yml.tags.find(tag => tag.name == endpoint.tag)

if (!ymlTag) return;
if (!customFlags[endpoint.tag]) {
customFlags[endpoint.tag] = []
}
for (let flag in ymlTag) {
if (flag.startsWith('x-') && ymlTag[flag] && !customFlags[endpoint.tag].includes(flag)) {
customFlags[endpoint.tag].push(flag)
}
}
})

for (let tag in mdxFile) {
const element = React.createElement(ApiTemplate, { tag: tag, endpoints: mdxFile[tag], customFlags: customFlags[tag] });
const component = renderToMDX(element)
const fileName = path.join(outputDir, tag.toLowerCase().replace(" ", "-") + ".mdx")
const dir = path.dirname(fileName)
await mkdir(dir, { recursive: true })
await writeFile(fileName, component)
}
} catch (error) {
console.error(error);
}
})()

async function readYaml(url: string) {
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.statusText}`);
const text = await res.text();
const parsed = YAML.parse(text) as OpenAPIV3_1.Document;
if (parsed.openapi != "3.1.0") {
throw new Error('Unsupported OpenAPI version: ' + parsed.openapi);
}
return parsed;
}
173 changes: 173 additions & 0 deletions openapi-gen/templates/ApiTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Endpoint, Parameter, Request } from "../tsxParser/openapi-extractor"
import { CodeGroup, Col, Row, Stringify, Property, Properties, Details, Summary, Badge } from "../tsxParser/components"

export type ApiTemplateProps = {
tag: string,
endpoints: Endpoint[]
customFlags?: string[]
}

export const ApiTemplate = ({ tag, endpoints, customFlags }: ApiTemplateProps) => {
return (
<>
<Stringify str={`export const title = '${tag}'`} />

{endpoints.map((end, index) => {
const flags = customFlags
? [...new Set([...customFlags, ...end.flags])]
: [...new Set(end.flags)];
return (
<div key={`${end.method}-${end.path}-${index}`}>
<h2>
{end.summary}
{flags.length > 0 && flags.map(flag => <Badge key={flag} customFlag={flag} />)}
{` {{ tag: '${end.method.toUpperCase()}', label: '${end.path}' }}`}
</h2>

<Row>
<Col>
{end.description}

{/* Handle Path Parameters for all methods that might have them */}
{end.parameters && end.parameters.length > 0 && (
<ParametersApi parameters={end.parameters} />
)}

{/* Handle Request Body for POST/PUT methods */}
{(end.method === "POST" || end.method === "PUT") && end.request && (
<PostBodyApi bodyRequest={end.request} path={end.path} />
)}
</Col>

<Col sticky>
<CodeGroup
title="Request"
endPoint={end}
/>
{end.responses["200"]?.schema &&
<CodeGroup
title="Response"
endPoint={end}
/>}
{end.responses["201"]?.schema &&
<CodeGroup
title="Response"
endPoint={end}
/>}
</Col>
</Row>
{"\n---"}
</div>
)
})}
</>
)
}

const ParametersApi = ({ parameters }: { parameters: Parameter[] }) => {
const params: Parameter[] = [];
const query: Parameter[] = [];
for (let p of parameters) {
if (p.in === "query") {
query.push(p)
} else {
params.push(p)
}
}

return (
<>
{params.length > 0 && <>
<h3>Path Parameters</h3>
<Properties>
{params.map((p, index) => {
return (
<Property
key={`param-${p.name}-${index}`}
name={p.name}
type={p.type}
required={p.required}
>
{p.description}
</Property>
)
})}
</Properties>
</>}
{query.length > 0 && <>
<h3>Query Parameters</h3>
<Properties>
{query.map((p, index) => {
return (
<Property
key={`query-${p.name}-${index}`}
name={p.name}
type={p.type}
required={p.required}
>
{p.description}
</Property>
)
})}
</Properties>
</>}
</>
)
}

const PostBodyApi = ({ bodyRequest, path }: { bodyRequest: Request[], path: string }) => {
return (
<>
<h3>Request-Body Parameters</h3>
<Properties>{renderProperties(bodyRequest, path)}</Properties>
</>
)
}


const renderProperties = (bodyRequest: Request[], path: string, prefix = "body",) => {
return bodyRequest.map((req, index) => {
const key = `${prefix}-${req.name}-${index}`

if (req.bodyObj && req.bodyObj.length > 0 && req.type === "object" || req.type === "object[]") {
return (
<Property
key={key}
name={req.name}
type={req.type}
required={req.required}
minLen={req.minLength}
maxLen={req.maxLength}
min={req.minimum}
max={req.maximum}
enumList={req.enumList}
>
<Details className="custom-details" open>
<Summary>{req.description || "More Information"}</Summary>
<Properties>
{req.bodyObj && renderProperties(req.bodyObj, path, `nested-${req.name}`)}
</Properties>
</Details>
</Property>
)
}

// Primitive type
return (
<Property
key={key}
name={req.name}
type={req.type}
required={req.required}
min={req.minimum}
max={req.maximum}
maxLen={req.maxLength}
minLen={req.minLength}
enumList={req.enumList}
>
{req.description}
</Property>
)
})
}

26 changes: 26 additions & 0 deletions openapi-gen/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"jsx": "react-jsx",
"esModuleInterop": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noImplicitAny": false,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
Loading