@mx560/swagger2ts
Version:
a tool to generate ts model file
467 lines (460 loc) • 16.3 kB
JavaScript
import request from 'umi-request';
import path, { resolve } from 'path';
import fs from 'fs';
import ejs from 'ejs';
var Version;
(function (Version) {
Version[Version["OAS2"] = 0] = "OAS2";
Version[Version["OAS3"] = 1] = "OAS3";
})(Version || (Version = {}));
const getOriginalRef = (ref) => {
if (!ref) {
return '';
}
const strs = ref.split('/');
return strs[strs.length - 1];
};
const getAllDeps = (type) => {
if (!type) {
return [];
}
return type.split('<').map(t => t.replace(/>/g, '').replace(/\[\]/g, ''));
};
const toGenericsTypes = (types) => {
return types.replace(/«/g, '<').replace(/»/g, '>');
};
const toGenerics = (types) => {
return types.length === 1 ? types[0] : `${types.join('<')}${types.slice(1).map(() => '>').join('')}`;
};
const removeGenericsSign = (type) => {
return type.replace(/<T>/g, '');
};
const removeGenericsType = (type) => {
return type.split("<")[0];
};
const removeArraySign = (type) => {
return type.replace(/\[\]/g, '');
};
const report = (dist, code) => {
console.log(blue(path.relative(process.cwd(), dist)) + ' ' + getSize(code));
};
const getSize = (code) => {
return (code.length / 1024).toFixed(2) + 'kb';
};
const blue = (str) => {
return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m';
};
const isString = (obj) => {
return Object.prototype.toString.call(obj) === '[object String]';
};
const writeFile = async (outputDir, content, fileName = 'typings.ts') => {
const output = resolve(outputDir, fileName);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
fs.writeFileSync(output, content);
report(output, content);
};
const getLastPath = (path) => {
if (!path) {
return '';
}
const strs = path.split('/');
return strs[strs.length - 1];
};
const getMapType = (properties) => {
const { $ref } = properties;
if ($ref) {
return `{ [key: string]: ${getOriginalRef($ref)} }`;
}
return `{ [key: string]: string }`;
};
const getArrayType = (properties) => {
const { $ref } = properties;
if ($ref) {
return getOriginalRef($ref);
}
return getType(properties);
};
const dateEnum = ['Date', 'date', 'dateTime', 'date-time', 'datetime', 'LocalTime', 'LocalDate', 'LocalDateTime'];
const stringEnum = ['string', 'email', 'password', 'url', 'byte', 'binary'];
const getType = (param, hasGenerics) => {
if (!param) {
return 'any';
}
const originalRef = getOriginalRef(param.$ref);
const { type, enum: enumTypes = [], additionalProperties } = param;
if (!type && originalRef) {
if (dateEnum.includes(originalRef)) {
return 'string';
}
return toGenericsTypes(originalRef);
}
const numberEnum = [
'int64',
'integer',
'long',
'float',
'double',
'number',
'int',
'float',
'double',
'int32',
'int64'
];
if (enumTypes.length) {
return `'${enumTypes.join('\' | \'')}'`;
}
if (numberEnum.includes(type)) {
return 'number';
}
if (dateEnum.includes(type)) {
return 'string';
}
if (stringEnum.includes(type)) {
return 'string';
}
if (type === 'boolean') {
return 'boolean';
}
if (type === 'object' && additionalProperties) {
return hasGenerics ? 'T' : getMapType(additionalProperties) ?? 'any';
}
if (type === 'array') {
return hasGenerics ? 'T[]' : `${getArrayType(param.items)}[]`;
}
return 'any';
};
const parseProperties = (properties) => {
return Object.keys(properties).map((name) => {
const parameter = properties[name];
return {
in: 'body',
name,
type: getType(parameter, false),
description: parameter.description ?? '',
required: false
};
});
};
const getParams = (definitions, parameters) => {
if (!parameters || parameters.length === 0) {
return [];
}
if (parameters[0]?.in === 'body') {
const originalRef = getOriginalRef(parameters?.[0]?.schema.$ref);
const properties = definitions[originalRef]?.properties;
if (!properties) {
return [
{
in: 'body',
name: parameters[0].name,
type: 'any',
description: parameters[0].description ?? '',
required: false
}
];
}
return parseProperties(properties);
}
return parameters.map((parameter) => {
return {
in: parameter.in,
name: parameter.name,
type: getType(parameter, false),
description: parameter.description ?? '',
required: parameter.required ?? false
};
});
};
const getUrlText = (path) => {
if (path.includes('{')) {
return `${path.replace(/{/g, '${pathVars.')}`;
}
return `${path}`;
};
const getApis = (data, definitions, types, version) => {
const getResponseType = (schema, generics) => {
if (schema?.type) {
return getType(schema);
}
const originalRef = getOriginalRef(schema?.$ref);
if (originalRef) {
const type = originalRef?.replace(/«/g, '<').replace(/»/g, '>');
const deps = getAllDeps(type);
if (deps.length <= 1 && generics?.includes(type ?? '')) {
return type + '<any>';
}
const typesWithoutSign = types.map((type) => removeGenericsSign(type.name));
const depMap = new Map();
depMap.set('List', 'Array');
for (let i = 0; i < deps.length; i++) {
if (depMap.has(deps[i])) {
deps[i] = depMap.get(deps[i]);
continue;
}
if (!typesWithoutSign.includes(deps[i])) {
deps[i] = 'any';
}
}
return toGenerics(deps);
}
return 'any';
};
const apis = [];
const parseOperation = (path, method, api) => {
const params = getParams(definitions, api?.parameters);
let schema;
if (version === Version.OAS2) {
schema = (api?.responses?.['200'] ?? api?.responses?.['201']).schema;
}
else {
const content = (api?.responses?.['200'] ?? api?.responses?.['201']).content ?? {};
const firstProp = Object.keys(content)[0];
if (content[firstProp]) {
schema = content[firstProp].schema;
if (schema && api?.requestBody) {
const content = api.requestBody.content;
const firstProp = Object.keys(content)[0];
const schema = content[firstProp].schema;
const originalRef = getOriginalRef(schema.$ref);
const properties = definitions[originalRef]?.properties;
if (!properties) {
params.push({
in: 'body',
name: 'unknownParam',
type: 'any',
description: '',
required: false
});
}
else {
parseProperties(properties).forEach((param) => params.push(param));
}
}
}
}
apis.push({
tag: api?.tags?.[0] ?? '',
name: api?.operationId ?? '',
description: api?.summary ?? '',
request: {
url: path,
urlText: getUrlText(path),
method: method.toUpperCase(),
params,
filter: {
path: params.filter((param) => param.in === 'path'),
query: params.filter((param) => param.in === 'query'),
body: params.filter((param) => param.in === 'body'),
formdata: params.filter((param) => param.in === 'formdata')
}
},
response: {
type: getResponseType(schema, types.filter((type) => type.isGenerics).map((type) => removeGenericsSign(type.name)))
}
});
};
Object.keys(data).forEach((path) => {
const methods = data[path];
if (methods.get) {
parseOperation(path, 'get', methods.get);
}
if (methods.post) {
parseOperation(path, 'post', methods.post);
}
if (methods.put) {
parseOperation(path, 'put', methods.put);
}
if (methods.delete) {
parseOperation(path, 'delete', methods.delete);
}
});
return apis;
};
const getTypeParams = (properties, hasGenerics, requiredProperty = []) => {
return Object.keys(properties).map((property) => {
const param = properties[property];
return {
isGenerics: hasGenerics,
name: property,
type: getType(param, hasGenerics),
description: param.description ?? '',
required: requiredProperty.includes(property)
};
});
};
const getAttributeDeps = (schema) => {
const depSet = new Set();
const getDep = (obj) => {
for (const key in obj) {
if (typeof obj[key] === 'object') {
getDep(obj[key]);
}
else if (key === '$ref') {
let value = getLastPath(obj[key]);
if (!dateEnum.includes(value)) {
depSet.add(value);
}
}
}
};
getDep(schema);
return Array.from(depSet);
};
const getTypes = (definitions) => {
const generics = new Set();
Object.keys(definitions).forEach((definition) => {
const genericArr = definition.split('«');
genericArr.pop();
genericArr.forEach((g) => generics.add(g));
});
const types = [];
Object.keys(definitions).forEach((definition) => {
let defText = definition;
const deps = getAllDeps(toGenericsTypes(definition));
if (deps.length > 1) {
defText = deps[0];
}
const def = definitions[definition];
if (!def.properties) {
return;
}
if (!types.some((type) => removeGenericsSign(type.name) === defText)) {
const isGenerics = generics.has(defText);
const deps = getAttributeDeps(def.properties);
types.push({
isGenerics,
name: isGenerics ? `${defText}<T>` : removeGenericsType(defText),
description: def.description ?? '',
params: getTypeParams(def.properties, isGenerics, def.required),
deps
});
}
});
return types;
};
const spec2ToOpenApi = (data) => {
const definitions = data.definitions ?? {};
const types = getTypes(definitions ?? {});
const apis = getApis(data.paths, definitions, types, Version.OAS2);
return { types, apis };
};
const spec3ToOpenApi = (data) => {
const definitions = data.components.schemas ?? {};
const types = getTypes(data.components.schemas ?? {});
const apis = getApis(data.paths, definitions, types, Version.OAS3);
return { types, apis };
};
const getOpenApi = (data) => {
return data.swagger === '2.0' ? spec2ToOpenApi(data) : spec3ToOpenApi(data);
};
const renderFile = (file, data) => {
return new Promise((resolve, reject) => {
ejs.renderFile(file, data, {}, (err, str) => {
if (err) {
reject(err.message);
}
resolve(str);
});
});
};
const generateTs = async (originalOpenApi, options) => {
const { template = 'umi-request', importText = '', outputDir, generateRequest = false, generateModel = true, multipleFiles = true } = options;
if (!outputDir) {
throw new Error('please input outputDir!');
}
const templates = ['umi-request', 'axios'];
if (!templates.includes(template)) {
throw new Error(`oops, there is no template of ${template} so far, you can open an issue at https://github.com/huajiayi/openapi-tool/issues.`);
}
let openapi = JSON.parse(JSON.stringify(originalOpenApi));
const { types, apis } = openapi;
if (generateModel) {
if (multipleFiles) {
const filePath = resolve(__dirname, '../', 'src', 'template', 'type.ejs');
for (const type of types) {
let depTexts = [];
if (type.deps?.length) {
depTexts = type.deps.map((dep) => {
return `import { ${dep} } from './${dep}';`;
});
}
const content = await renderFile(filePath, { type, deps: depTexts });
writeFile(outputDir, content, `${removeGenericsSign(type.name)}.ts`);
}
}
else {
const filePath = resolve(__dirname, '../', 'src', 'template', 'types.ejs');
const content = await renderFile(filePath, { types });
writeFile(outputDir, content);
}
}
if (generateRequest) {
const tagMap = new Map();
apis.forEach(api => {
if (!tagMap.has(api.tag)) {
tagMap.set(api.tag, []);
}
tagMap.get(api.tag)?.push(api);
});
tagMap.forEach(async (apis, tag) => {
const filePath = resolve(__dirname, '../', 'src', 'template', `${template}.ejs`);
const deps = new Set();
apis.forEach((api) => {
api.request.params.forEach((param) => {
const dep = removeArraySign(param.type);
if (types.some((type) => removeGenericsSign(type.name) === dep)) {
deps.add(dep);
}
});
getAllDeps(api.response.type).forEach((dep) => {
if (types.some((type) => removeGenericsSign(type.name) === dep)) {
deps.add(dep);
}
});
});
const service = await renderFile(filePath, { importText, deps, apis, typescript: true });
const output = resolve(outputDir, `${tag}.ts`);
fs.writeFileSync(output, service);
report(output, service);
});
}
};
const plugins = [];
class OpenApiTool {
constructor(options) {
const { data, url } = options;
if (!data && !url) {
throw new Error('please input either data or url!');
}
this.options = options;
this.registerPlugins(plugins);
}
static use(plugin, options) {
plugins.push({ plugin, options });
}
async getOpenApi(generatorTsOptions) {
const { data, url } = this.options;
let jsonData = data;
if (url) {
jsonData = await request.get(url);
}
if (data && isString(data)) {
jsonData = JSON.parse(data);
}
if (generatorTsOptions.generateSwaggerJson) {
writeFile(generatorTsOptions.outputDir, JSON.stringify(jsonData, null, 2), 'swagger.json');
}
return getOpenApi(jsonData);
}
async generateTs(generatorTsOptions) {
const openapi = await this.getOpenApi(generatorTsOptions);
await generateTs(openapi, generatorTsOptions);
}
registerPlugins(plugins) {
plugins.forEach((pluginObj) => pluginObj.plugin(OpenApiTool, pluginObj.options));
}
}
export { OpenApiTool as default };