Created
July 14, 2023 10:44
-
-
Save solesensei/e61659de69e88c1997515ebdf6684e10 to your computer and use it in GitHub Desktop.
OpeanAPI v3 to TypeScript Schemas
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fetch = require('node-fetch'); | |
const fs = require('fs'); | |
const BASE_URL = 'http://localhost:8080'; | |
const SERVICES = ['api', 'api/dt/v1']; | |
function JSONstringifyOrder(obj, space) { | |
var allKeys = []; | |
JSON.stringify(obj, function (key, value) { | |
allKeys.push(key); | |
return value; | |
}); | |
allKeys.sort(); | |
return JSON.stringify(obj, allKeys, space); | |
} | |
function genQueryInterface(operationId) { | |
return 'T' + upperCaseFirst(operationId) + 'Query'; | |
} | |
function genBodyInterface(operationId) { | |
return 'T' + upperCaseFirst(operationId) + 'Body'; | |
} | |
function upperCaseFirst(str) { | |
return str.charAt(0).toUpperCase() + str.slice(1); | |
} | |
function genPathCall(method, path, params) { | |
const queryParams = params.filter((p) => p.in === 'query'); | |
const bodyParams = params.filter((p) => p.in === 'requestBody'); | |
return ( | |
`\`${path.replace(/\{[^}]*\}/g, (e) => `\$${e}`)}\`` + | |
(queryParams.length === 0 ? '' : ' + formatParams(queryParams as any)') + | |
(bodyParams.length === 0 | |
? ', { signal }' | |
: ", { method: '" + | |
method.toUpperCase() + | |
"', body: JSON.stringify(data), signal, headers: { 'Content-Type': 'application/json' } }") | |
); | |
} | |
function allOf(ao) { | |
if (ao.length === 1) { | |
return ao[0]; | |
} else { | |
return `(${ao.map((i) => getInterfaceName(i)).join(' | ')})`; | |
} | |
} | |
function getComponentObject(name, response) { | |
return response.components.schemas[name]; | |
} | |
function genParamTypeNonNullable(param, response) { | |
const schema = param.schema || param; | |
if (schema.type === 'boolean') { | |
return 'boolean'; | |
} else if (schema.type === 'string') { | |
if (schema.enum) { | |
return schema.enum.map((v) => `'${v}'`).join(' | '); | |
} else { | |
return 'string'; | |
} | |
} else if (schema.type === 'integer' || schema.type === 'number') { | |
return 'number'; | |
} else if (schema.type === 'array') { | |
if (schema.items.enum && schema.items.type === 'string') { | |
return `(${genParamType(schema.items, response)})[]`; | |
} | |
if (schema.items.type) { | |
return `${genParamTypeNonNullable(schema.items, response)}[]`; | |
} | |
if (schema.items.$ref) { | |
const component = getComponentObject(getInterfaceName(schema.items.$ref), response) | |
if (component.enum) { | |
return `(${component.enum.map((v) => `'${v}'`).join(' | ')})[]`; | |
} | |
return `${getInterfaceName(schema.items.$ref)}[]`; | |
} | |
const types = schema.items.anyOf.map((d) => genParamType(d, response)); | |
if (types.length > 1) { | |
return `(${types.join(' | ')})[]`; | |
} else { | |
return `${types[0]}[]`; | |
} | |
} else if (schema.allOf) { | |
return schema.allOf.map((i) => { | |
const component = getComponentObject(getInterfaceName(i.$ref), response); | |
if (component.enum) { | |
return component.enum.map((v) => `'${v}'`).join(' | '); | |
} | |
return getInterfaceName(i.$ref); | |
}).join(' | '); | |
} else if (schema.anyOf) { | |
return schema.anyOf.map((i) => genParamType(i, response)).join(' | '); | |
} else if (schema.type === 'object' && schema.additionalProperties) { | |
return `{[key: string]: ${genParamType(schema.additionalProperties, response)}}`; | |
} else if (schema.$ref) { | |
const component = getComponentObject(getInterfaceName(schema.$ref), response) | |
if (component.enum) { | |
return component.enum.map((v) => `'${v}'`).join(' | '); | |
} | |
return getInterfaceName(schema.$ref); | |
} else if (schema.type === 'object' && schema.additionalProperties) { | |
return `{[id: string]: ${allOf(schema.additionalProperties.allOf)}}`; | |
} else if (schema.type === 'object') { | |
return 'Object'; | |
} | |
throw new Error(`no type ${schema.type} ${JSON.stringify(param)}`); | |
} | |
function genParamType(param, response) { | |
return genParamTypeNonNullable(param, response); | |
} | |
const formatParams = ` | |
function formatParams(queryParams: {[key: string]: (string | boolean | number) | (string | boolean | number)[]}) { | |
const params = Object.entries(queryParams) | |
if (!params.length) return ''; | |
return '?' + params.map(([name, value]) => { | |
if(Array.isArray(value)) | |
return \`\${name}=\${value.map(encodeURIComponent).join(',')}\` | |
return \`\${name}=\${encodeURIComponent(value)}\` | |
}).join('&') | |
} | |
`; | |
const apiFetch = ` | |
function apiFetch<T>(path: string, params: RequestInit = {}) { | |
return fetch(path, params) | |
.then(async response => { | |
const res = await response.json() | |
if(response.status >= 200 && response.status < 300) | |
return res as T | |
throw res | |
}) | |
} | |
`; | |
function getInterfaceName(ref) { | |
return ref.replace(/^.*\//g, ''); | |
} | |
function compareString(a, b) { | |
if (a < b) { | |
return -1; | |
} else if (a > b) { | |
return 1; | |
} else { | |
return 0; | |
} | |
} | |
function compareFirstString(a, b) { | |
return compareString(a[0], b[0]); | |
} | |
Promise.all( | |
SERVICES.map((service) => | |
fetch(`${BASE_URL}/${service}/docs/openapi.json`).then((r) => r.json()), | |
), | |
).then((responses) => { | |
response = { | |
components: { | |
schemas: Object.assign({}, ...responses.map((r) => r.components.schemas)), | |
}, | |
paths: Object.assign({}, ...responses.map((r) => r.paths)), | |
}; | |
const paths = Object.keys(response.paths); | |
let entryPoints = []; | |
paths.forEach((path) => { | |
const pathObj = response.paths[path]; | |
Object.keys(pathObj).forEach((method) => { | |
entryPoints.push(pathObj[method]); | |
}); | |
}); | |
const sortedDefinitions = Object.entries(response.components.schemas).sort(compareFirstString); | |
const data = | |
'// DO NOT EDIT: Generated by npm run gen_api\n' + | |
formatParams + | |
apiFetch + | |
'export class SwaggerApi {\n' + | |
Object.entries(response.paths) | |
.map(([path, methods]) => { | |
return Object.entries(methods) | |
.map(([method, entryPoint]) => { | |
const parameters = entryPoint.parameters || []; | |
let params = parameters | |
.filter((p) => p.in === 'path') | |
.map((p) => `${p.name}: ${genParamType(p, response)}`); | |
const queryParams = parameters.filter((p) => p.in === 'query'); | |
if (queryParams.length > 0) { | |
params.push( | |
`queryParams: ${genQueryInterface(entryPoint.operationId)}`, | |
); | |
} | |
const bodyParams = parameters.filter((p) => p.in === 'requestBody'); | |
if (bodyParams.length > 0) { | |
params.push(`data: ${genBodyInterface(entryPoint.operationId)}`); | |
} | |
params.push(`signal: AbortSignal | null = null`); | |
const schema = entryPoint.responses['200'].content['application/json'].schema; | |
return ( | |
` /**\n` + | |
` * ${entryPoint.summary}\n` + | |
` * ${entryPoint.description}\n` + | |
` */\n` + | |
` ${entryPoint.operationId} (${params.join(', ')}) {\n` + | |
` return apiFetch<${schema && schema.$ref ? schema.$ref.replace(/^.*\//g, '') : 'unknown' | |
}>(${genPathCall(method, path, parameters)})\n` + | |
` }\n` | |
); | |
}) | |
.join(''); | |
}) | |
.join('') + | |
'}\n\n' + | |
Object.entries(response.paths) | |
.map(([path, methods]) => | |
Object.entries(methods) | |
.filter( | |
([_, entry]) => | |
entry.parameters && entry.parameters.filter((p) => p.in === 'query').length !== 0, | |
) | |
.map(([_method, entryPoint]) => { | |
return ( | |
`interface ${genQueryInterface(entryPoint.operationId)} {\n` + | |
entryPoint.parameters | |
.filter((p) => p.in === 'query') | |
.map((p) => { | |
return ( | |
(p.description | |
? ` /**\n` + ` * ${p.description}\n */\n` | |
: ``) + ` ${p.name}?: ${genParamType(p, response)}\n` | |
); | |
}) | |
.join('') + | |
`}\n\n` | |
); | |
}) | |
.join(''), | |
) | |
.join('') + | |
`\n` + | |
Object.entries(response.paths) | |
.map(([path, methods]) => | |
Object.entries(methods) | |
.filter( | |
([_, entry]) => | |
entry.parameters && entry.parameters.filter((p) => p.in === 'requestBody').length !== 0, | |
) | |
.map(([_method, entryPoint]) => { | |
return ( | |
`interface ${genBodyInterface(entryPoint.operationId)} {\n` + | |
entryPoint.parameters | |
.filter((p) => p.in === 'requestBody') | |
.map((p) => { | |
const requestBody = p.requestBody; | |
if (requestBody.content['application/json']) { | |
const schema = requestBody.content['application/json'].schema; | |
if (schema && schema.properties) { | |
return Object.entries(schema.properties) | |
.map(([fieldName, meta]) => { | |
return ( | |
(meta.description | |
? ` /**\n` + | |
` * ${meta.description}\n */\n` | |
: ``) + | |
` ${fieldName}${meta.required ? '' : '?' | |
}: ${genParamType(meta, response)}\n` | |
); | |
}) | |
.join(''); | |
} | |
} | |
}) | |
.join('') + | |
`}\n\n` | |
); | |
}) | |
.join(''), | |
) | |
.join('') + | |
sortedDefinitions | |
.map(([name, definition]) => { | |
if (definition.type === 'object') { | |
return ( | |
`export interface ${name} {\n` + | |
Object.entries(definition.properties) | |
.sort(compareFirstString) | |
.map(([fieldName, meta]) => { | |
return ( | |
(meta.description | |
? ` /**\n` + ` * ${meta.description}\n */\n` | |
: ``) + | |
` ${fieldName}${(definition.required || []).indexOf(fieldName) !== -1 | |
? '' | |
: '?' | |
}: ${genParamType(meta, response)}\n` | |
); | |
}) | |
.join('') + | |
`}\n` | |
); | |
} | |
}) | |
.join('\n'); | |
fs.writeFileSync('./src/services/swagger.json', JSONstringifyOrder(response, 3), 'utf-8'); | |
fs.writeFile('./src/services/SwaggerApi.ts', data, (e) => { | |
console.log('finished'); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Swagger V2