/**
* 根据 Swagger json 自动生成接口服务文件
*/
const config = require('./config');
// 输出文件及目录定义
const ROOT_PATH = './src/services';
const ENUM_FILE = `${ROOT_PATH}/enums.ts`;
const TYPING_D = `${ROOT_PATH}/typing.d.ts`;
const API_PATH = `${ROOT_PATH}/apis`;
// JS关键字
const JS_KEY_WORDS = [
'break', 'else', 'new', 'var', 'case', 'finally', 'return', 'void', 'catch', 'for', 'switch',
'while', 'continue', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in',
'try', 'do', 'instranceof', 'typeof', 'abstract', 'enum', 'int', 'short', 'boolean', 'export',
'interface', 'static', 'byte', 'extends', 'long', 'super', 'char', 'final', 'native', 'synchronized',
'class', 'float', 'package', 'throws', 'const', 'goto', 'private ', 'transient', 'debugger',
'implements', 'protected ', 'volatile', 'double', 'import', 'public',
];
/** 数值类型映射 */
const IntegerMapping = {
int16: 'number',
int32: 'number',
int64: 'string',
};
/** 返回结果类型映射 */
const ResultMapping = {
Int16: 'number',
Int32: 'number',
Int64: 'string',
Boolean: 'boolean',
Object: 'any',
String: 'string',
DateTime: 'string',
};
/** 统一注释 */
const UnifyComment = `// @ts-ignore\n/* eslint-disable */\n\n// 该文件自动生成,请勿手动修改!`;
// ----------------------------------------
// 执行外部命令
const execSync = require('child_process').execSync;
// 文件操作
const fs = require('fs');
// ----------------------------------------
/**
* 下划线中横线分隔字符串转换为驼峰格式
* @param {string} value 待转换字符串
* @param {boolean} [capitalize] 首字母大写
* @returns
*/
const kebabToCamelCase = (value, capitalize = false) => {
let n = value.replace(/[_-][a-zA-z]/g, (str) => str.substr(-1).toUpperCase()).trim() || '';
if (capitalize && n.length != 0) {
n = n.slice(0, 1).toUpperCase() + n.slice(1);
}
return n;
};
/**
* 根据路径生成操作名称
* @param {string} path 接口路径
* @returns 取路径最后一级的名称
*/
const getActionName = (path) => {
const pp = path.split('/');
if (pp.length == 0) {
return '';
}
const n = pp[pp.length - 1];
let an = kebabToCamelCase(n);
if (JS_KEY_WORDS.includes(an.toLocaleLowerCase())) {
an = `${an}Action`;
}
return an;
};
/**
* 生成控制器名称
* @param {string} name Swagger的tag名称
* @returns
*/
const getControllerName = (name) => {
const n = kebabToCamelCase(name, true);
return `${n}Controller`;
};
/**
* 清除文件
* @param {*} dir 需要清空的目录
*/
const clearFiles = (dir) => {
const oldFiles = fs.readdirSync(dir);
oldFiles.forEach((f) => {
const filePath = `${dir}/${f}`;
const s = fs.statSync(filePath);
if (s.isDirectory()) {
return;
}
fs.unlinkSync(filePath);
});
};
/** 获取引用类型 */
const getRefEntity = ($ref) => {
const rs = $ref.split('/');
if (rs.length == 0) {
return 'any';
}
return rs[rs.length - 1];
};
/** 获取简单类型 */
const getSimpleType = ({ type, format }) => {
if (!type) {
return 'any';
}
const ltype = type.toLowerCase();
if (['string', 'boolean', 'number'].includes(type)) {
return ltype;
}
if (ltype == 'integer') {
return IntegerMapping[format] ?? 'number';
}
return 'any';
};
/** 获取响应结果类型 */
const getResultType = (type) => {
const t = ResultMapping[type];
if (t) {
return t;
}
return `API.${type}`;
};
/** 获取实体类型 */
const getEntityType = (name, schema) => {
const props = schema.properties;
let entity = '';
const desc = schema.description;
if (desc) {
entity = `/** ${desc} */\n`;
}
entity = `${entity}type ${name} = {\n`;
const requiredList = schema.required ?? [];
for (const pname in props) {
const prop = props[pname];
let member = '';
const memberDesc = prop.description;
if (memberDesc) {
member = `/** ${memberDesc} */\n`;
}
let memberType = '';
if (prop['$ref']) {
memberType = `${getRefEntity(prop['$ref'])}`;
} else if (prop['items']) {
if (prop['items']['$ref']) {
memberType = `${getRefEntity(prop['items']['$ref'])}[]`;
} else {
memberType = `${getSimpleType(prop['items'])}[]`;
}
} else {
memberType = `${getSimpleType(prop)}`;
}
const required = requiredList.includes(pname) || (prop.nullable !== undefined && !prop.nullable);
member = `${member}${pname}${required ? '' : '?'}: ${memberType};\n`;
entity = `${entity}${member}`;
}
entity = `${entity}}\n`;
return entity;
};
/** 获取请求类型 */
const getRequestBodyType = (reqBody) => {
if (reqBody['content']['application/json']) {
const schema = reqBody['content']['application/json']['schema'];
let tps = '';
if (schema['$ref']) {
tps = getRefEntity(schema['$ref']);
} else if (schema['items']) {
if (schema['items']['$ref']) {
tps = getRefEntity(schema['items']['$ref']);
} else {
tps = getSimpleType(schema['items']);
}
} else {
tps = getSimpleType(schema);
}
if (schema['type'] == 'array') {
return `${tps}[]`;
}
return tps;
}
if (reqBody['content']['multipart/form-data']) {
return 'FormData';
}
return 'any';
};
/** 获取统一返回类型 */
const getXnRestfulResultType = (propRef) => {
const rtype = getRefEntity(propRef).replaceAll('XnRestfulResult_', '');
const ps = rtype.split('_');
if (ps.length == 1) {
return getResultType(ps);
}
if (ps[0] == 'List') {
return `${getResultType(ps[1])}[]`;
}
if (ps[0] == 'PageResult') {
return `API.PageResponse<${getResultType(ps[1])}>`;
}
// return getResultType(ps[1]);
// return getResultType(ps[0]);
return getResultType(rtype);
};
/** 获取响应类型 */
const getResponseType = (res) => {
const schema = res['200']?.['content']?.['application/json']?.['schema'];
if (!schema) {
return 'any';
}
if (schema['$ref']) {
return getXnRestfulResultType(schema['$ref']);
} else if (schema['items']) {
if (schema['items']['$ref']) {
return getXnRestfulResultType(schema['items']['$ref']);
} else {
return getSimpleType(schema['items']);
}
} else {
return getSimpleType(schema);
}
};
/** 获取查询参数类型 */
const getParamsType = (parameters) => {
let pb = '';
for (const p of parameters) {
const pdesc = p.description ?? p.name;
pb = `${pb}/** ${pdesc} */\n${p.name}${p.required ? '' : '?'}:`;
const prop = p.schema;
let ptype = '';
if (prop['$ref']) {
ptype = `${getRefEntity(prop['$ref'])}`;
} else if (prop['items']) {
if (prop['items']['$ref']) {
ptype = `${getRefEntity(prop['items']['$ref'])}[]`;
} else {
ptype = `${getSimpleType(prop['items'])}[]`;
}
} else {
ptype = `${getSimpleType(prop)}`;
}
pb = `${pb}${ptype};\n`;
}
return `{\n${pb}}`;
};
/** 判断是否简单类型 */
const isSimpleType = (type) => {
return ['string', 'number', 'boolean', 'object', 'any', 'formdata'].includes(type.toLowerCase());
};
/** 获取 tag 描述字典 */
const getTagDescDict = (tags) => {
let tagDict = {};
for (const tag of tags) {
tagDict[tag.name] = tag.description ?? tag.name;
}
return tagDict;
};
// ----------------------------------------
/** 生成枚举 */
const generateEnums = (schemas) => {
const enums = [];
const importEnums = [];
for (let ek in schemas) {
if (schemas[ek].enum) {
const desc = schemas[ek].description;
if (!desc) {
continue;
}
const s1 = desc.replaceAll('
', '').split(' ');
if (s1 && s1.length > 1) {
const enumComment = s1?.[0];
const items = [];
for (let j = 1; j < s1.length; j++) {
const s2 = s1[j].split(' ');
if (s2.length > 3) {
const itemComent = s2[0];
const itemCode = s2[1];
const itemValue = s2[3];
items.push(`\n /** ${itemComent} */\n ${itemCode} = ${itemValue},`);
}
}
enums.push(`\n/** ${enumComment} */\nexport enum ${ek} {${items.join('')}\n}`);
importEnums.push(ek);
}
}
}
const fs = require('fs');
const es = enums.join('\n');
const ies = importEnums.map((t) => ` ${t}`).join(',\n');
fs.writeFileSync(ENUM_FILE, `${UnifyComment}\n\n${es}\n\nexport default {\n${ies},\n};\n`);
execSync(`prettier --write ${ENUM_FILE}`);
return importEnums;
};
/** 生成类型定义文件 */
const generateEntity = (schemas, enums) => {
// 生成类型
let entities = [];
for (const name in schemas) {
const schema = schemas[name];
if (
name.includes('XnRestfulResult_') ||
name.includes('PageResult_') ||
schema.enum ||
!schema.properties
) {
continue;
}
// const entity = getEntityType(name.split('_')[0], schema);
// const ns = name.split('_');
// const entity = getEntityType(ns[ns.length - 1], schema);
const entity = getEntityType(name, schema);
entities.push(entity);
}
const fs = require('fs');
const ies = enums.map((t) => ` ${t}`).join(',\n');
const es = entities.join('\n');
fs.writeFileSync(
TYPING_D,
`${UnifyComment}
import {
${ies},
} from './enums';
declare global {
declare namespace API {
/** 分页查询请求参数基类型 */
type PageQueryType = {
/** 当前页值 */
pageIndex: number;
/** 每页大小 */
pageSize: number;
/** 搜索值 */
searchValue?: string;
/** 搜索开始时间 */
searchBeginTime?: string;
/** 搜索结束时间 */
searchEndTime?: string;
/** 排序字段 */
sortField?: string;
/** 排序方法,默认升序,否则降序(配合antd前端,约定参数为 Ascend,Dscend) */
sortOrder?: string;
/** 降序排序(不要问我为什么是descend不是desc,前端约定参数就是这样) */
descStr?: string;
/** 复杂查询条件 */
searchParameters?: Condition[];
};
/** 后端服务请求返回参数 */
type ResponseType = {
/** 执行成功 */
success?: boolean;
/** 状态码 */
code?: number;
/** 错误码 */
errorCode?: number;
/** 错误信息 */
errorMessage?: any;
/** 消息显示类型 */
showType?: number;
/** 数据 */
data?: T;
/** 附加数据 */
extras?: any;
/** 时间戳 */
timestamp?: number;
};
/** 分页数据对象 */
type PageResponse = {
/** 当前页值 */
pageIndex: number;
/** 每页大小 */
pageSize: number;
/** 数据总数 */
totalCount: number;
/** 总页数 */
totalPage?: number;
/** 数据行 */
items?: T[];
};
${es}}
}`,
);
execSync(`prettier --write ${TYPING_D}`);
};
/** 生成控制器文件 */
const generateController = (paths, tagDict, enums) => {
// 构造控制器
let controllers = {};
for (const apiPath in paths) {
const actionName = getActionName(apiPath);
const api = paths[apiPath];
for (const verb in api) {
// 只处理 post 和 get,其它忽略
if (!['post', 'get'].includes(verb)) {
continue;
}
const action = api[verb];
const actionDesc = action.summary ?? actionName;
// tag 作为控制器名称,如果没有 tag 则跳过
const tags = action.tags ?? [];
if (tags.length < 1) {
continue;
}
for (const tag of tags) {
const controllerName = getControllerName(tag);
if (!controllers.hasOwnProperty(controllerName)) {
controllers[controllerName] = {
description: tagDict[tag],
actions: [],
actionNames: [],
enums: [],
};
}
let paramsType = '';
let dataType = '';
let responseType = '';
if (action.parameters) {
paramsType = getParamsType(action.parameters);
}
if (action.requestBody) {
dataType = getRequestBodyType(action.requestBody);
}
if (action.responses) {
responseType = getResponseType(action.responses);
}
const es = enums.filter(t => paramsType.indexOf(t) !== -1 || dataType.indexOf(t) !== -1 || responseType.indexOf(t) !== -1);
if (es.length > 0) {
controllers[controllerName].enums.push(es[0]);
}
const actionFullDesc = `/** ${actionDesc} ${verb.toUpperCase()} ${apiPath} */\n`;
let actionBody = actionFullDesc;
// export api
if (actionName.toLowerCase().includes('export') || actionName.toLowerCase().includes('download')) {
actionBody = `${actionFullDesc}export async function ${actionName}(`;
if (verb == 'get') {
if (paramsType != '') {
actionBody = `${actionBody}params:${paramsType},`;
}
} else {
if (paramsType != '') {
actionBody = `${actionBody}params:${paramsType},`;
}
if (dataType != '') {
const prefix = isSimpleType(dataType) ? '' : 'API.';
actionBody = `${actionBody}data:${prefix}${dataType},`;
}
}
actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
actionBody = `${actionBody}const url='${apiPath}';\n`;
actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
if (verb == 'get' && paramsType != '') {
actionBody = `${actionBody}params,`;
}
if (verb == 'post') {
let pbv = '';
if (paramsType != '') {
pbv = 'params,';
}
if (dataType != '') {
pbv = `${pbv}data,`;
}
actionBody = `${actionBody}${pbv}`;
}
actionBody = `${actionBody}...(options||{}), responseType: 'blob', getResponse: true} as RequestOptions;\n`;
actionBody = `${actionBody}const res = await request(url, config);\n`;
actionBody = `${actionBody}const hcd = res.request.getResponseHeader('Content-Disposition');\n`;
actionBody = `${actionBody}if(!hcd){ return null; }\n`;
actionBody = `${actionBody}let fileName = '';\n`;
actionBody = `${actionBody}const cd = contentDisposition.parse(hcd)\n`;
actionBody = `${actionBody}if (cd?.parameters?.filename) { fileName = cd?.parameters?.filename; }\n`;
actionBody = `${actionBody}return { fileName, data:res.data };\n}\n`;
}
// other api
else {
actionBody = `${actionFullDesc}export async function ${actionName}(`;
if (verb == 'get') {
if (paramsType != '') {
actionBody = `${actionBody}params:${paramsType},`;
}
} else {
if (paramsType != '') {
actionBody = `${actionBody}params:${paramsType},`;
}
if (dataType != '') {
const prefix = isSimpleType(dataType) ? '' : 'API.';
actionBody = `${actionBody}data:${prefix}${dataType},`;
}
}
actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
actionBody = `${actionBody}const url='${apiPath}';\n`;
actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
if (verb == 'get' && paramsType != '') {
actionBody = `${actionBody}params,`;
}
if (verb == 'post') {
let pbv = '';
if (paramsType != '') {
pbv = 'params,';
}
if (dataType != '') {
pbv = `${pbv}data,`;
}
actionBody = `${actionBody}${pbv}`;
}
actionBody = `${actionBody}...(options||{})};\n`;
actionBody = `${actionBody}const res = await request>(url, config);`;
actionBody = `${actionBody}return res?.data;\n}\n`;
}
controllers[controllerName].actions.push(actionBody);
controllers[controllerName].actionNames.push(`${actionFullDesc}${actionName},\n`);
}
}
}
// 生成控制器文件
let csnames = [];
for (let ck in controllers) {
const c = controllers[ck];
const es = [...new Set(c.enums)].map((t) => `${t}, `);
let importEnums = '';
if (es.length > 0) {
importEnums = `\nimport { ${es.join('')} } from '../enums';`;
}
csnames.push({ name: ck, description: c.description });
const controllerFile = `${API_PATH}/${ck}.ts`;
const isBolb = c.actionNames.findIndex(t => t.includes('export') || t.includes('download')) !== -1;
fs.writeFileSync(
controllerFile,
`${UnifyComment}
// --------------------------------------------------------------------------
// ${c.description}
// --------------------------------------------------------------------------
import { request${isBolb ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
${isBolb ? 'import contentDisposition from \'content-disposition\';' : ''}
${c.actions.join('\n')}
export default {
${c.actionNames.join('')}
};
`,
);
execSync(`prettier --write ${controllerFile}`);
}
// 生成index.ts
const ecs = csnames.map((t) => `import * as ${t.name} from './${t.name}';\n`);
const edefaults = csnames.map((t) => `/** ${t.description} */\n${t.name},\n`);
const indexFile = `${API_PATH}/index.ts`;
fs.writeFileSync(
indexFile,
`${UnifyComment}
${ecs.join('')}
export default {
${edefaults.join('')}
};
`,
);
execSync(`prettier --write ${indexFile}`);
};
// ----------------------------------------
/** 生成接口服务 */
const generate = (error, response, body) => {
// OpenApi 描述文件
const swagger = JSON.parse(body);
// 实体模型
const schemas = swagger['components']['schemas'];
// API路径
const apiPaths = swagger['paths'];
// 开始处理
console.info('生成后台接口文件');
console.info(`-----------------\n${new Date().toLocaleString()}\n-----------------\n清理文件`);
clearFiles(ROOT_PATH);
if (!fs.existsSync(API_PATH)) {
fs.mkdirSync(API_PATH);
} else {
clearFiles(API_PATH);
}
console.info('清理完成!');
console.log('-----------------\n开始生成');
// 生成枚举文件
console.log('> 生成枚举文件');
const enums = generateEnums(schemas);
// 生成实体类型文件
console.log('> 生成实体类型文件');
generateEntity(schemas, enums);
// 生成控制器文件
console.log('> 生成控制器文件');
generateController(apiPaths, getTagDescDict(swagger['tags'] ?? []), enums);
console.info('生成完成!');
};
const request = require('request');
request.get(config.swaggerJson, generate);