UNPKG

@qc2168/swagger-egg

Version:

swagger document generator for egg.

239 lines (216 loc) 9.33 kB
'use strict'; const fs = require('fs'); const fse = require('fs-extra'); const assert = require('assert'); const path = require('path'); const Router = require('./router_loader'); const { globbyFile, loadFile } = require('./file_loader'); const commentLoader = require('./comment/comment_loader'); const commentParser = require('./comment/comment_parser'); const schemaLoader = require('./schema_loader'); const ts2schemaLoader = require('./ts2schema_loader'); const utils = require('./utils'); const crypto = require('crypto'); class Swagger { constructor(app, pathToSwaggerUi) { this.app = app; this.swaggerUiPath = pathToSwaggerUi; this.baseDir = app.config.baseDir; this.config = app.config.swaggerEgg; this.supportDataType = ['string', 'number', 'integer', 'boolean', 'array', 'file']; this.fileExtend = utils.detectProjectLanguage(app); this.build(); } build() { // Prepare swagger default object this.swaggerObject = utils.prepareSwaggerObject(this.app, utils.prepareDefaultOptions(this.config)); // Load router file const routerPath = path.join(this.baseDir, `/app/router.${this.fileExtend}`); this.router = new Router(this.app, routerPath); this.router.getRouter(); // Load schema file const schemaPath = path.join(this.baseDir, this.config.schema.path); const schemaObject = this.config.typescriptJsonSchema ? ts2schemaLoader(schemaPath, this.baseDir) : schemaLoader(schemaPath); if (schemaObject.definitions) { this.swaggerObject.definitions = schemaObject.definitions; } // add inner definitions Object.keys(schemaObject).forEach((key) => { if (schemaObject[key].definitions) { this.swaggerObject.definitions = Object.assign( this.swaggerObject.definitions || {}, schemaObject[key].definitions ); } }); this.schema = schemaObject; // add swagger attribute to app instance this.app.swagger = { schema: schemaObject, }; // Scanner controller directory const controllerDirectory = path.join(this.baseDir, '/app/controller'); const files = globbyFile(controllerDirectory); for (const file of files) { const fileName = utils.camelCase(file.replace(/\.(js|ts)$/, '').replace('/', '.')); const filePath = path.join(this.baseDir, `/app/controller/${file}`); const comments = commentLoader(filePath); let functionName = null; if (!comments.length) continue; for (const comment of comments) { if (!comment.tags.length) continue; const { tags, description: rawDescription } = comment; const description = rawDescription.replace(/\s{0,}#swagger-api$/g, ''); let functionIndex = null; // Get action from jsdoc function tag tags.forEach((tag, index) => { if (tag.title === 'function') { assert(!functionIndex, `Duplex function tag of ${JSON.stringify(tag)}`); functionIndex = index; } }); if (functionIndex === null) continue; const tag = tags.splice(functionIndex, 1); functionName = tag[0].name; const action = `controller\.${fileName}\.${functionName}`; const appPrefixAction = `app.controller\.${fileName}\.${functionName}`; const operationId = `${fileName}/${functionName}`; // Get method and path from routerMap const routerObject = this.router.routerMap.get(action) || this.router.routerMap.get(appPrefixAction); if (routerObject) { let { path, method } = routerObject; if (!this.swaggerObject.paths[path]) { this.swaggerObject.paths[path] = {}; } this.swaggerObject.paths[path][method] = { description, operationId, parameters: [], responses: {}, }; // Deal with other swagger tags for (const tag of tags) { const { title, description } = tag; if (title === 'description' && description.length) { const types = description.match(/#(parameters|responses|consumes|produces|tags)/g); if (!types) continue; const type = types.shift(); const params = description.replace(`#${type}`, ''); switch (type) { case '#parameters': path = this.getParameter(path, method, params); break; case '#responses': this.getResponnseParser(path, method, params); break; case '#consumes': this.getCommonParser(path, method, params, 'consumes'); break; case '#produces': this.getCommonParser(path, method, params, 'produces'); break; default: this.getCommonParser(path, method, params, 'tags'); break; } } else if (title === 'summary' && description.length) { this.swaggerObject.paths[path][method].summary = description; } } } } } // Generate swagger.json file this.generateDocument(); // Change swagger-ui-dist's default api url config const swaggerUiBundlePath = path.join(this.swaggerUiPath, './index.html'); const prefix = this.app.config.static.prefix || '/public'; const swaggerUiConfig = this.config.swaggerUI; // swagger ui config swaggerUiConfig.url = `"${prefix}${/\/$/.test(prefix) ? '' : '/'}swagger/index.json"`; const bundleFileString = utils.prepareSwaggerUi(loadFile(swaggerUiBundlePath), swaggerUiConfig); fs.unlinkSync(swaggerUiBundlePath); // delete old file fs.appendFileSync(swaggerUiBundlePath, bundleFileString); // create new file } /** * Generate swagger.json file * * @memberof Swagger */ generateDocument() { const swaggerDocString = utils.finalize(this.swaggerObject); const fileMd5Name = crypto.createHash('md5').update(swaggerDocString).digest('hex'); this.fileMd5Name = fileMd5Name; // set md5 file name const filePath = path.join(this.app.baseDir, `app/public/swagger/index.json`); const dirPath = path.join(this.app.baseDir, 'app/public/swagger'); fse.emptyDirSync(dirPath); // empty target dir fs.appendFileSync(filePath, swaggerDocString); } getCommonParser(path, method, commomParams, type) { if (!this.tags && type === 'tags') { this.tags = this.swaggerObject.tags.map((tagObject) => tagObject.name); } const params = commomParams.match(/\s+(\S)+/g) || []; const trimParams = params.map((param) => { const trimParams = param.trim(); if (type === 'tags') { assert(this.tags.includes(trimParams), `${param} tag is not defined in swagger config`); } return trimParams; }); this.swaggerObject.paths[path][method][type] = trimParams; } getParameter(path, method, parameterComments) { let parameter = commentParser.parameterParser(parameterComments); // replace in parameter in path if (parameter.in === 'path') { const originalPath = path; const originalPathObj = this.swaggerObject.paths[originalPath]; const reg = new RegExp(`:${parameter.name}`, 'g'); path = path.replace(reg, `{${parameter.name}}`); this.swaggerObject.paths[path] = Object.assign({}, this.swaggerObject.paths[path], originalPathObj); if (originalPath !== path) delete this.swaggerObject.paths[originalPath]; } const schema = this.getSchema(parameter.schemaArray); // delete temporary field delete parameter.schemaArray; if (parameter.in === 'body') parameter.schema = schema; if (parameter.in === 'query') { const properties = (schema.properties); parameter=[] for (const [key, value] of Object.entries(properties)) { parameter.push({ ...value, name:key,in: 'query', require: schema.required.includes(key) }) } } else Object.assign(parameter, schema); this.swaggerObject.paths[path][method].parameters.push(...(Array.isArray(parameter)?parameter:[parameter])); return path; } getResponnseParser(path, method, responseComments) { const parameter = commentParser.responnseParser(responseComments); const { status, description, schemaArray } = parameter; const schema = this.getSchema(schemaArray); if (schema && schema.$ref) { schema.$ref = decodeURIComponent(schema.$ref); } this.swaggerObject.paths[path][method].responses[status] = { description, schema, }; } getSchema(parameter) { const type = parameter[0]; if (this.supportDataType.includes(type)) { return { type, }; } const schemaObject = parameter.reduce((acc, item) => { assert(acc && typeof acc === 'object', `${item} schema is not defined`); return acc[item] || (acc.definitions && acc.definitions[item]); }, this); return schemaObject; } } module.exports = Swagger;