swaggergenerateapi
Version:
570 lines (540 loc) • 15.2 kB
text/typescript
import axios from 'axios';
import fs from 'fs';
import _path from 'path';
import { fileURLToPath } from 'url';
import ejs from 'ejs';
import { camelCase, find, findIndex, forEach, mapKeys, orderBy, remove, zipObject } from 'lodash-es';
import type {
ApiController,
ApiData,
ApiMethod,
Enum,
EnumItem,
Model,
NswagOptions,
Parameter,
Propertie,
Type,
} from './types';
import https from 'https';
import inquirer from 'inquirer';
/**
* 获取Swagger的JSON数据
* @param {*} swaggerUrl
*/
function getSwaggerData(swaggerUrl: string) {
const agent = new https.Agent({
rejectUnauthorized: false,
});
return new Promise((resolve, reject) => {
axios.get(swaggerUrl, { httpsAgent: agent }).then((response) => {
if (response.status == 200) {
const d = response.data;
if (typeof d == 'string') {
const obj = eval('(' + d + ')');
resolve(obj);
} else {
resolve(d);
}
} else {
reject(new Error('获取swagger数据失败'));
}
});
});
}
/**
* 删除文件夹
* @param path 地址
*/
function removeDirSync(path: string) {
let files = [];
/**
* 判断给定的路径是否存在
*/
if (fs.existsSync(path)) {
/**
* 返回文件和子目录的数组
*/
files = fs.readdirSync(path);
files.forEach(function (file) {
const curPath = _path.join(path, file);
/**
* fs.statSync同步读取文件夹文件,如果是文件夹,在重复触发函数
*/
if (fs.statSync(curPath).isDirectory()) {
// recurse
removeDirSync(curPath);
} else {
fs.unlinkSync(curPath);
}
});
/**
* 清除文件夹
*/
fs.rmdirSync(path);
} else {
console.log(`路径[${path}]不存在`);
}
}
/**
* 创建目录
* @param path 目录
*/
function markDirsSync(path: string) {
try {
if (!fs.existsSync(path)) {
let pathtmp = '';
path.split(/[/\\]/).forEach((dirname) => {
// 这里指用/ 或\ 都可以分隔目录 如 linux的/usr/local/services 和windows的 d:\temp\aaaa
if (pathtmp) {
pathtmp = _path.join(pathtmp, dirname);
} else {
pathtmp = dirname || '/';
}
if (!fs.existsSync(pathtmp)) {
fs.mkdirSync(pathtmp);
}
});
}
return true;
} catch (e) {
console.log('创建目录出错', e);
return false;
}
}
/**
* 生成代码
* @param tplPath 模板绝对地址
* @param data 数据
* @param outPath 文件存放绝对地址
* @param fileName 文件名称
*/
function codeRender(tplPath: string, data: any, outPath: string, fileName: string) {
if (markDirsSync(outPath)) {
const fileText = ejs.render(fs.readFileSync(tplPath, 'utf-8'), data);
const savePath = _path.join(outPath, fileName);
fs.writeFileSync(savePath, fileText);
}
}
/**
* 去掉换行
* @param str 字符串
*/
function removeLineBreak(str: string) {
return str ? str.replace(/[\r\n]/g, '') : '';
}
/**
* 参数名称处理
* @param {*} oldName
*/
function getParameterName(oldName: string) {
let newName = oldName;
// 关键词处理
if (oldName === 'number') {
newName = 'num';
}
if (oldName === 'string') {
newName = 'str';
}
newName = camelCase(newName).replace(/\./g, '_');
return newName;
}
/**
* 处理重名问题
* @param name 当前名称
* @param list 列表,对象必须有Name属性才行
*/
function reName(name: string, list: Array<any>) {
// 方法名称-重名处理
if (findIndex(list, { Name: name }) !== -1) {
let i = 1;
while (true) {
if (findIndex(list, { Name: name + '_' + i }) !== -1) {
i++;
} else {
name = name + '_' + i;
break;
}
}
}
return name;
}
/**
* 格式化属性
* @param properties 属性
* @param options 配置
*/
function convertType(properties: any, options: NswagOptions): any {
let type: Type = {
TypeOf: 'string',
TsType: 'void',
Ref: '',
};
if (!properties) {
return type;
}
if (properties.hasOwnProperty('oneOf')) {
return convertType(properties.oneOf[0], options);
}
if (properties.hasOwnProperty('allOf')) {
return convertType(properties.allOf[0], options);
}
if (properties.hasOwnProperty('schema')) {
return convertType(properties.schema, options);
}
if (properties.hasOwnProperty('$ref')) {
const t = options.FormatModelName(properties.$ref.substring(properties.$ref.lastIndexOf('/') + 1));
type = {
TypeOf: 'schema',
TsType: t,
Ref: t,
};
} else if (properties.hasOwnProperty('enum')) {
type = {
TypeOf: 'enum',
TsType: properties.enum
.map((item: any) => {
return JSON.stringify(item);
})
.join(' | '),
Ref: '',
};
} else if (properties.type === 'array') {
const iType = convertType(properties.items, options);
type = {
TypeOf: 'array',
TsType: 'Array<' + iType.TsType + '>',
Ref: iType.Ref,
};
} else {
type = {
TypeOf: properties.type,
TsType: '',
Ref: '',
};
switch (properties.type) {
case 'string':
type.TypeOf = 'string';
type.TsType = 'string';
break;
case 'number':
case 'integer':
type.TypeOf = 'number';
type.TsType = 'number';
break;
case 'boolean':
type.TypeOf = 'boolean';
type.TsType = 'boolean';
break;
case 'file':
type.TypeOf = 'file';
type.TsType = 'string | Blob';
break;
default:
type.TsType = 'boolean';
break;
}
}
return type;
}
/**
* swagger 文档格式化
* @param swagger
* @param options
*/
function formatData(swagger: any, options: NswagOptions) {
// 文档模式 是否openapi 模式 还是 默认 swagger模式
const isOpenApi = swagger.hasOwnProperty('openapi');
let apiData: ApiData = {
BaseInfo: {
Title: swagger.info.title, // 接口标题
Description: swagger.info.description, // 接口说明
Version: swagger.info.version, // 接口版本号
},
Controllers: [],
Models: [],
Enums: [],
};
// 格式化属性方法
function fmProperties(properties: any, model: Model, required = []) {
forEach(properties, function (propertie, name) {
const newp: Propertie = {
Name: name,
Description: removeLineBreak(propertie.description),
Type: convertType(propertie, options),
Nullable: true,
};
if (propertie.hasOwnProperty('nullable')) {
newp.Nullable = propertie.nullable;
} else {
if (required.find((r) => r == name)) {
newp.Nullable = false;
}
}
model.Properties.push(newp);
});
}
// dto对象 / enum对象
forEach(isOpenApi ? swagger.components.schemas : swagger.definitions, function (definition, name) {
// 排除 #/definitions/***
if (!name.startsWith('#')) {
if (definition.hasOwnProperty('enum')) {
const e: Enum = {
Name: options.FormatModelName(name),
Description: removeLineBreak(definition.description),
Items: [],
};
const enums = zipObject(definition['x-enumNames'], definition.enum);
forEach(enums, function (enumValue: any, enumName) {
const item: EnumItem = {
Name: enumName,
Value: enumValue,
};
e.Items.push(item);
});
apiData.Enums.push(e);
} else {
const m: Model = {
Name: options.FormatModelName(name),
Description: removeLineBreak(definition.description),
IsParameter: false,
BaseModel: '',
Properties: [],
};
// 格式化属性
if (definition.hasOwnProperty('allOf')) {
forEach(definition.allOf, function (propertie) {
if (propertie.hasOwnProperty('$ref')) {
m.BaseModel = options.FormatModelName(
propertie.$ref.substring(propertie.$ref.lastIndexOf('/') + 1)
);
} else {
if (propertie.hasOwnProperty('properties')) {
fmProperties(propertie.properties, m);
}
}
});
} else if (definition.hasOwnProperty('required')) {
fmProperties(definition.properties, m, definition.required);
} else {
fmProperties(definition.properties, m);
}
apiData.Models.push(m);
}
}
});
// 模块
mapKeys(swagger.ControllerDesc, function (value, key) {
apiData.Controllers.push({
Name: options.FormatControllerName(key),
Description: removeLineBreak(value) || '后台太懒没写注释',
Methods: [],
ImportModels: [],
});
return key;
});
// 方法
forEach(swagger.paths, function (api, url) {
forEach(api, function (md, requestName) {
// 模块名称
const cName = options.FormatControllerName(md.tags[0]);
// 当前模块
let currController = find(apiData.Controllers, { Name: cName });
if (!currController) {
// 没有就新加一个模块
currController = {
Name: cName,
Description: '后台太懒没写注释',
Methods: [],
ImportModels: [],
};
apiData.Controllers.push(currController);
}
// 方法名称
let mName = options.FormatMethodName(url);
mName = reName(mName, currController.Methods);
// 添加方法
const method: ApiMethod = {
Name: mName,
Url: url,
Description: removeLineBreak(md.summary) || '后台太懒没写注释',
RequestName: requestName,
Parameters: [],
ParametersQuery: [],
ParametersBody: [],
ParametersFormData: [],
ParametersHeader: [],
ParametersPath: [],
Responses: convertType(
//todo: swagger写注解@ApiResponse({status: 200})才会有这个属性,要不然是default
md.responses['200']
? isOpenApi
? md.responses['200'].content
? md.responses['200'].content['application/json'].schema
: 'any'
: md.responses['200'].schema
: 'any',
options
),
MockData: null,
};
// 方法参数处理
// 兼容openapi 模式 requestBody 参数
if (isOpenApi && md.requestBody) {
md.parameters = [];
md.parameters.push(
Object.assign(
{
name: md.requestBody['x-name'] || 'input',
required: md.requestBody.required || true,
in: 'body',
description: '后台太懒没有写注释',
},
md.requestBody.content['application/json']
)
);
}
forEach(md.parameters, (parameter: any) => {
let pa: Parameter = {
Name: parameter.name,
CamelCaseName: reName(getParameterName(parameter.name), method.Parameters),
Description: removeLineBreak(parameter.description),
In: parameter.in,
Required: parameter.required || false,
Default: '',
Type: convertType(parameter, options),
};
if (pa.In === 'query') {
method.ParametersQuery.push(pa);
method.Parameters.push(pa);
}
if (pa.In === 'body') {
method.ParametersBody.push(pa);
method.Parameters.push(pa);
}
if (pa.In === 'formData') {
method.ParametersFormData.push(pa);
method.Parameters.push(pa);
}
if (pa.In === 'header') {
method.ParametersHeader.push(pa);
}
if (pa.In === 'path') {
method.ParametersPath.push(pa);
method.Parameters.push(pa);
}
// 接口参数:存在引用型参数&没有没添加到引用列表的则添加到引用列表
if (pa.Type.Ref && currController && currController.ImportModels.indexOf(pa.Type.Ref) == -1) {
currController.ImportModels.push(pa.Type.Ref);
// 标记为输入参数对象
const d = find(apiData.Models, { Name: pa.Type.Ref });
if (d) {
d.IsParameter = true;
}
}
});
// 排序一下参数,把非必填参数排后面
method.Parameters = orderBy(method.Parameters, ['Required'], ['desc']);
// 返回值:存在引用型参数&没有没添加到引用列表的则添加到引用列表
method.Responses.Ref &&
currController &&
currController.ImportModels.indexOf(method.Responses.Ref) == -1 &&
currController.ImportModels.push(method.Responses.Ref);
// 添加方法
currController.Methods.push(method);
});
});
// 调整方法顺序,因为mock时 有可能匹配错误的mock拦截
apiData.Controllers.map((c) => {
c.Methods = orderBy(c.Methods, ['Name'], ['desc']);
return c;
});
// 清理无方法空模块
remove(apiData.Controllers, (c: ApiController) => {
return c.Methods.length <= 0;
});
return apiData;
}
/**
* 格式化成TS统一模板格式数据-数据源
* @param swaggerUrl
* @param options
*/
function getApiData(swaggerUrl: string, options: NswagOptions): Promise<ApiData> {
return new Promise((resolve, reject) => {
console.log(`开始获取swagger数据`)
getSwaggerData(swaggerUrl)
.then((r: any) => {
console.log(`开始格式化swagger数据`)
const apiData = formatData(r, options);
resolve(apiData);
})
.catch((e) => {
reject(e);
});
});
}
/**
* 生成
* @param apiData 标准化数据
* @param options 生成配置
*/
async function codeBuild(apiData: ApiData, options: NswagOptions) {
const __filename = fileURLToPath(import.meta.url);
const __dirname = _path.dirname(__filename);
if (!options.OutPath) {
console.log("请设置绝对路径输出目录")
process.exit(0);
}
const savePath = options.OutPath;
const saveBasePath = _path.join(savePath, 'base');
const saveMethodDir = _path.join(savePath, 'api');
const saveModelsDir = _path.join(savePath, 'model');
const tplPath = options.TplPath || _path.join(__dirname, './tpl');
const tplMethodPath = _path.join(tplPath, 'method.ejs');
const tplModelsPath = _path.join(tplPath, 'model.ejs');
const tplBasePath = _path.join(tplPath, 'base.ejs');
if (fs.existsSync(saveBasePath)) {
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
default: false,
message: `基类已存在,是否重新生成?`
}
])
if (ok) {
console.log('清理所有旧文件');
removeDirSync(savePath);
console.log('生成基类');
codeRender(tplBasePath, { options }, saveBasePath, "useAxios.ts");
} else {
console.log('清理接口文件');
removeDirSync(saveMethodDir);
console.log('清理模型文件');
removeDirSync(saveModelsDir);
}
} else {
console.log('生成基类');
// 生成-基类
codeRender(tplBasePath, { options }, saveBasePath, "useAxios.ts");
}
console.log('生成dto对象');
// 生成-dto对象
codeRender(tplModelsPath, { apiData, options }, saveModelsDir, 'index.ts');
console.log(`生成服务`);
// 按模块生成接口
apiData.Controllers.forEach((controller: any) => {
// 生成-接口
codeRender(tplMethodPath, { controller, options }, saveMethodDir, controller.Name + '.ts');
});
console.log(`\n${apiData.Controllers.length}个服务已生成 ${saveMethodDir}`);
}
/**
* 生成
* @param options 生成配置
*/
export default async function build(options: NswagOptions) {
const apiData = await getApiData(options.SwaggerUrl, options);
await codeBuild(apiData, options);
}