import * as LibFs from 'mz/fs'; import * as program from 'commander'; import * as LibPath from 'path'; import { FieldInfo, lcfirst, MethodInfo, mkdir, parseMsgNamesFromProto, parseProto, ProtoFile, ProtoMsgImportInfo, ProtoMsgImportInfos, ProtoParseResult, readProtoList, ucfirst, JoiComment } from './lib/lib'; const pkg = require('../../package.json'); const TypeDefaultValue = { double: 0, float: 0, int32: 0, int64: 0, uint32: 0, uint64: 0, sint32: 0, sint64: 0, fixed32: 0, fixed64: 0, sfixed32: 0, sfixed64: 0, string: '', bool: false }; const enum ParamType { REQUEST = 1, RESPONSE } program.version(pkg.version) .option('-p, --proto ', 'directory of proto files') .option('-o, --output ', 'directory to output document') .option('-i, --import ', 'third party proto import path: e.g path1,path2,path3', function list(val) { return val.split(','); }) .option('-a, --all', 'generate all service in one md document') .option('-s, --service', 'generate a md document for each service') .option('-m, --method', 'generate a md document for each method') .parse(process.argv); const PROTO_DIR = (program as any).proto === undefined ? undefined : LibPath.normalize((program as any).proto); const OUTPUT_DIR = (program as any).output === undefined ? undefined : LibPath.normalize((program as any).output); const IMPORTS = (program as any).import === undefined ? [] : (program as any).import; const ALL = (program as any).all !== undefined; const SERVICE = (program as any).service !== undefined || true; const METHOD = (program as any).method != undefined; class DocumentCLI { private _rootFiles: Array = []; private _protoFiles: Array = []; private _serviceInfos: ProtoMsgImportInfos = {}; private _typeInfos: ProtoMsgImportInfos = {}; private _serviceIndex = -1; private _methodIndex = -1; static instance() { return new DocumentCLI(); } public async run() { console.log('DocumentCLI start.'); await this._validate(); await this._loadProtos(); await this._genDocuments(); } private async _validate() { console.log('DocumentCLI validate.'); if (!PROTO_DIR) { throw new Error('--proto is required'); } if (!OUTPUT_DIR) { throw new Error('--output is required'); } let protoStat = await LibFs.stat(PROTO_DIR); if (!protoStat.isDirectory()) { throw new Error('--proto is not a directory'); } let outputStat = await LibFs.stat(OUTPUT_DIR); if (!outputStat.isDirectory()) { throw new Error('--output is not a directory'); } } private async _loadProtos() { console.log('ServiceCLI load result files.'); this._rootFiles = await readProtoList(PROTO_DIR, OUTPUT_DIR); this._protoFiles = this._protoFiles.concat(this._rootFiles); if (IMPORTS.length > 0) { for (let i = 0; i < IMPORTS.length; i++) { this._protoFiles = this._protoFiles.concat(await readProtoList(LibPath.normalize(IMPORTS[i]), OUTPUT_DIR)); } } if (this._protoFiles.length === 0) { throw new Error('no proto files found'); } } private async _genDocuments() { console.log('DocumentCLI generate Documents.'); // 从 proto 文件中解析出 ProtobufIParserResult 数据 let parseResults = [] as Array; for (let i = 0; i < this._protoFiles.length; i++) { let protoFile = this._protoFiles[i]; if (!protoFile) { continue; } let parseResult = {} as ProtoParseResult; parseResult.result = await parseProto(protoFile); parseResult.protoFile = protoFile; parseResults.push(parseResult); let msgImportInfos = parseMsgNamesFromProto(parseResult.result, protoFile); for (let msgTypeStr in msgImportInfos) { const msgInfo: ProtoMsgImportInfo = msgImportInfos[msgTypeStr]; if (msgInfo.methods && msgInfo.methods.length > 0) { this._serviceInfos[msgTypeStr] = msgInfo; } else if (msgInfo.fields && msgInfo.fields.length > 0) { this._typeInfos[msgTypeStr] = msgInfo; } } } // make router dir in OUTPUT_DIR await mkdir(LibPath.join(OUTPUT_DIR, 'document')); if (ALL) { this._serviceIndex = 0; await LibFs.writeFile(LibPath.join(OUTPUT_DIR, 'document', 'Document.md'), this._genAll()); } if (SERVICE) { this._serviceIndex = -1; const servicePath = LibPath.join(OUTPUT_DIR, 'document'); await mkdir(servicePath); Object.keys(this._serviceInfos).forEach(async (key) => { const service = this._serviceInfos[key] as ProtoMsgImportInfo; if (this._rootFiles.indexOf(service.protoFile) >= 0) { this._methodIndex = 0; await LibFs.writeFile(LibPath.join(servicePath, service.msgType.replace(/^\S+\./, '') + '.md'), this._genService(service)); } }); } if (METHOD) { this._methodIndex = -1; const routerPath = LibPath.join(OUTPUT_DIR, 'document', 'method'); await mkdir(routerPath); Object.keys(this._serviceInfos).forEach(async (key) => { const service = this._serviceInfos[key] as ProtoMsgImportInfo; if (this._rootFiles.indexOf(service.protoFile) >= 0) { service.methods.forEach(async (method) => { await LibFs.writeFile(LibPath.join(routerPath, method.methodName + '.md'), this._genMethod(method)); }); } }); } } private _genAll(): string { let services = ''; Object.keys(this._serviceInfos).forEach(async (key) => { const service = this._serviceInfos[key] as ProtoMsgImportInfo; if (this._rootFiles.indexOf(service.protoFile) >= 0) { services += this._genService(service); } }); return ` # 接口文档 [TOC] ${services} `; } private _genService(service: ProtoMsgImportInfo): string { let methods = ''; service.methods.forEach((method) => { methods += this._genMethod(method); }); if (this._serviceIndex !== -1) { this._serviceIndex++; } return ` ## ${this._serviceIndex !== -1 ? this._serviceIndex + '. ' : ''}${service.msgType.replace(/^\S+\./, '')} ${this._serviceIndex === -1 ? '[TOC]' : ''} ${methods} `; } private _genMethod(method: MethodInfo): string { let methodDesc = '无'; if (method.methodComment && typeof method.methodComment === 'object' && method.methodComment.hasOwnProperty('Desc')) { methodDesc = method.methodComment['Desc']; } if (this._methodIndex !== -1) { this._methodIndex++; } return ` ### ${this._methodIndex === -1 ? '' : this._methodIndex + '. '}${method.methodName} **简要描述:** - ${methodDesc} **请求${method.googleHttpOption ? 'Router:' : '方法:'}** - \` ${method.googleHttpOption ? method.googleHttpOption.router : lcfirst(method.methodName)} \` **请求方式:** - ${method.googleHttpOption ? method.googleHttpOption.method : 'RPC'} ${this._genParam(method.requestType, ParamType.REQUEST, method.googleHttpOption !== undefined)} ${this._genParam(method.responseType, ParamType.RESPONSE, method.googleHttpOption !== undefined)} `; } private _genParam(paramName: string, paramType: ParamType, isGateway: boolean): string { let childData: Array = []; let param = ''; let paramObject = {}; let childContent = ''; const isRequestStyle: boolean = (paramType === ParamType.REQUEST) && isGateway; if (this._typeInfos.hasOwnProperty(paramName)) { const msgImport = this._typeInfos[paramName] as ProtoMsgImportInfo; msgImport.fields.forEach((field) => { param += this._genFieldInfo(field, paramType, childData, paramObject); }); } if (childData.length > 0) { childData = childData.reverse(); childData.forEach((childType) => { childContent += this._genChildContent(childType, paramType, isGateway); }); } let content = ` **${paramType === ParamType.REQUEST ? '请求' : '返回'}结构说明:** ${isRequestStyle ? '|参数名|必选|类型|默认值|说明|' : '|参数名|类型|说明|'} ${isRequestStyle ? '|:---|:---|:---|:---|:---|' : '|:---|:---|:---|'} ${param} ${childContent} **参数示例** \`\`\` ${JSON.stringify(paramObject, null, 2)} \`\`\` `; return content; } private _genChildContent(type: string, paramType: ParamType, isGateway: boolean): string { if (!this._typeInfos.hasOwnProperty(type)) { return ''; } const msgImport = this._typeInfos[type] as ProtoMsgImportInfo; let param = ''; msgImport.fields.forEach((field) => { param += this._genFieldInfo(field, paramType); }); const isRequestStyle: boolean = (paramType === ParamType.REQUEST) && isGateway; let content = ` **${type.replace(/^\S+\./, '')}数据格式描述:** ${isRequestStyle ? '|字段名|必选|类型|默认值|说明|' : '|字段名|类型|说明|'} ${isRequestStyle ? '|:---|:---|:---|:---|:---|' : '|:---|:---|:---|'} ${param} `; return content; } private _genFieldInfo(field: FieldInfo, paramType: ParamType, childData?: Array, paramObject?: object): string { let isRequired = false; let isRepeated = field.isRepeated; let keyType = field.keyType; let defaultValue = TypeDefaultValue[field.fieldType]; defaultValue = defaultValue === undefined ? '{}' : defaultValue; let desc = '无'; let fieldType; if (isRepeated) { fieldType = 'Array< T >'; } if (keyType) { fieldType = `Map< ${ucfirst(keyType)}, T >`; } if (field.fieldInfo && typeof field.fieldInfo === 'string') { const msgTypeStr = field.fieldInfo as string; if (this._typeInfos.hasOwnProperty(msgTypeStr)) { const nextFields = this._typeInfos[msgTypeStr].fields; let childParamObject = {}; nextFields.forEach((nextField) => { this._genFieldInfo(nextField, paramType, childData, childParamObject); }); if (childData) { childData.push(msgTypeStr); } if (paramObject) { paramObject[`${field.fieldName}${isRepeated ? 'List' : ''}`] = isRepeated ? [childParamObject] : childParamObject; } const objectType = `${field.fieldType.replace(/^\S+\./, '')}`; fieldType = fieldType ? fieldType.replace('T', objectType) : objectType; } } else { // |参数名|必选|类型|默认值|说明| if (field.fieldComment && typeof field.fieldComment === 'object') { if (field.fieldComment.hasOwnProperty('Joi')) { const joiComment = field.fieldComment['Joi'] as JoiComment; isRequired = joiComment.required; defaultValue = joiComment.defaultValue || defaultValue; } if (field.fieldComment.hasOwnProperty('Desc')) { desc = field.fieldComment['Desc'] as string; } } if (paramObject) { paramObject[field.fieldName] = isRepeated ? [defaultValue] : defaultValue; } fieldType = fieldType ? fieldType.replace('T', ucfirst(field.fieldType)) : ucfirst(field.fieldType); } if (paramType === ParamType.REQUEST) { defaultValue = typeof defaultValue === 'string' ? `"${defaultValue}"` : defaultValue; return `|${field.fieldName}${isRepeated ? 'List' : ''}|${isRequired ? '必传' : '可传'}|${fieldType}|${isRequired ? '---' : defaultValue}|${desc}|\n`; } else { return `|${field.fieldName}${isRepeated ? 'List' : ''}|${fieldType}|${desc}|\n`; } } } DocumentCLI.instance().run().catch((err: Error) => { console.log('err: ', err.message); });