Skip to content

Instantly share code, notes, and snippets.

@solesensei
Created July 14, 2023 10:44
Show Gist options
  • Save solesensei/7b3af164c9ef990a1166e9e4b2c00010 to your computer and use it in GitHub Desktop.
Save solesensei/7b3af164c9ef990a1166e9e4b2c00010 to your computer and use it in GitHub Desktop.
Swagger V2 to TypeScript Schemas
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 === 'body');
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 genParamTypeNonNullable(param) {
if (param.type === 'boolean') {
return 'boolean';
} else if (param.type === 'string') {
if (param.enum) {
return param.enum.map((v) => `'${v}'`).join(' | ');
} else {
return 'string';
}
} else if (param.type === 'enum') {
return param.enum.map((v) => `'${v}'`).join(' | ');
} else if (param.type === 'integer' || param.type === 'number') {
return 'number';
} else if (param.type === 'array') {
if (param.items.enum && param.items.type === 'string') {
return `(${genParamType(param.items)})[]`;
}
if (param.items.type) {
return `${genParamType(param.items)}[]`;
}
if (param.items.$ref) {
return `${getInterfaceName(param.items.$ref)}[]`;
}
const types = param.items.allOf.map((d) => getInterfaceName(d.$ref));
if (types.length > 1) {
return `(${types.join(' | ')})[]`;
} else {
return `${types[0]}[]`;
}
} else if (param.allOf) {
return param.allOf.map((i) => getInterfaceName(i.$ref)).join(' | ');
} else if (param.type === 'object' && param.additionalProperties) {
return `{[key: string]: ${genParamType(param.additionalProperties)}}`;
} else if (param.$ref) {
return getInterfaceName(param.$ref);
} else if (param.type === 'object' && param.additionalProperties) {
return `{[id: string]: ${allOf(param.additionalProperties.allOf)}}`;
} else if (param.type === 'object') {
return 'Object';
}
throw new Error(`no type ${param.type} ${JSON.stringify(param)}`);
}
function genParamType(param) {
if (param['x-nullable']) {
return `${genParamTypeNonNullable(param)} | null`;
} else {
return genParamTypeNonNullable(param);
}
}
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 = {
definitions: Object.assign({}, ...responses.map((r) => r.definitions)),
paths: Object.assign({}, ...responses.map((r) => r.paths)),
};
const paths = Object.keys(response.paths);
let entryPoints = [];
paths.forEach((path) => {
const pathObj = response.paths[path];
if (pathObj.get) {
entryPoints.push({});
}
});
const sortedDefinitions = Object.entries(response.definitions).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]) => {
let params = entryPoint.parameters
.filter((p) => p.in === 'path')
.map((p) => `${p.name}: ${genParamType(p)}`);
const queryParams = entryPoint.parameters.filter((p) => p.in === 'query');
if (queryParams.length > 0) {
params.push(
`queryParams: ${genQueryInterface(entryPoint.operationId)}`,
);
}
const bodyParams = entryPoint.parameters.filter((p) => p.in === 'body');
if (bodyParams.length > 0) {
params.push(`data: ${genBodyInterface(entryPoint.operationId)}`);
}
params.push(`signal: AbortSignal | null = null`);
const schema = entryPoint.responses['200'].schema;
return (
` /**\n` +
` * ${entryPoint.summary}\n` +
` * ${entryPoint.description}\n` +
` */\n` +
` ${entryPoint.operationId} (${params.join(', ')}) {\n` +
` return apiFetch<${
schema ? schema.$ref.replace(/^.*\//g, '') : 'unknown'
}>(${genPathCall(method, path, entryPoint.parameters)})\n` +
` }\n`
);
})
.join('');
})
.join('') +
'}\n\n' +
Object.entries(response.paths)
.map(([path, methods]) =>
Object.entries(methods)
.filter(
([_, entry]) =>
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)}\n`
);
})
.join('') +
`}\n\n`
);
})
.join(''),
)
.join('') +
`\n` +
Object.entries(response.paths)
.map(([path, methods]) =>
Object.entries(methods)
.filter(
([_, entry]) =>
entry.parameters.filter((p) => p.in === 'body').length !== 0,
)
.map(([_method, entryPoint]) => {
return (
`interface ${genBodyInterface(entryPoint.operationId)} {\n` +
entryPoint.parameters
.filter((p) => p.in === 'body')
.map((p) => {
return Object.entries(p.schema.properties)
.map(([fieldName, meta]) => {
return (
(meta.description
? ` /**\n` +
` * ${meta.description}\n */\n`
: ``) +
` ${fieldName}${
meta.required ? '' : '?'
}: ${genParamType(meta)}\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)}\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');
});
});
@solesensei
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment