/** * 根据 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);