Skip to content

Instantly share code, notes, and snippets.

@solesensei
Created July 14, 2023 10:44
Show Gist options
  • Save solesensei/e61659de69e88c1997515ebdf6684e10 to your computer and use it in GitHub Desktop.
Save solesensei/e61659de69e88c1997515ebdf6684e10 to your computer and use it in GitHub Desktop.
OpeanAPI v3 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 === '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');
});
});
@solesensei
Copy link
Author

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