import * as LibFs from 'mz/fs';
import * as program from 'commander';
import * as LibPath from 'path';
import {
genRpcMethodInfo, lcfirst, mkdir, parseMsgNamesFromProto, parseProto, parseServicesFromProto, Proto, ProtoFile,
ProtoMsgImportInfos, ProtoParseResult, readProtoList, RpcMethodImportPathInfos, RpcMethodInfo, RpcProtoServicesInfo
} from './lib/lib';
import { TplEngine } from './lib/template';
import { Method as ProtobufMethod, Service as ProtobufService } from 'protobufjs';
const pkg = require('../../package.json');
program.version(pkg.version)
.option('-p, --proto
', 'directory of proto files')
.option('-o, --output ', 'directory to output service codes')
.option('-i, --import ', 'third party proto import path: e.g path1,path2,path3', function list(val) {
return val.split(',');
})
.option('-e, --exclude ', 'files or paths in -p shall be excluded: e.g file1,path1,path2,file2', function list(val) {
return val.split(',');
})
.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 EXCLUDES = (program as any).exclude === undefined ? [] : (program as any).exclude;
class ServiceCLI {
private _protoFiles: Array = [];
private _protoMsgImportInfos: ProtoMsgImportInfos = {};
static instance() {
return new ServiceCLI();
}
public async run() {
console.log('ServiceCLI start.');
await this._validate();
await this._loadProtos();
await this._genProtoServices();
}
private async _validate() {
console.log('ServiceCLI 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 proto files.');
this._protoFiles = await readProtoList(PROTO_DIR, OUTPUT_DIR);
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 _genProtoServices() {
console.log('ServiceCLI generate services.');
let protoServicesInfos = [] as Array;
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) {
this._protoMsgImportInfos[msgTypeStr] = msgImportInfos[msgTypeStr];
}
}
await mkdir(LibPath.join(OUTPUT_DIR, 'services'));
for (let i = 0; i < parseResults.length; i++) {
let protoInfo = parseResults[i] as ProtoParseResult;
let services = parseServicesFromProto(protoInfo.result);
if (services.length === 0) {
continue;
}
// handle excludes
let protoFilePath = LibPath.join(protoInfo.protoFile.protoPath, protoInfo.protoFile.relativePath, protoInfo.protoFile.fileName);
let shallIgnore = false;
if (EXCLUDES.length > 0) {
EXCLUDES.forEach((exclude: string) => {
if (protoFilePath.indexOf(LibPath.normalize(exclude)) !== -1) {
shallIgnore = true;
}
});
}
for (let i = 0; i < services.length; i++) {
let methodInfos = await this._genService(protoInfo.protoFile, services[i], shallIgnore);
if (!shallIgnore) {
let protoServicesInfo = {
protoFile: protoInfo.protoFile,
protoServiceImportPath: Proto.genProtoServiceImportPath(protoInfo.protoFile),
services: {} as { [serviceName: string]: Array },
protoMessageImportPath: {},
} as RpcProtoServicesInfo;
protoServicesInfo.services[services[i].name] = methodInfos;
const importSet: { [key: string]: Set } = {};
methodInfos.forEach((methodInfo: RpcMethodInfo) => {
const imports: RpcMethodImportPathInfos = methodInfo.protoMsgImportPath;
for (const path of Object.keys(imports)) {
const importValues = imports[path] as string[];
if(!importSet[path]) {
importSet[path] = new Set();
}
for(const importValue of importValues) {
importSet[path].add(importValue);
}
}
});
for(const key of Object.keys(importSet)) {
const set = importSet[key];
if(set.size > 0) {
protoServicesInfo.protoMessageImportPath[key.substring(9)] = [...set];
}
}
protoServicesInfos.push(protoServicesInfo);
}
}
}
if (protoServicesInfos.length === 0) {
return;
}
let outputPath = LibPath.join(OUTPUT_DIR, 'services', 'Register.ts');
TplEngine.registerHelper('lcfirst', lcfirst);
let content = TplEngine.render('rpcs/register', {
infos: protoServicesInfos,
});
await LibFs.writeFile(outputPath, content);
}
private async _genService(protoFile: ProtoFile, service: ProtobufService, shallIgnore: boolean = false): Promise> {
console.log('ServiceCLI generate service: %s', service.name);
let methodKeys = Object.keys(service.methods);
if (methodKeys.length === 0) {
return;
}
let methodInfos = [];
for (let i = 0; i < methodKeys.length; i++) {
let methodKey = methodKeys[i];
let method = service.methods[methodKey];
methodInfos.push(await this._genServiceMethod(protoFile, service, method, shallIgnore));
}
return Promise.resolve(methodInfos);
}
private async _genServiceMethod(protoFile: ProtoFile, service: ProtobufService, method: ProtobufMethod, shallIgnore: boolean = false): Promise {
console.log('ServiceCLI generate service method: %s.%s', service.name, method.name);
let outputPath = Proto.genFullOutputServicePath(protoFile, service, method);
let methodInfo = genRpcMethodInfo(protoFile, method, outputPath, this._protoMsgImportInfos);
if (!method.requestStream && !method.responseStream) {
methodInfo.callTypeStr = 'ServerUnaryCall';
methodInfo.callGenerics = `<${methodInfo.requestTypeStr}>`;
methodInfo.hasCallback = true;
methodInfo.hasRequest = true;
} else if (!method.requestStream && method.responseStream) {
methodInfo.callTypeStr = 'ServerWritableStream';
methodInfo.callGenerics = `<${methodInfo.requestTypeStr}>`;
methodInfo.hasRequest = true;
} else if (method.requestStream && !method.responseStream) {
methodInfo.callTypeStr = 'ServerReadableStream';
methodInfo.callGenerics = `<${methodInfo.requestTypeStr}>`;
methodInfo.hasCallback = true;
} else if (method.requestStream && method.responseStream) {
methodInfo.callTypeStr = 'ServerDuplexStream';
methodInfo.callGenerics = `<${methodInfo.requestTypeStr}, ${methodInfo.responseTypeStr}>`;
}
// write files
if (!shallIgnore) {
await mkdir(LibPath.dirname(outputPath));
let content = TplEngine.render('rpcs/service', {
callTypeStr: methodInfo.callTypeStr,
callGenerics: methodInfo.callGenerics,
requestTypeStr: methodInfo.requestTypeStr,
responseTypeStr: methodInfo.responseTypeStr,
hasCallback: methodInfo.hasCallback,
hasRequest: methodInfo.hasRequest,
methodName: methodInfo.methodName,
protoMsgImportPath: methodInfo.protoMsgImportPath,
});
await LibFs.writeFile(outputPath, content);
}
return Promise.resolve(methodInfo);
}
}
ServiceCLI.instance().run().catch((err: Error) => {
console.log('err: ', err.message);
});