From 291ec9576f5033511f0436d9afd8ccf4f5ff6170 Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Sun, 11 Oct 2020 18:34:46 -0400 Subject: [PATCH] feat(ts-protoc-gen): initial commit, broken --- src/ts-protoc-gen/BUILD.bazel | 24 ++ src/ts-protoc-gen/CodePrinter.ts | 26 ++ src/ts-protoc-gen/ExportMap.ts | 84 ++++ src/ts-protoc-gen/Printer.ts | 30 ++ src/ts-protoc-gen/WellKnown.ts | 14 + src/ts-protoc-gen/index.ts | 61 +++ src/ts-protoc-gen/parameters.ts | 10 + src/ts-protoc-gen/rules/BUILD.bazel | 23 + .../rules/change_import_style.ts | 121 ++++++ src/ts-protoc-gen/rules/index.bzl | 255 +++++++++++ src/ts-protoc-gen/service/common.ts | 142 +++++++ src/ts-protoc-gen/service/grpcnode.ts | 127 ++++++ src/ts-protoc-gen/service/grpcweb.ts | 397 ++++++++++++++++++ src/ts-protoc-gen/ts/FieldTypes.ts | 59 +++ src/ts-protoc-gen/ts/enum.ts | 16 + src/ts-protoc-gen/ts/extensions.ts | 14 + src/ts-protoc-gen/ts/fileDescriptorTSD.ts | 49 +++ src/ts-protoc-gen/ts/message.ts | 235 +++++++++++ src/ts-protoc-gen/ts/oneof.ts | 16 + src/ts-protoc-gen/util.ts | 184 ++++++++ 20 files changed, 1887 insertions(+) create mode 100644 src/ts-protoc-gen/BUILD.bazel create mode 100644 src/ts-protoc-gen/CodePrinter.ts create mode 100644 src/ts-protoc-gen/ExportMap.ts create mode 100644 src/ts-protoc-gen/Printer.ts create mode 100644 src/ts-protoc-gen/WellKnown.ts create mode 100644 src/ts-protoc-gen/index.ts create mode 100644 src/ts-protoc-gen/parameters.ts create mode 100644 src/ts-protoc-gen/rules/BUILD.bazel create mode 100644 src/ts-protoc-gen/rules/change_import_style.ts create mode 100644 src/ts-protoc-gen/rules/index.bzl create mode 100644 src/ts-protoc-gen/service/common.ts create mode 100644 src/ts-protoc-gen/service/grpcnode.ts create mode 100644 src/ts-protoc-gen/service/grpcweb.ts create mode 100644 src/ts-protoc-gen/ts/FieldTypes.ts create mode 100644 src/ts-protoc-gen/ts/enum.ts create mode 100644 src/ts-protoc-gen/ts/extensions.ts create mode 100644 src/ts-protoc-gen/ts/fileDescriptorTSD.ts create mode 100644 src/ts-protoc-gen/ts/message.ts create mode 100644 src/ts-protoc-gen/ts/oneof.ts create mode 100644 src/ts-protoc-gen/util.ts diff --git a/src/ts-protoc-gen/BUILD.bazel b/src/ts-protoc-gen/BUILD.bazel new file mode 100644 index 0000000..4d76fbf --- /dev/null +++ b/src/ts-protoc-gen/BUILD.bazel @@ -0,0 +1,24 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "src", + srcs = glob([ + "*.ts", + "service/*.ts", + "ts/*.ts", + ]), + deps = [ + "@npm//@types/google-protobuf", + "@npm//@types/node", + "@npm//google-protobuf", + ], +) + +nodejs_binary( + name = "protoc-gen-ts", + data = [":src"], + entry_point = ":index.ts", +) diff --git a/src/ts-protoc-gen/CodePrinter.ts b/src/ts-protoc-gen/CodePrinter.ts new file mode 100644 index 0000000..7892e2a --- /dev/null +++ b/src/ts-protoc-gen/CodePrinter.ts @@ -0,0 +1,26 @@ +import {Printer} from "./Printer"; +import {generateIndent} from "./util"; + +export class CodePrinter { + private indentation: string; + constructor(private depth: number, private printer: Printer) { + this.indentation = generateIndent(1); + } + indent() { + this.depth++; + return this; + } + dedent() { + this.depth--; + return this; + } + printLn(line: string) { + this.printer.printLn(new Array(this.depth + 1).join(this.indentation) + line); + return this; + } + + printEmptyLn() { + this.printer.printEmptyLn(); + return this; + } +} diff --git a/src/ts-protoc-gen/ExportMap.ts b/src/ts-protoc-gen/ExportMap.ts new file mode 100644 index 0000000..c8410b3 --- /dev/null +++ b/src/ts-protoc-gen/ExportMap.ts @@ -0,0 +1,84 @@ +import { + FileDescriptorProto, + DescriptorProto, + MessageOptions, + EnumOptions, + FieldDescriptorProto +} from "google-protobuf/google/protobuf/descriptor_pb"; + +import Type = FieldDescriptorProto.Type; + +export type ExportMessageEntry = { + pkg: string, + fileName: string, + messageOptions: MessageOptions, + mapFieldOptions?: { + key: [Type, string], + value: [Type, string], + } +}; + +export type ExportEnumEntry = { + pkg: string, + fileName: string, + enumOptions: EnumOptions, +}; + +export class ExportMap { + messageMap: {[key: string]: ExportMessageEntry} = {}; + enumMap: {[key: string]: ExportEnumEntry} = {}; + + exportNested(scope: string, fileDescriptor: FileDescriptorProto, message: DescriptorProto) { + const messageEntry: ExportMessageEntry = { + pkg: fileDescriptor.getPackage(), + fileName: fileDescriptor.getName(), + messageOptions: message.getOptions(), + mapFieldOptions: message.getOptions() && message.getOptions().getMapEntry() ? { + key: [message.getFieldList()[0].getType(), message.getFieldList()[0].getTypeName().slice(1)], + value: [message.getFieldList()[1].getType(), message.getFieldList()[1].getTypeName().slice(1)], + } : undefined, + }; + + const packagePrefix = scope ? scope + "." : ""; + + const entryName = `${packagePrefix}${message.getName()}`; + this.messageMap[entryName] = messageEntry; + + message.getNestedTypeList().forEach(nested => { + this.exportNested(`${packagePrefix}${message.getName()}`, fileDescriptor, nested); + }); + + message.getEnumTypeList().forEach(enumType => { + const identifier = `${packagePrefix}${message.getName()}.${enumType.getName()}`; + this.enumMap[identifier] = { + pkg: fileDescriptor.getPackage(), + fileName: fileDescriptor.getName(), + enumOptions: enumType.getOptions(), + }; + }); + } + + addFileDescriptor(fileDescriptor: FileDescriptorProto) { + const scope = fileDescriptor.getPackage(); + fileDescriptor.getMessageTypeList().forEach(messageType => { + this.exportNested(scope, fileDescriptor, messageType); + }); + + fileDescriptor.getEnumTypeList().forEach(enumType => { + const packagePrefix = scope ? scope + "." : ""; + this.enumMap[packagePrefix + enumType.getName()] = { + pkg: fileDescriptor.getPackage(), + fileName: fileDescriptor.getName(), + enumOptions: enumType.getOptions(), + }; + }); + } + + getMessage(str: string): ExportMessageEntry | undefined { + return this.messageMap[str]; + } + + getEnum(str: string): ExportEnumEntry | undefined { + return this.enumMap[str]; + } +} diff --git a/src/ts-protoc-gen/Printer.ts b/src/ts-protoc-gen/Printer.ts new file mode 100644 index 0000000..4a31ef1 --- /dev/null +++ b/src/ts-protoc-gen/Printer.ts @@ -0,0 +1,30 @@ +import {generateIndent} from "./util"; + +export class Printer { + indentStr: string; + output: string = ""; + + constructor(indentLevel: number) { + this.indentStr = generateIndent(indentLevel); + } + + printLn(str: string) { + this.output += this.indentStr + str + "\n"; + } + + print(str: string) { + this.output += str; + } + + printEmptyLn() { + this.output += "\n"; + } + + printIndentedLn(str: string) { + this.output += this.indentStr + " " + str + "\n"; + } + + getOutput(): string { + return this.output; + } +} diff --git a/src/ts-protoc-gen/WellKnown.ts b/src/ts-protoc-gen/WellKnown.ts new file mode 100644 index 0000000..51556da --- /dev/null +++ b/src/ts-protoc-gen/WellKnown.ts @@ -0,0 +1,14 @@ +export const WellKnownTypesMap: {[key: string]: string} = { + "google/protobuf/compiler/plugin.proto": "google-protobuf/google/protobuf/compiler/plugin_pb", + "google/protobuf/any.proto": "google-protobuf/google/protobuf/any_pb", + "google/protobuf/api.proto": "google-protobuf/google/protobuf/api_pb", + "google/protobuf/descriptor.proto": "google-protobuf/google/protobuf/descriptor_pb", + "google/protobuf/duration.proto": "google-protobuf/google/protobuf/duration_pb", + "google/protobuf/empty.proto": "google-protobuf/google/protobuf/empty_pb", + "google/protobuf/field_mask.proto": "google-protobuf/google/protobuf/field_mask_pb", + "google/protobuf/source_context.proto": "google-protobuf/google/protobuf/source_context_pb", + "google/protobuf/struct.proto": "google-protobuf/google/protobuf/struct_pb", + "google/protobuf/timestamp.proto": "google-protobuf/google/protobuf/timestamp_pb", + "google/protobuf/type.proto": "google-protobuf/google/protobuf/type_pb", + "google/protobuf/wrappers.proto": "google-protobuf/google/protobuf/wrappers_pb" +}; diff --git a/src/ts-protoc-gen/index.ts b/src/ts-protoc-gen/index.ts new file mode 100644 index 0000000..33e69ac --- /dev/null +++ b/src/ts-protoc-gen/index.ts @@ -0,0 +1,61 @@ +import {printFileDescriptorTSD} from "./ts/fileDescriptorTSD"; +import {ExportMap} from "./ExportMap"; +import {replaceProtoSuffix, withAllStdIn, getParameterEnums} from "./util"; +import {CodeGeneratorRequest, CodeGeneratorResponse} from "google-protobuf/google/protobuf/compiler/plugin_pb"; +import {FileDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {generateGrpcWebService} from "./service/grpcweb"; +import {generateGrpcNodeService} from "./service/grpcnode"; +import {ServiceParameter} from "./parameters"; + +/** + * This is the ProtoC compiler plugin. + * + * The Protocol Buffers Compiler can be extended to [support new languages via plugins](https://developers.google.com/protocol-buffers/docs/reference/other). + * A plugin is just a program which reads a CodeGeneratorRequest protocol buffer from standard input + * and then writes a CodeGeneratorResponse protocol buffer to standard output. + * These message types are defined in [plugin.proto](https://github.com/google/protobuf/blob/master/src/google/protobuf/compiler/plugin.proto). + * + */ +withAllStdIn((inputBuff: Buffer) => { + try { + const typedInputBuff = new Uint8Array(inputBuff.length); + typedInputBuff.set(inputBuff); + + const codeGenRequest = CodeGeneratorRequest.deserializeBinary(typedInputBuff); + const codeGenResponse = new CodeGeneratorResponse(); + const exportMap = new ExportMap(); + const fileNameToDescriptor: {[key: string]: FileDescriptorProto} = {}; + + const parameter = codeGenRequest.getParameter(); + const {service, mode} = getParameterEnums(parameter); + + const generateGrpcWebServices = service === ServiceParameter.GrpcWeb; + const generateGrpcNodeServices = service === ServiceParameter.GrpcNode; + + codeGenRequest.getProtoFileList().forEach(protoFileDescriptor => { + fileNameToDescriptor[protoFileDescriptor.getName()] = protoFileDescriptor; + exportMap.addFileDescriptor(protoFileDescriptor); + }); + + codeGenRequest.getFileToGenerateList().forEach(fileName => { + const outputFileName = replaceProtoSuffix(fileName); + const thisFile = new CodeGeneratorResponse.File(); + thisFile.setName(outputFileName + ".d.ts"); + thisFile.setContent(printFileDescriptorTSD(fileNameToDescriptor[fileName], exportMap)); + codeGenResponse.addFile(thisFile); + + if (generateGrpcWebServices) { + generateGrpcWebService(outputFileName, fileNameToDescriptor[fileName], exportMap) + .forEach(file => codeGenResponse.addFile(file)); + } else if (generateGrpcNodeServices) { + const file = generateGrpcNodeService(outputFileName, fileNameToDescriptor[fileName], exportMap, mode); + codeGenResponse.addFile(file); + } + }); + + process.stdout.write(Buffer.from(codeGenResponse.serializeBinary())); + } catch (err) { + console.error("protoc-gen-ts error: " + err.stack + "\n"); + process.exit(1); + } +}); diff --git a/src/ts-protoc-gen/parameters.ts b/src/ts-protoc-gen/parameters.ts new file mode 100644 index 0000000..c5d8e7d --- /dev/null +++ b/src/ts-protoc-gen/parameters.ts @@ -0,0 +1,10 @@ +export enum ServiceParameter { + None, + GrpcWeb, + GrpcNode +} + +export enum ModeParameter { + None, + GrpcJs +} diff --git a/src/ts-protoc-gen/rules/BUILD.bazel b/src/ts-protoc-gen/rules/BUILD.bazel new file mode 100644 index 0000000..9e8b90c --- /dev/null +++ b/src/ts-protoc-gen/rules/BUILD.bazel @@ -0,0 +1,23 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") + +nodejs_binary( + name = "change_import_style", + data = [ + ":change_import_style_lib", + ], + entry_point = ":change_import_style.ts", + visibility = ["//visibility:public"], +) + +ts_library( + name = "change_import_style_lib", + srcs = [ + "change_import_style.ts", + ], + deps = [ + "@npm//@types/minimist", + "@npm//@types/node", + "@npm//minimist", + ], +) diff --git a/src/ts-protoc-gen/rules/change_import_style.ts b/src/ts-protoc-gen/rules/change_import_style.ts new file mode 100644 index 0000000..4c41036 --- /dev/null +++ b/src/ts-protoc-gen/rules/change_import_style.ts @@ -0,0 +1,121 @@ +/** + * Converts a list of generated protobuf-js files from commonjs modules into named AMD modules. + * + * Arguments: + * --workspace_name + * --input_base_path + * --output_module_name + * --input_file_path + * --output_file_path + */ +import minimist = require('minimist'); +import fs = require('fs'); + +function main() { + const args = minimist(process.argv.slice(2)); + + const initialContents = fs.readFileSync(args.input_file_path, 'utf8'); + + const umdContents = convertToUmd(args, initialContents); + fs.writeFileSync(args.output_umd_path, umdContents, 'utf8'); + + const commonJsContents = convertToESM(args, initialContents); + fs.writeFileSync(args.output_es6_path, commonJsContents, 'utf8'); +} + +function replaceRecursiveFilePaths(args: any) { + return (contents: string) => { + return contents.replace(/(\.\.\/)+/g, `${args.workspace_name}/`); + }; +} + +function removeJsExtensionsFromRequires(contents: string) { + return contents.replace(/(require\(.*).js/g, (_, captureGroup: string) => { + return captureGroup; + }); +} + +function convertToUmd(args: any, initialContents: string): string { + const wrapInAMDModule = (contents: string) => { + return `// GENERATED CODE DO NOT EDIT +(function (factory) { + if (typeof module === "object" && typeof module.exports === "object") { + var v = factory(require, exports); + if (v !== undefined) module.exports = v; + } + else if (typeof define === "function" && define.amd) { + define("${args.input_base_path}/${args.output_module_name}", factory); + } +})(function (require, exports) { + ${contents} +}); +`; + }; + + const transformations: ((c: string) => string)[] = [ + wrapInAMDModule, + replaceRecursiveFilePaths(args), + removeJsExtensionsFromRequires, + ]; + return transformations.reduce((currentContents, transform) => { + return transform(currentContents); + }, initialContents); +} + +// Converts the CommonJS format from protoc to the ECMAScript Module format. +// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules +function convertToESM(args: any, initialContents: string): string { + const replaceGoogExtendWithExports = (contents: string) => { + return contents.replace( + /goog\.object\.extend\(exports, ([\w\.]+)\);/g, + (_, packageName: string) => { + const exportSymbols = /goog\.exportSymbol\('([\w\.]+)',.*\);/g; + const symbols = []; + + let match: RegExpExecArray | null = exportSymbols.exec(initialContents); + while (match) { + // We want to ignore embedded export targets, IE: `DeliveryPerson.DataCase`. + const exportTarget = match[1].substr(packageName.length + 1); + if (!exportTarget.includes('.')) { + symbols.push(exportTarget); + } + match = exportSymbols.exec(initialContents); + } + + return `export const { ${symbols.join(', ')} } = ${packageName}`; + } + ); + }; + + const replaceRequiresWithImports = (contents: string) => { + return contents.replace( + /var ([\w\d_]+) = require\((['"][\w\d@/_-]+['"])\);/g, + 'import * as $1 from $2;' + ); + }; + + const replaceRequiresWithSubpackageImports = (contents: string) => { + return contents.replace( + /var ([\w\d_]+) = require\((['"][\w\d@/_-]+['"])\)\.([\w\d_]+);/g, + 'import * as $1 from $2;' + ); + }; + + const replaceCJSExportsWithECMAExports = (contents: string) => { + return contents.replace(/exports\.([\w\d_]+) = .*;/g, 'export { $1 };'); + }; + + const transformations: ((c: string) => string)[] = [ + replaceRecursiveFilePaths(args), + removeJsExtensionsFromRequires, + replaceGoogExtendWithExports, + replaceRequiresWithImports, + replaceRequiresWithSubpackageImports, + replaceCJSExportsWithECMAExports, + ]; + return transformations.reduce((currentContents, transform) => { + return transform(currentContents); + }, initialContents); +} + +main(); diff --git a/src/ts-protoc-gen/rules/index.bzl b/src/ts-protoc-gen/rules/index.bzl new file mode 100644 index 0000000..2bf0328 --- /dev/null +++ b/src/ts-protoc-gen/rules/index.bzl @@ -0,0 +1,255 @@ +load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "JSEcmaScriptModuleInfo", "JSNamedModuleInfo") +load("@rules_proto//proto:defs.bzl", "ProtoInfo") + +TypescriptProtoLibraryAspect = provider( + fields = { + "es5_outputs": "The ES5 JS files produced directly from the src protos", + "es6_outputs": "The ES6 JS files produced directly from the src protos", + "dts_outputs": "Ths TS definition files produced directly from the src protos", + "deps_es5": "The transitive ES5 JS dependencies", + "deps_es6": "The transitive ES6 JS dependencies", + "deps_dts": "The transitive dependencies' TS definitions", + }, +) + +def _proto_path(proto): + """ + The proto path is not really a file path + It's the path to the proto that was seen when the descriptor file was generated. + """ + path = proto.path + root = proto.root.path + ws = proto.owner.workspace_root + if path.startswith(root): + path = path[len(root):] + if path.startswith("/"): + path = path[1:] + if path.startswith(ws): + path = path[len(ws):] + if path.startswith("/"): + path = path[1:] + return path + +def _get_protoc_inputs(target, ctx): + inputs = [] + inputs += target[ProtoInfo].direct_sources + inputs += target[ProtoInfo].transitive_descriptor_sets.to_list() + return inputs + +def _get_input_proto_names(target): + """ + Builds a string containing all of the input proto file names separated by spaces. + """ + proto_inputs = [] + for src in target[ProtoInfo].direct_sources: + if src.extension != "proto": + fail("Input must be a proto file") + normalized_file = _proto_path(src) + proto_inputs.append(normalized_file) + return " ".join(proto_inputs) + +def _build_protoc_command(target, ctx): + protoc_command = "%s" % (ctx.executable._protoc.path) + + protoc_command += " --plugin=protoc-gen-ts=%s" % (ctx.executable._ts_protoc_gen.path) + + protoc_output_dir = ctx.var["BINDIR"] + protoc_command += " --ts_out=service=grpc-web:%s" % (protoc_output_dir) + protoc_command += " --js_out=import_style=commonjs,binary:%s" % (protoc_output_dir) + + descriptor_sets_paths = [desc.path for desc in target[ProtoInfo].transitive_descriptor_sets.to_list()] + protoc_command += " --descriptor_set_in=%s" % (":".join(descriptor_sets_paths)) + + protoc_command += " %s" % (_get_input_proto_names(target)) + + return protoc_command + +def _create_post_process_command(target, ctx, js_outputs, js_outputs_es6): + """ + Builds a post-processing command that: + - Updates the existing protoc output files to be UMD modules + - Creates a new es6 file from the original protoc output + """ + convert_commands = [] + for [output, output_es6] in zip(js_outputs, js_outputs_es6): + file_path = "/".join([p for p in [ + ctx.workspace_name, + ctx.label.package, + ] if p]) + file_name = output.basename[:-len(output.extension) - 1] + + convert_command = ctx.executable._change_import_style.path + convert_command += " --workspace_name {}".format(ctx.workspace_name) + convert_command += " --input_base_path {}".format(file_path) + convert_command += " --output_module_name {}".format(file_name) + convert_command += " --input_file_path {}".format(output.path) + convert_command += " --output_umd_path {}".format(output.path) + convert_command += " --output_es6_path {}".format(output_es6.path) + convert_commands.append(convert_command) + + return " && ".join(convert_commands) + +def _get_outputs(target, ctx): + """ + Calculates all of the files that will be generated by the aspect. + """ + js_outputs = [] + js_outputs_es6 = [] + dts_outputs = [] + for src in target[ProtoInfo].direct_sources: + file_name = src.basename[:-len(src.extension) - 1] + for f in ["_pb", "_pb_service"]: + full_name = file_name + f + output = ctx.actions.declare_file(full_name + ".js") + js_outputs.append(output) + output_es6 = ctx.actions.declare_file(full_name + ".mjs") + js_outputs_es6.append(output_es6) + + for f in ["_pb.d.ts", "_pb_service.d.ts"]: + output = ctx.actions.declare_file(file_name + f) + dts_outputs.append(output) + + return [js_outputs, js_outputs_es6, dts_outputs] + +def typescript_proto_library_aspect_(target, ctx): + """ + A bazel aspect that is applied on every proto_library rule on the transitive set of dependencies + of a typescript_proto_library rule. + + Handles running protoc to produce the generated JS and TS files. + """ + + [js_outputs, js_outputs_es6, dts_outputs] = _get_outputs(target, ctx) + protoc_outputs = dts_outputs + js_outputs + js_outputs_es6 + + all_commands = [ + _build_protoc_command(target, ctx), + _create_post_process_command(target, ctx, js_outputs, js_outputs_es6), + ] + + tools = [] + tools.extend(ctx.files._protoc) + tools.extend(ctx.files._ts_protoc_gen) + tools.extend(ctx.files._change_import_style) + + ctx.actions.run_shell( + inputs = depset(_get_protoc_inputs(target, ctx)), + outputs = protoc_outputs, + progress_message = "Creating Typescript pb files %s" % ctx.label, + command = " && ".join(all_commands), + tools = depset(tools), + ) + + dts_outputs = depset(dts_outputs) + es5_outputs = depset(js_outputs) + es6_outputs = depset(js_outputs_es6) + deps_dts = [] + deps_es5 = [] + deps_es6 = [] + + for dep in ctx.rule.attr.deps: + aspect_data = dep[TypescriptProtoLibraryAspect] + deps_dts.append(aspect_data.dts_outputs) + deps_dts.append(aspect_data.deps_dts) + deps_es5.append(aspect_data.es5_outputs) + deps_es5.append(aspect_data.deps_es5) + deps_es6.append(aspect_data.es6_outputs) + deps_es6.append(aspect_data.deps_es6) + + return [TypescriptProtoLibraryAspect( + dts_outputs = dts_outputs, + es5_outputs = es5_outputs, + es6_outputs = es6_outputs, + deps_dts = depset(transitive = deps_dts), + deps_es5 = depset(transitive = deps_es5), + deps_es6 = depset(transitive = deps_es6), + )] + +typescript_proto_library_aspect = aspect( + implementation = typescript_proto_library_aspect_, + attr_aspects = ["deps"], + attrs = { + "_ts_protoc_gen": attr.label( + allow_files = True, + executable = True, + cfg = "host", + default = Label("//src/ts-protoc-gen:protoc-gen-ts"), + ), + "_protoc": attr.label( + allow_single_file = True, + executable = True, + cfg = "host", + default = Label("@com_google_protobuf//:protoc"), + ), + "_change_import_style": attr.label( + executable = True, + cfg = "host", + allow_files = True, + default = Label("//src/ts-protoc-gen/rules:change_import_style"), + ), + }, +) + +def _typescript_proto_library_impl(ctx): + """ + Handles converting the aspect output into a provider compatible with the rules_typescript rules. + """ + aspect_data = ctx.attr.proto[TypescriptProtoLibraryAspect] + dts_outputs = aspect_data.dts_outputs + transitive_declarations = depset(transitive = [dts_outputs, aspect_data.deps_dts]) + es5_outputs = aspect_data.es5_outputs + es6_outputs = aspect_data.es6_outputs + outputs = depset(transitive = [es5_outputs, es6_outputs, dts_outputs]) + + es5_srcs = depset(transitive = [es5_outputs, aspect_data.deps_es5]) + es6_srcs = depset(transitive = [es6_outputs, aspect_data.deps_es6]) + return struct( + typescript = struct( + declarations = dts_outputs, + transitive_declarations = transitive_declarations, + es5_sources = es5_srcs, + es6_sources = es6_srcs, + transitive_es5_sources = es5_srcs, + transitive_es6_sources = es6_srcs, + ), + providers = [ + DefaultInfo(files = outputs), + DeclarationInfo( + declarations = dts_outputs, + transitive_declarations = transitive_declarations, + type_blacklisted_declarations = depset([]), + ), + JSNamedModuleInfo( + direct_sources = es5_srcs, + sources = es5_srcs, + ), + JSEcmaScriptModuleInfo( + direct_sources = es6_srcs, + sources = es6_srcs, + ), + ], + ) + +typescript_proto_library = rule( + attrs = { + "proto": attr.label( + mandatory = True, + allow_single_file = True, + providers = [ProtoInfo], + aspects = [typescript_proto_library_aspect], + ), + "_ts_protoc_gen": attr.label( + allow_files = True, + executable = True, + cfg = "host", + default = Label("//src/ts-protoc-gen:protoc-gen-ts"), + ), + "_protoc": attr.label( + allow_single_file = True, + executable = True, + cfg = "host", + default = Label("@com_google_protobuf//:protoc"), + ), + }, + implementation = _typescript_proto_library_impl, +) diff --git a/src/ts-protoc-gen/service/common.ts b/src/ts-protoc-gen/service/common.ts new file mode 100644 index 0000000..9e24596 --- /dev/null +++ b/src/ts-protoc-gen/service/common.ts @@ -0,0 +1,142 @@ +import {CodeGeneratorResponse} from "google-protobuf/google/protobuf/compiler/plugin_pb"; +import {FileDescriptorProto, MethodDescriptorProto, ServiceDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {ExportMap} from "../ExportMap"; +import {WellKnownTypesMap} from "../WellKnown"; +import {getFieldType, MESSAGE_TYPE} from "../ts/FieldTypes"; +import {filePathToPseudoNamespace, replaceProtoSuffix, getPathToRoot, normaliseFieldObjectName} from "../util"; + +export function createFile(output: string, filename: string): CodeGeneratorResponse.File { + const file = new CodeGeneratorResponse.File(); + file.setName(filename); + file.setContent(output); + return file; +} + +type CallingTypes = { + requestType: string + responseType: string +}; + +function getCallingTypes(method: MethodDescriptorProto, exportMap: ExportMap): CallingTypes { + return { + requestType: getFieldType(MESSAGE_TYPE, method.getInputType().slice(1), "", exportMap), + responseType: getFieldType(MESSAGE_TYPE, method.getOutputType().slice(1), "", exportMap), + }; +} + +function isUsed(fileDescriptor: FileDescriptorProto, pseudoNamespace: string, exportMap: ExportMap) { + return fileDescriptor.getServiceList().some(service => { + return service.getMethodList().some(method => { + const callingTypes = getCallingTypes(method, exportMap); + const namespacePackage = pseudoNamespace + "."; + return ( + callingTypes.requestType.indexOf(namespacePackage) === 0 || + callingTypes.responseType.indexOf(namespacePackage) === 0 + ); + }); + }); +} + +export type ImportDescriptor = { + readonly namespace: string + readonly path: string +}; + +export type RPCMethodDescriptor = { + readonly nameAsPascalCase: string, + readonly nameAsCamelCase: string, + readonly functionName: string, + readonly serviceName: string, + readonly requestStream: boolean + readonly responseStream: boolean + readonly requestType: string + readonly responseType: string +}; + +export class RPCDescriptor { + private readonly grpcService: GrpcServiceDescriptor; + private readonly protoService: ServiceDescriptorProto; + private readonly exportMap: ExportMap; + + constructor(grpcService: GrpcServiceDescriptor, protoService: ServiceDescriptorProto, exportMap: ExportMap) { + this.grpcService = grpcService; + this.protoService = protoService; + this.exportMap = exportMap; + } + get name(): string { + return this.protoService.getName(); + } + + get qualifiedName(): string { + return (this.grpcService.packageName ? `${this.grpcService.packageName}.` : "") + this.name; + } + + get methods(): RPCMethodDescriptor[] { + return this.protoService.getMethodList() + .map(method => { + const callingTypes = getCallingTypes(method, this.exportMap); + const nameAsCamelCase = method.getName()[0].toLowerCase() + method.getName().substr(1); + return { + nameAsPascalCase: method.getName(), + nameAsCamelCase, + functionName: normaliseFieldObjectName(nameAsCamelCase), + serviceName: this.name, + requestStream: method.getClientStreaming(), + responseStream: method.getServerStreaming(), + requestType: callingTypes.requestType, + responseType: callingTypes.responseType, + }; + }); + } +} + +export class GrpcServiceDescriptor { + private readonly fileDescriptor: FileDescriptorProto; + private readonly exportMap: ExportMap; + private readonly pathToRoot: string; + + constructor(fileDescriptor: FileDescriptorProto, exportMap: ExportMap) { + this.fileDescriptor = fileDescriptor; + this.exportMap = exportMap; + this.pathToRoot = getPathToRoot(fileDescriptor.getName()); + } + + get filename(): string { + return this.fileDescriptor.getName(); + } + + get packageName(): string { + return this.fileDescriptor.getPackage(); + } + + get imports(): ImportDescriptor[] { + const dependencies = this.fileDescriptor.getDependencyList() + .filter(dependency => isUsed(this.fileDescriptor, filePathToPseudoNamespace(dependency), this.exportMap)) + .map(dependency => { + const namespace = filePathToPseudoNamespace(dependency); + if (dependency in WellKnownTypesMap) { + return { + namespace, + path: WellKnownTypesMap[dependency], + }; + } else { + return { + namespace, + path: `${this.pathToRoot}${replaceProtoSuffix(replaceProtoSuffix(dependency))}` + }; + } + }); + const hostProto = { + namespace: filePathToPseudoNamespace(this.filename), + path: `${this.pathToRoot}${replaceProtoSuffix(this.filename)}`, + }; + return [ hostProto ].concat(dependencies); + } + + get services(): RPCDescriptor[] { + return this.fileDescriptor.getServiceList() + .map(service => { + return new RPCDescriptor(this, service, this.exportMap); + }); + } +} diff --git a/src/ts-protoc-gen/service/grpcnode.ts b/src/ts-protoc-gen/service/grpcnode.ts new file mode 100644 index 0000000..07653ce --- /dev/null +++ b/src/ts-protoc-gen/service/grpcnode.ts @@ -0,0 +1,127 @@ +import {ExportMap} from "../ExportMap"; +import {Printer} from "../Printer"; +import {FileDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {CodeGeneratorResponse} from "google-protobuf/google/protobuf/compiler/plugin_pb"; +import {createFile, RPCDescriptor, GrpcServiceDescriptor, RPCMethodDescriptor} from "./common"; +import { ModeParameter } from "../parameters"; + +export function generateGrpcNodeService(filename: string, descriptor: FileDescriptorProto, exportMap: ExportMap, modeParameter: ModeParameter): CodeGeneratorResponse.File { + const definitionFilename = filename.replace(/_pb$/, "_grpc_pb.d.ts"); + return createFile(generateTypeScriptDefinition(descriptor, exportMap, modeParameter), definitionFilename); +} + +function generateTypeScriptDefinition(fileDescriptor: FileDescriptorProto, exportMap: ExportMap, modeParameter: ModeParameter): string { + const serviceDescriptor = new GrpcServiceDescriptor(fileDescriptor, exportMap); + const printer = new Printer(0); + + const hasServices = serviceDescriptor.services.length > 0; + + // Header. + if (hasServices) { + printer.printLn("// GENERATED CODE -- DO NOT EDIT!"); + printer.printEmptyLn(); + } else { + printer.printLn("// GENERATED CODE -- NO SERVICES IN PROTO"); + return printer.getOutput(); + } + + printer.printLn(`// package: ${serviceDescriptor.packageName}`); + printer.printLn(`// file: ${serviceDescriptor.filename}`); + printer.printEmptyLn(); + + // Import statements. + serviceDescriptor.imports + .forEach(importDescriptor => { + printer.printLn(`import * as ${importDescriptor.namespace} from "${importDescriptor.path}";`); + }); + const importPackage = modeParameter === ModeParameter.GrpcJs ? "@grpc/grpc-js" : "grpc"; + printer.printLn(`import * as grpc from "${importPackage}";`); + + // Services. + serviceDescriptor.services + .forEach(service => { + printer.printEmptyLn(); + printService(printer, service); + printer.printEmptyLn(); + printClient(printer, service); + }); + + return printer.getOutput(); +} + +function printService(printer: Printer, service: RPCDescriptor) { + const serviceName = `${service.name}Service`; + printer.printLn(`interface I${serviceName} extends grpc.ServiceDefinition {`); + service.methods + .forEach(method => { + const methodType = `grpc.MethodDefinition<${method.requestType}, ${method.responseType}>`; + printer.printIndentedLn(`${method.nameAsCamelCase}: ${methodType};`); + }); + printer.printLn("}"); + printer.printEmptyLn(); + printer.printLn(`export const ${serviceName}: I${serviceName};`); +} + +function printClient(printer: Printer, service: RPCDescriptor) { + printer.printLn(`export class ${service.name}Client extends grpc.Client {`); + printer.printIndentedLn("constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);"); + service.methods + .forEach(method => { + if (!method.requestStream && !method.responseStream) { + printUnaryRequestMethod(printer, method); + } else if (!method.requestStream) { + printServerStreamRequestMethod(printer, method); + } else if (!method.responseStream) { + printClientStreamRequestMethod(printer, method); + } else { + printBidiStreamRequest(printer, method); + } + }); + printer.printLn("}"); +} + +const metadata = "metadata: grpc.Metadata | null"; +const options = "options: grpc.CallOptions | null"; +const metadataOrOptions = "metadataOrOptions: grpc.Metadata | grpc.CallOptions | null"; + +const optionalMetadata = "metadata?: grpc.Metadata | null"; +const optionalOptions = "options?: grpc.CallOptions | null"; +const optionalMetadataOrOptions = "metadataOrOptions?: grpc.Metadata | grpc.CallOptions | null"; + +function printUnaryRequestMethod(printer: Printer, method: RPCMethodDescriptor) { + const name = method.nameAsCamelCase; + const argument = `argument: ${method.requestType}`; + const callback = `callback: grpc.requestCallback<${method.responseType}>`; + const returnType = "grpc.ClientUnaryCall"; + + printer.printIndentedLn(`${name}(${argument}, ${callback}): ${returnType};`); + printer.printIndentedLn(`${name}(${argument}, ${metadataOrOptions}, ${callback}): ${returnType};`); + printer.printIndentedLn(`${name}(${argument}, ${metadata}, ${options}, ${callback}): ${returnType};`); +} + +function printServerStreamRequestMethod(printer: Printer, method: RPCMethodDescriptor) { + const name = method.nameAsCamelCase; + const argument = `argument: ${method.requestType}`; + const returnType = `grpc.ClientReadableStream<${method.responseType}>`; + + printer.printIndentedLn(`${name}(${argument}, ${optionalMetadataOrOptions}): ${returnType};`); + printer.printIndentedLn(`${name}(${argument}, ${optionalMetadata}, ${optionalOptions}): ${returnType};`); +} + +function printClientStreamRequestMethod(printer: Printer, method: RPCMethodDescriptor) { + const name = method.nameAsCamelCase; + const callback = `callback: grpc.requestCallback<${method.responseType}>`; + const returnType = `grpc.ClientWritableStream<${method.requestType}>`; + + printer.printIndentedLn(`${name}(${callback}): grpc.ClientWritableStream<${method.requestType}>;`); + printer.printIndentedLn(`${name}(${metadataOrOptions}, ${callback}): ${returnType};`); + printer.printIndentedLn(`${name}(${metadata}, ${options}, ${callback}): ${returnType};`); +} + +function printBidiStreamRequest(printer: Printer, method: RPCMethodDescriptor) { + const name = method.nameAsCamelCase; + const returnType = `grpc.ClientDuplexStream<${method.requestType}, ${method.responseType}>`; + + printer.printIndentedLn(`${name}(${optionalMetadataOrOptions}): ${returnType};`); + printer.printIndentedLn(`${name}(${optionalMetadata}, ${optionalOptions}): ${returnType};`); +} diff --git a/src/ts-protoc-gen/service/grpcweb.ts b/src/ts-protoc-gen/service/grpcweb.ts new file mode 100644 index 0000000..1a9d8d5 --- /dev/null +++ b/src/ts-protoc-gen/service/grpcweb.ts @@ -0,0 +1,397 @@ +import {ExportMap} from "../ExportMap"; +import {Printer} from "../Printer"; +import {CodePrinter} from "../CodePrinter"; +import {FileDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {CodeGeneratorResponse} from "google-protobuf/google/protobuf/compiler/plugin_pb"; +import {createFile, RPCMethodDescriptor, RPCDescriptor, GrpcServiceDescriptor} from "./common"; + +export function generateGrpcWebService(filename: string, descriptor: FileDescriptorProto, exportMap: ExportMap): CodeGeneratorResponse.File[] { + return [ + createFile(generateTypeScriptDefinition(descriptor, exportMap), `${filename}_service.d.ts`), + createFile(generateJavaScript(descriptor, exportMap), `${filename}_service.js`), + ]; +} + +function generateTypeScriptDefinition(fileDescriptor: FileDescriptorProto, exportMap: ExportMap): string { + const serviceDescriptor = new GrpcServiceDescriptor(fileDescriptor, exportMap); + const printer = new Printer(0); + + // Header. + printer.printLn(`// package: ${serviceDescriptor.packageName}`); + printer.printLn(`// file: ${serviceDescriptor.filename}`); + printer.printEmptyLn(); + + if (serviceDescriptor.services.length === 0) { + return printer.getOutput(); + } + + // Import statements. + serviceDescriptor.imports + .forEach(importDescriptor => { + printer.printLn(`import * as ${importDescriptor.namespace} from "${importDescriptor.path}";`); + }); + printer.printLn(`import {grpc} from "@improbable-eng/grpc-web";`); + printer.printEmptyLn(); + + // Services. + serviceDescriptor.services + .forEach(service => { + + // Method Type Definitions + service.methods.forEach(method => { + printer.printLn(`type ${method.serviceName}${method.nameAsPascalCase} = {`); + printer.printIndentedLn(`readonly methodName: string;`); + printer.printIndentedLn(`readonly service: typeof ${method.serviceName};`); + printer.printIndentedLn(`readonly requestStream: ${method.requestStream};`); + printer.printIndentedLn(`readonly responseStream: ${method.responseStream};`); + printer.printIndentedLn(`readonly requestType: typeof ${method.requestType};`); + printer.printIndentedLn(`readonly responseType: typeof ${method.responseType};`); + printer.printLn(`};`); + printer.printEmptyLn(); + }); + + printer.printLn(`export class ${service.name} {`); + printer.printIndentedLn(`static readonly serviceName: string;`); + service.methods.forEach(method => { + printer.printIndentedLn(`static readonly ${method.nameAsPascalCase}: ${method.serviceName}${method.nameAsPascalCase};`); + }); + printer.printLn(`}`); + printer.printEmptyLn(); + }); + + + + printer.printLn(`export type ServiceError = { message: string, code: number; metadata: grpc.Metadata }`); + printer.printLn(`export type Status = { details: string, code: number; metadata: grpc.Metadata }`); + printer.printEmptyLn(); + printer.printLn("interface UnaryResponse {"); + printer.printIndentedLn("cancel(): void;"); + printer.printLn("}"); + printer.printLn(`interface ResponseStream {`); + printer.printIndentedLn(`cancel(): void;`); + printer.printIndentedLn(`on(type: 'data', handler: (message: T) => void): ResponseStream;`); + printer.printIndentedLn(`on(type: 'end', handler: (status?: Status) => void): ResponseStream;`); + printer.printIndentedLn(`on(type: 'status', handler: (status: Status) => void): ResponseStream;`); + printer.printLn(`}`); + printer.printLn(`interface RequestStream {`); + printer.printIndentedLn(`write(message: T): RequestStream;`); + printer.printIndentedLn(`end(): void;`); + printer.printIndentedLn(`cancel(): void;`); + printer.printIndentedLn(`on(type: 'end', handler: (status?: Status) => void): RequestStream;`); + printer.printIndentedLn(`on(type: 'status', handler: (status: Status) => void): RequestStream;`); + printer.printLn(`}`); + printer.printLn(`interface BidirectionalStream {`); + printer.printIndentedLn(`write(message: ReqT): BidirectionalStream;`); + printer.printIndentedLn(`end(): void;`); + printer.printIndentedLn(`cancel(): void;`); + printer.printIndentedLn(`on(type: 'data', handler: (message: ResT) => void): BidirectionalStream;`); + printer.printIndentedLn(`on(type: 'end', handler: (status?: Status) => void): BidirectionalStream;`); + printer.printIndentedLn(`on(type: 'status', handler: (status: Status) => void): BidirectionalStream;`); + printer.printLn(`}`); + printer.printEmptyLn(); + + // Add a client stub that talks with the @improbable-eng/grpc-web library + serviceDescriptor.services + .forEach(service => { + printServiceStubTypes(printer, service); + printer.printEmptyLn(); + }); + + return printer.getOutput(); +} + +function generateJavaScript(fileDescriptor: FileDescriptorProto, exportMap: ExportMap): string { + const serviceDescriptor = new GrpcServiceDescriptor(fileDescriptor, exportMap); + const printer = new Printer(0); + + // Header. + printer.printLn(`// package: ${serviceDescriptor.packageName}`); + printer.printLn(`// file: ${serviceDescriptor.filename}`); + printer.printEmptyLn(); + + if (serviceDescriptor.services.length === 0) { + return printer.getOutput(); + } + + // Import Statements + serviceDescriptor.imports + .forEach(importDescriptor => { + printer.printLn(`var ${importDescriptor.namespace} = require("${importDescriptor.path}");`); + }); + printer.printLn(`var grpc = require("@improbable-eng/grpc-web").grpc;`); + printer.printEmptyLn(); + + // Services. + serviceDescriptor.services + .forEach(service => { + printer.printLn(`var ${service.name} = (function () {`); + printer.printIndentedLn(`function ${service.name}() {}`); + printer.printIndentedLn(`${service.name}.serviceName = "${service.qualifiedName}";`); + printer.printIndentedLn(`return ${service.name};`); + printer.printLn(`}());`); + printer.printEmptyLn(); + + service.methods + .forEach(method => { + printer.printLn(`${method.serviceName}.${method.nameAsPascalCase} = {`); + printer.printIndentedLn(`methodName: "${method.nameAsPascalCase}",`); + printer.printIndentedLn(`service: ${method.serviceName},`); + printer.printIndentedLn(`requestStream: ${method.requestStream},`); + printer.printIndentedLn(`responseStream: ${method.responseStream},`); + printer.printIndentedLn(`requestType: ${method.requestType},`); + printer.printIndentedLn(`responseType: ${method.responseType}`); + printer.printLn(`};`); + printer.printEmptyLn(); + }); + printer.printLn(`exports.${service.name} = ${service.name};`); + printer.printEmptyLn(); + + // Add a client stub that talks with the @improbable-eng/grpc-web library + printServiceStub(printer, service); + + printer.printEmptyLn(); + }); + + return printer.getOutput(); +} + +function printServiceStub(methodPrinter: Printer, service: RPCDescriptor) { + const printer = new CodePrinter(0, methodPrinter); + + printer + .printLn(`function ${service.name}Client(serviceHost, options) {`) + .indent().printLn(`this.serviceHost = serviceHost;`) + .printLn(`this.options = options || {};`) + .dedent().printLn(`}`) + .printEmptyLn(); + + service.methods.forEach((method: RPCMethodDescriptor) => { + if (method.requestStream && method.responseStream) { + printBidirectionalStubMethod(printer, method); + } else if (method.requestStream) { + printClientStreamStubMethod(printer, method); + } else if (method.responseStream) { + printServerStreamStubMethod(printer, method); + } else { + printUnaryStubMethod(printer, method); + } + printer.printEmptyLn(); + }); + printer.printLn(`exports.${service.name}Client = ${service.name}Client;`); +} + +function printUnaryStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { + printer + .printLn(`${method.serviceName}Client.prototype.${method.nameAsCamelCase} = function ${method.functionName}(requestMessage, metadata, callback) {`) + .indent().printLn(`if (arguments.length === 2) {`) + .indent().printLn(`callback = arguments[1];`) + .dedent().printLn("}") + .printLn(`var client = grpc.unary(${method.serviceName}.${method.nameAsPascalCase}, {`) + .indent().printLn(`request: requestMessage,`) + .printLn(`host: this.serviceHost,`) + .printLn(`metadata: metadata,`) + .printLn(`transport: this.options.transport,`) + .printLn(`debug: this.options.debug,`) + .printLn(`onEnd: function (response) {`) + .indent().printLn(`if (callback) {`) + .indent().printLn(`if (response.status !== grpc.Code.OK) {`) + .indent().printLn(`var err = new Error(response.statusMessage);`) + .printLn(`err.code = response.status;`) + .printLn(`err.metadata = response.trailers;`) + .printLn(`callback(err, null);`) + .dedent().printLn(`} else {`) + .indent().printLn(`callback(null, response.message);`) + .dedent().printLn(`}`) + .dedent().printLn(`}`) + .dedent().printLn(`}`) + .dedent().printLn(`});`) + .printLn(`return {`) + .indent().printLn(`cancel: function () {`) + .indent().printLn(`callback = null;`) + .printLn(`client.close();`) + .dedent().printLn(`}`) + .dedent().printLn(`};`) + .dedent().printLn(`};`); +} + +function printServerStreamStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { + printer + .printLn(`${method.serviceName}Client.prototype.${method.nameAsCamelCase} = function ${method.functionName}(requestMessage, metadata) {`) + .indent().printLn(`var listeners = {`) + .indent().printLn(`data: [],`) + .printLn(`end: [],`) + .printLn(`status: []`) + .dedent().printLn(`};`) + .printLn(`var client = grpc.invoke(${method.serviceName}.${method.nameAsPascalCase}, {`) + .indent().printLn(`request: requestMessage,`) + .printLn(`host: this.serviceHost,`) + .printLn(`metadata: metadata,`) + .printLn(`transport: this.options.transport,`) + .printLn(`debug: this.options.debug,`) + .printLn(`onMessage: function (responseMessage) {`) + .indent().printLn(`listeners.data.forEach(function (handler) {`) + .indent().printLn(`handler(responseMessage);`) + .dedent().printLn(`});`) + .dedent().printLn(`},`) + .printLn(`onEnd: function (status, statusMessage, trailers) {`) + .indent().printLn(`listeners.status.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners.end.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners = null;`) + .dedent().printLn(`}`) + .dedent().printLn(`});`) + .printLn(`return {`) + .indent().printLn(`on: function (type, handler) {`) + .indent().printLn(`listeners[type].push(handler);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`cancel: function () {`) + .indent().printLn(`listeners = null;`) + .printLn(`client.close();`) + .dedent().printLn(`}`) + .dedent().printLn(`};`) + .dedent().printLn(`};`); +} + +function printClientStreamStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { + printer + .printLn(`${method.serviceName}Client.prototype.${method.nameAsCamelCase} = function ${method.functionName}(metadata) {`) + .indent().printLn(`var listeners = {`) + .indent().printLn(`end: [],`) + .printLn(`status: []`) + .dedent().printLn(`};`) + .printLn(`var client = grpc.client(${method.serviceName}.${method.nameAsPascalCase}, {`) + .indent().printLn(`host: this.serviceHost,`) + .printLn(`metadata: metadata,`) + .printLn(`transport: this.options.transport`) + .dedent().printLn(`});`) + .printLn(`client.onEnd(function (status, statusMessage, trailers) {`) + .indent().printLn(`listeners.status.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners.end.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners = null;`) + .dedent().printLn(`});`) + .printLn(`return {`) + .indent().printLn(`on: function (type, handler) {`) + .indent().printLn(`listeners[type].push(handler);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`write: function (requestMessage) {`) + .indent().printLn(`if (!client.started) {`) + .indent().printLn(`client.start(metadata);`) + .dedent().printLn(`}`) + .printLn(`client.send(requestMessage);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`end: function () {`) + .indent().printLn(`client.finishSend();`) + .dedent().printLn(`},`) + .printLn(`cancel: function () {`) + .indent().printLn(`listeners = null;`) + .printLn(`client.close();`) + .dedent().printLn(`}`) + .dedent().printLn(`};`) + .dedent().printLn(`};`); +} + +function printBidirectionalStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { + printer + .printLn(`${method.serviceName}Client.prototype.${method.nameAsCamelCase} = function ${method.functionName}(metadata) {`) + .indent().printLn(`var listeners = {`) + .indent().printLn(`data: [],`) + .printLn(`end: [],`) + .printLn(`status: []`) + .dedent().printLn(`};`) + .printLn(`var client = grpc.client(${method.serviceName}.${method.nameAsPascalCase}, {`) + .indent().printLn(`host: this.serviceHost,`) + .printLn(`metadata: metadata,`) + .printLn(`transport: this.options.transport`) + .dedent().printLn(`});`) + .printLn(`client.onEnd(function (status, statusMessage, trailers) {`) + .indent().printLn(`listeners.status.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners.end.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners = null;`) + .dedent().printLn(`});`) + .printLn(`client.onMessage(function (message) {`) + .indent().printLn(`listeners.data.forEach(function (handler) {`) + .indent().printLn(`handler(message);`) + .dedent().printLn(`})`) + .dedent().printLn(`});`) + .printLn(`client.start(metadata);`) + .printLn(`return {`) + .indent().printLn(`on: function (type, handler) {`) + .indent().printLn(`listeners[type].push(handler);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`write: function (requestMessage) {`) + .indent().printLn(`client.send(requestMessage);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`end: function () {`) + .indent().printLn(`client.finishSend();`) + .dedent().printLn(`},`) + .printLn(`cancel: function () {`) + .indent().printLn(`listeners = null;`) + .printLn(`client.close();`) + .dedent().printLn(`}`) + .dedent().printLn(`};`) + .dedent().printLn(`};`); +} + +function printServiceStubTypes(methodPrinter: Printer, service: RPCDescriptor) { + const printer = new CodePrinter(0, methodPrinter); + + printer + .printLn(`export class ${service.name}Client {`) + .indent().printLn(`readonly serviceHost: string;`) + .printEmptyLn() + .printLn(`constructor(serviceHost: string, options?: grpc.RpcOptions);`); + + service.methods.forEach((method: RPCMethodDescriptor) => { + if (method.requestStream && method.responseStream) { + printBidirectionalStubMethodTypes(printer, method); + } else if (method.requestStream) { + printClientStreamStubMethodTypes(printer, method); + } else if (method.responseStream) { + printServerStreamStubMethodTypes(printer, method); + } else { + printUnaryStubMethodTypes(printer, method); + } + }); + printer.dedent().printLn("}"); +} + +function printUnaryStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { + printer + .printLn(`${method.nameAsCamelCase}(`) + .indent().printLn(`requestMessage: ${method.requestType},`) + .printLn(`metadata: grpc.Metadata,`) + .printLn(`callback: (error: ServiceError|null, responseMessage: ${method.responseType}|null) => void`) + .dedent().printLn(`): UnaryResponse;`) + .printLn(`${method.nameAsCamelCase}(`) + .indent().printLn(`requestMessage: ${method.requestType},`) + .printLn(`callback: (error: ServiceError|null, responseMessage: ${method.responseType}|null) => void`) + .dedent().printLn(`): UnaryResponse;`); +} + +function printServerStreamStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { + printer.printLn(`${method.nameAsCamelCase}(requestMessage: ${method.requestType}, metadata?: grpc.Metadata): ResponseStream<${method.responseType}>;`); +} + +function printClientStreamStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { + printer.printLn(`${method.nameAsCamelCase}(metadata?: grpc.Metadata): RequestStream<${method.requestType}>;`); +} + +function printBidirectionalStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { + printer.printLn(`${method.nameAsCamelCase}(metadata?: grpc.Metadata): BidirectionalStream<${method.requestType}, ${method.responseType}>;`); +} diff --git a/src/ts-protoc-gen/ts/FieldTypes.ts b/src/ts-protoc-gen/ts/FieldTypes.ts new file mode 100644 index 0000000..7ccd739 --- /dev/null +++ b/src/ts-protoc-gen/ts/FieldTypes.ts @@ -0,0 +1,59 @@ +import {filePathToPseudoNamespace, withinNamespaceFromExportEntry} from "../util"; +import {ExportMap} from "../ExportMap"; +import {FieldDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; + +export const MESSAGE_TYPE = 11; +export const BYTES_TYPE = 12; +export const ENUM_TYPE = 14; + +const TypeNumToTypeString: {[key: number]: string} = {}; +TypeNumToTypeString[1] = "number"; // TYPE_DOUBLE +TypeNumToTypeString[2] = "number"; // TYPE_FLOAT +TypeNumToTypeString[3] = "number"; // TYPE_INT64 +TypeNumToTypeString[4] = "number"; // TYPE_UINT64 +TypeNumToTypeString[5] = "number"; // TYPE_INT32 +TypeNumToTypeString[6] = "number"; // TYPE_FIXED64 +TypeNumToTypeString[7] = "number"; // TYPE_FIXED32 +TypeNumToTypeString[8] = "boolean"; // TYPE_BOOL +TypeNumToTypeString[9] = "string"; // TYPE_STRING +TypeNumToTypeString[10] = "Object"; // TYPE_GROUP +TypeNumToTypeString[MESSAGE_TYPE] = "Object"; // TYPE_MESSAGE - Length-delimited aggregate. +TypeNumToTypeString[BYTES_TYPE] = "Uint8Array"; // TYPE_BYTES +TypeNumToTypeString[13] = "number"; // TYPE_UINT32 +TypeNumToTypeString[ENUM_TYPE] = "number"; // TYPE_ENUM +TypeNumToTypeString[15] = "number"; // TYPE_SFIXED32 +TypeNumToTypeString[16] = "number"; // TYPE_SFIXED64 +TypeNumToTypeString[17] = "number"; // TYPE_SINT32 - Uses ZigZag encoding. +TypeNumToTypeString[18] = "number"; // TYPE_SINT64 - Uses ZigZag encoding. + +export function getTypeName(fieldTypeNum: number): string { + return TypeNumToTypeString[fieldTypeNum]; +} + +export function getFieldType(type: FieldDescriptorProto.Type, typeName: string, currentFileName: string, exportMap: ExportMap): string { + if (type === MESSAGE_TYPE) { + const fromExport = exportMap.getMessage(typeName); + if (!fromExport) { + throw new Error("Could not getFieldType for message: " + typeName); + } + const withinNamespace = withinNamespaceFromExportEntry(typeName, fromExport); + if (fromExport.fileName === currentFileName) { + return withinNamespace; + } else { + return filePathToPseudoNamespace(fromExport.fileName) + "." + withinNamespace; + } + } else if (type === ENUM_TYPE) { + const fromExport = exportMap.getEnum(typeName); + if (!fromExport) { + throw new Error("Could not getFieldType for enum: " + typeName); + } + const withinNamespace = withinNamespaceFromExportEntry(typeName, fromExport); + if (fromExport.fileName === currentFileName) { + return `${withinNamespace}Map`; + } else { + return filePathToPseudoNamespace(fromExport.fileName) + "." + withinNamespace; + } + } else { + return TypeNumToTypeString[type]; + } +} diff --git a/src/ts-protoc-gen/ts/enum.ts b/src/ts-protoc-gen/ts/enum.ts new file mode 100644 index 0000000..97b6fd1 --- /dev/null +++ b/src/ts-protoc-gen/ts/enum.ts @@ -0,0 +1,16 @@ +import {EnumDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {Printer} from "../Printer"; + +export function printEnum(enumDescriptor: EnumDescriptorProto, indentLevel: number) { + const printer = new Printer(indentLevel); + const enumInterfaceName = `${enumDescriptor.getName()}Map`; + printer.printEmptyLn(); + printer.printLn(`export interface ${enumInterfaceName} {`); + enumDescriptor.getValueList().forEach(value => { + printer.printIndentedLn(`${value.getName().toUpperCase()}: ${value.getNumber()};`); + }); + printer.printLn(`}`); + printer.printEmptyLn(); + printer.printLn(`export const ${enumDescriptor.getName()}: ${enumInterfaceName};`); + return printer.getOutput(); +} diff --git a/src/ts-protoc-gen/ts/extensions.ts b/src/ts-protoc-gen/ts/extensions.ts new file mode 100644 index 0000000..fbe2bb2 --- /dev/null +++ b/src/ts-protoc-gen/ts/extensions.ts @@ -0,0 +1,14 @@ +import {Printer} from "../Printer"; +import {ExportMap} from "../ExportMap"; +import {FieldDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {snakeToCamel} from "../util"; +import {getFieldType} from "./FieldTypes"; + +export function printExtension(fileName: string, exportMap: ExportMap, extension: FieldDescriptorProto, indentLevel: number): string { + const printer = new Printer(indentLevel + 1); + printer.printEmptyLn(); + const extensionName = snakeToCamel(extension.getName()); + const fieldType = getFieldType(extension.getType(), extension.getTypeName().slice(1), fileName, exportMap); + printer.printLn(`export const ${extensionName}: jspb.ExtensionFieldInfo<${fieldType}>;`); + return printer.output; +} diff --git a/src/ts-protoc-gen/ts/fileDescriptorTSD.ts b/src/ts-protoc-gen/ts/fileDescriptorTSD.ts new file mode 100644 index 0000000..b824a7e --- /dev/null +++ b/src/ts-protoc-gen/ts/fileDescriptorTSD.ts @@ -0,0 +1,49 @@ +import {filePathToPseudoNamespace, replaceProtoSuffix, getPathToRoot} from "../util"; +import {ExportMap} from "../ExportMap"; +import {Printer} from "../Printer"; +import {FileDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {WellKnownTypesMap} from "../WellKnown"; +import {printMessage} from "./message"; +import {printEnum} from "./enum"; +import {printExtension} from "./extensions"; + +export function printFileDescriptorTSD(fileDescriptor: FileDescriptorProto, exportMap: ExportMap) { + const fileName = fileDescriptor.getName(); + const packageName = fileDescriptor.getPackage(); + + const printer = new Printer(0); + + printer.printLn(`// package: ${packageName}`); + printer.printLn(`// file: ${fileDescriptor.getName()}`); + + const upToRoot = getPathToRoot(fileName); + + printer.printEmptyLn(); + printer.printLn(`import * as jspb from "google-protobuf";`); + + fileDescriptor.getDependencyList().forEach((dependency: string) => { + const pseudoNamespace = filePathToPseudoNamespace(dependency); + if (dependency in WellKnownTypesMap) { + printer.printLn(`import * as ${pseudoNamespace} from "${WellKnownTypesMap[dependency]}";`); + } else { + const filePath = replaceProtoSuffix(dependency); + printer.printLn(`import * as ${pseudoNamespace} from "${upToRoot}${filePath}";`); + } + }); + + fileDescriptor.getMessageTypeList().forEach(enumType => { + printer.print(printMessage(fileName, exportMap, enumType, 0, fileDescriptor)); + }); + + fileDescriptor.getExtensionList().forEach(extension => { + printer.print(printExtension(fileName, exportMap, extension, 0)); + }); + + fileDescriptor.getEnumTypeList().forEach(enumType => { + printer.print(printEnum(enumType, 0)); + }); + + printer.printEmptyLn(); + + return printer.getOutput(); +} diff --git a/src/ts-protoc-gen/ts/message.ts b/src/ts-protoc-gen/ts/message.ts new file mode 100644 index 0000000..d4fa11a --- /dev/null +++ b/src/ts-protoc-gen/ts/message.ts @@ -0,0 +1,235 @@ +import { + filePathToPseudoNamespace, snakeToCamel, uppercaseFirst, oneOfName, isProto2, + withinNamespaceFromExportEntry, normaliseFieldObjectName, stripPrefix +} from "../util"; +import {ExportMap} from "../ExportMap"; +import { + FieldDescriptorProto, FileDescriptorProto, DescriptorProto, + FieldOptions +} from "google-protobuf/google/protobuf/descriptor_pb"; +import {MESSAGE_TYPE, BYTES_TYPE, ENUM_TYPE, getFieldType, getTypeName} from "./FieldTypes"; +import {Printer} from "../Printer"; +import {printEnum} from "./enum"; +import {printOneOfDecl} from "./oneof"; +import {printExtension} from "./extensions"; +import JSType = FieldOptions.JSType; + +function hasFieldPresence(field: FieldDescriptorProto, fileDescriptor: FileDescriptorProto): boolean { + if (field.getLabel() === FieldDescriptorProto.Label.LABEL_REPEATED) { + return false; + } + + if (field.hasOneofIndex()) { + return true; + } + + if (field.getType() === MESSAGE_TYPE) { + return true; + } + + if (isProto2(fileDescriptor)) { + return true; + } + + return false; +} + +export function printMessage(fileName: string, exportMap: ExportMap, messageDescriptor: DescriptorProto, indentLevel: number, fileDescriptor: FileDescriptorProto) { + const messageName = messageDescriptor.getName(); + const messageOptions = messageDescriptor.getOptions(); + if (messageOptions !== undefined && messageOptions.getMapEntry()) { + // this message type is the entry tuple for a map - don't output it + return ""; + } + + const objectTypeName = `AsObject`; + const toObjectType = new Printer(indentLevel + 1); + toObjectType.printLn(`export type ${objectTypeName} = {`); + + const printer = new Printer(indentLevel); + printer.printEmptyLn(); + printer.printLn(`export class ${messageName} extends jspb.Message {`); + + const oneOfGroups: Array> = []; + + messageDescriptor.getFieldList().forEach(field => { + if (field.hasOneofIndex()) { + const oneOfIndex = field.getOneofIndex(); + let existing = oneOfGroups[oneOfIndex]; + if (existing === undefined) { + existing = []; + oneOfGroups[oneOfIndex] = existing; + } + existing.push(field); + } + const snakeCaseName = stripPrefix(field.getName().toLowerCase(), "_"); + const camelCaseName = snakeToCamel(snakeCaseName); + const withUppercase = uppercaseFirst(camelCaseName); + const type = field.getType(); + + let exportType; + const fullTypeName = field.getTypeName().slice(1); + if (type === MESSAGE_TYPE) { + const fieldMessageType = exportMap.getMessage(fullTypeName); + if (fieldMessageType === undefined) { + throw new Error("No message export for: " + fullTypeName); + } + if (fieldMessageType.messageOptions !== undefined && fieldMessageType.messageOptions.getMapEntry()) { + // This field is a map + const keyTuple = fieldMessageType.mapFieldOptions!.key; + const keyType = keyTuple[0]; + const keyTypeName = getFieldType(keyType, keyTuple[1], fileName, exportMap); + const valueTuple = fieldMessageType.mapFieldOptions!.value; + const valueType = valueTuple[0]; + let valueTypeName = getFieldType(valueType, valueTuple[1], fileName, exportMap); + if (valueType === BYTES_TYPE) { + valueTypeName = "Uint8Array | string"; + } + if (valueType === ENUM_TYPE) { + valueTypeName = `${valueTypeName}[keyof ${valueTypeName}]`; + } + printer.printIndentedLn(`get${withUppercase}Map(): jspb.Map<${keyTypeName}, ${valueTypeName}>;`); + printer.printIndentedLn(`clear${withUppercase}Map(): void;`); + toObjectType.printIndentedLn(`${camelCaseName}Map: Array<[${keyTypeName}${keyType === MESSAGE_TYPE ? ".AsObject" : ""}, ${valueTypeName}${valueType === MESSAGE_TYPE ? ".AsObject" : ""}]>,`); + return; + } + const withinNamespace = withinNamespaceFromExportEntry(fullTypeName, fieldMessageType); + if (fieldMessageType.fileName === fileName) { + exportType = withinNamespace; + } else { + exportType = filePathToPseudoNamespace(fieldMessageType.fileName) + "." + withinNamespace; + } + } else if (type === ENUM_TYPE) { + const fieldEnumType = exportMap.getEnum(fullTypeName); + if (fieldEnumType === undefined) { + throw new Error("No enum export for: " + fullTypeName); + } + const withinNamespace = withinNamespaceFromExportEntry(fullTypeName, fieldEnumType); + if (fieldEnumType.fileName === fileName) { + exportType = withinNamespace; + } else { + exportType = filePathToPseudoNamespace(fieldEnumType.fileName) + "." + withinNamespace; + } + exportType = `${exportType}Map[keyof ${exportType}Map]`; + } else { + if (field.getOptions() && field.getOptions().hasJstype()) { + switch (field.getOptions().getJstype()) { + case JSType.JS_NUMBER: + exportType = "number"; + break; + case JSType.JS_STRING: + exportType = "string"; + break; + default: + exportType = getTypeName(type); + } + } else { + exportType = getTypeName(type); + } + } + + let hasClearMethod = false; + function printClearIfNotPresent() { + if (!hasClearMethod) { + hasClearMethod = true; + printer.printIndentedLn(`clear${withUppercase}${field.getLabel() === FieldDescriptorProto.Label.LABEL_REPEATED ? "List" : ""}(): void;`); + } + } + + if (hasFieldPresence(field, fileDescriptor)) { + printer.printIndentedLn(`has${withUppercase}(): boolean;`); + printClearIfNotPresent(); + } + + function printRepeatedAddMethod(valueType: string) { + const optionalValue = field.getType() === MESSAGE_TYPE; + printer.printIndentedLn(`add${withUppercase}(value${optionalValue ? "?" : ""}: ${valueType}, index?: number): ${valueType};`); + } + + if (field.getLabel() === FieldDescriptorProto.Label.LABEL_REPEATED) {// is repeated + printClearIfNotPresent(); + if (type === BYTES_TYPE) { + toObjectType.printIndentedLn(`${camelCaseName}List: Array,`); + printer.printIndentedLn(`get${withUppercase}List(): Array;`); + printer.printIndentedLn(`get${withUppercase}List_asU8(): Array;`); + printer.printIndentedLn(`get${withUppercase}List_asB64(): Array;`); + printer.printIndentedLn(`set${withUppercase}List(value: Array): void;`); + printRepeatedAddMethod("Uint8Array | string"); + } else { + toObjectType.printIndentedLn(`${camelCaseName}List: Array<${exportType}${type === MESSAGE_TYPE ? ".AsObject" : ""}>,`); + printer.printIndentedLn(`get${withUppercase}List(): Array<${exportType}>;`); + printer.printIndentedLn(`set${withUppercase}List(value: Array<${exportType}>): void;`); + printRepeatedAddMethod(exportType); + } + } else { + if (type === BYTES_TYPE) { + toObjectType.printIndentedLn(`${camelCaseName}: Uint8Array | string,`); + printer.printIndentedLn(`get${withUppercase}(): Uint8Array | string;`); + printer.printIndentedLn(`get${withUppercase}_asU8(): Uint8Array;`); + printer.printIndentedLn(`get${withUppercase}_asB64(): string;`); + printer.printIndentedLn(`set${withUppercase}(value: Uint8Array | string): void;`); + } else { + let fieldObjectType = exportType; + let canBeUndefined = false; + if (type === MESSAGE_TYPE) { + fieldObjectType += ".AsObject"; + if (!isProto2(fileDescriptor) || (field.getLabel() === FieldDescriptorProto.Label.LABEL_OPTIONAL)) { + canBeUndefined = true; + } + } else { + if (isProto2(fileDescriptor)) { + canBeUndefined = true; + } + } + const fieldObjectName = normaliseFieldObjectName(camelCaseName); + toObjectType.printIndentedLn(`${fieldObjectName}${canBeUndefined ? "?" : ""}: ${fieldObjectType},`); + printer.printIndentedLn(`get${withUppercase}(): ${exportType}${canBeUndefined ? " | undefined" : ""};`); + printer.printIndentedLn(`set${withUppercase}(value${type === MESSAGE_TYPE ? "?" : ""}: ${exportType}): void;`); + } + } + printer.printEmptyLn(); + }); + + toObjectType.printLn(`}`); + + messageDescriptor.getOneofDeclList().forEach(oneOfDecl => { + printer.printIndentedLn(`get${oneOfName(oneOfDecl.getName())}Case(): ${messageName}.${oneOfName(oneOfDecl.getName())}Case;`); + }); + + printer.printIndentedLn(`serializeBinary(): Uint8Array;`); + printer.printIndentedLn(`toObject(includeInstance?: boolean): ${messageName}.${objectTypeName};`); + printer.printIndentedLn(`static toObject(includeInstance: boolean, msg: ${messageName}): ${messageName}.${objectTypeName};`); + printer.printIndentedLn(`static extensions: {[key: number]: jspb.ExtensionFieldInfo};`); + printer.printIndentedLn(`static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo};`); + printer.printIndentedLn(`static serializeBinaryToWriter(message: ${messageName}, writer: jspb.BinaryWriter): void;`); + printer.printIndentedLn(`static deserializeBinary(bytes: Uint8Array): ${messageName};`); + printer.printIndentedLn(`static deserializeBinaryFromReader(message: ${messageName}, reader: jspb.BinaryReader): ${messageName};`); + + printer.printLn(`}`); + printer.printEmptyLn(); + + printer.printLn(`export namespace ${messageName} {`); + + printer.print(toObjectType.getOutput()); + + messageDescriptor.getNestedTypeList().forEach(nested => { + const msgOutput = printMessage(fileName, exportMap, nested, indentLevel + 1, fileDescriptor); + if (msgOutput !== "") { + // If the message class is a Map entry then it isn't output, so don't print the namespace block + printer.print(msgOutput); + } + }); + messageDescriptor.getEnumTypeList().forEach(enumType => { + printer.print(`${printEnum(enumType, indentLevel + 1)}`); + }); + messageDescriptor.getOneofDeclList().forEach((oneOfDecl, index) => { + printer.print(`${printOneOfDecl(oneOfDecl, oneOfGroups[index] || [], indentLevel + 1)}`); + }); + messageDescriptor.getExtensionList().forEach(extension => { + printer.print(printExtension(fileName, exportMap, extension, indentLevel + 1)); + }); + + printer.printLn(`}`); + + return printer.getOutput(); +} diff --git a/src/ts-protoc-gen/ts/oneof.ts b/src/ts-protoc-gen/ts/oneof.ts new file mode 100644 index 0000000..d9e36d7 --- /dev/null +++ b/src/ts-protoc-gen/ts/oneof.ts @@ -0,0 +1,16 @@ +import {Printer} from "../Printer"; +import {OneofDescriptorProto, FieldDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {oneOfName} from "../util"; + +export function printOneOfDecl(oneOfDecl: OneofDescriptorProto, oneOfFields: Array, indentLevel: number) { + const printer = new Printer(indentLevel); + printer.printEmptyLn(); + printer.printLn(`export enum ${oneOfName(oneOfDecl.getName())}Case {`); + printer.printIndentedLn(`${oneOfDecl.getName().toUpperCase()}_NOT_SET = 0,`); + oneOfFields.forEach(field => { + printer.printIndentedLn(`${field.getName().toUpperCase()} = ${field.getNumber()},`); + }); + printer.printLn("}"); + + return printer.output; +} diff --git a/src/ts-protoc-gen/util.ts b/src/ts-protoc-gen/util.ts new file mode 100644 index 0000000..7b41791 --- /dev/null +++ b/src/ts-protoc-gen/util.ts @@ -0,0 +1,184 @@ +import {parse} from "querystring"; +import {FileDescriptorProto} from "google-protobuf/google/protobuf/descriptor_pb"; +import {ExportEnumEntry, ExportMessageEntry} from "./ExportMap"; +import {ServiceParameter, ModeParameter} from "./parameters"; +export function filePathToPseudoNamespace(filePath: string): string { + return filePath.replace(".proto", "").replace(/\//g, "_").replace(/\./g, "_").replace(/\-/g, "_") + "_pb"; +} + +export function stripPrefix(str: string, prefix: string) { + if (str.substr(0, prefix.length) === prefix) { + return str.substr(prefix.length); + } + return str; +} + +export function snakeToCamel(str: string): string { + return str.replace(/(\_\w)/g, function(m) { + return m[1].toUpperCase(); + }); +} + +export function uppercaseFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +const PROTO2_SYNTAX = "proto2"; +export function isProto2(fileDescriptor: FileDescriptorProto): boolean { + // Empty syntax defaults to proto2 + return (fileDescriptor.getSyntax() === "" || fileDescriptor.getSyntax() === PROTO2_SYNTAX); +} + +export function oneOfName(name: string) { + return uppercaseFirst(snakeToCamel(name.toLowerCase())); +} + +export function generateIndent(indentLevel: number): string { + let indent = ""; + for (let i = 0; i < indentLevel; i++) { + indent += " "; + } + return indent; +} + +export function getPathToRoot(fileName: string) { + const depth = fileName.split("/").length; + return depth === 1 ? "./" : new Array(depth).join("../"); +} + +export function withinNamespaceFromExportEntry(name: string, exportEntry: ExportMessageEntry | ExportEnumEntry) { + return exportEntry.pkg ? name.substring(exportEntry.pkg.length + 1) : name; +} + +export function replaceProtoSuffix(protoFilePath: string): string { + const suffix = ".proto"; + const hasProtoSuffix = protoFilePath.slice(protoFilePath.length - suffix.length) === suffix; + return hasProtoSuffix + ? protoFilePath.slice(0, -suffix.length) + "_pb" + : protoFilePath; +} + +export function withAllStdIn(callback: (buffer: Buffer) => void): void { + const ret: Buffer[] = []; + let len = 0; + + const stdin = process.stdin; + stdin.on("readable", function () { + let chunk; + + while ((chunk = stdin.read())) { + if (!(chunk instanceof Buffer)) throw new Error("Did not receive buffer"); + ret.push(chunk); + len += chunk.length; + } + }); + + stdin.on("end", function () { + callback(Buffer.concat(ret, len)); + }); +} + +// normaliseFieldObjectName modifies the field name that appears in the `asObject` representation +// to match the logic found in `protobuf/compiler/js/js_generator.cc`. See: https://goo.gl/tX1dPQ +export function normaliseFieldObjectName(name: string): string { + switch (name) { + case "abstract": + case "boolean": + case "break": + case "byte": + case "case": + case "catch": + case "char": + case "class": + case "const": + case "continue": + case "debugger": + case "default": + case "delete": + case "do": + case "double": + case "else": + case "enum": + case "export": + case "extends": + case "false": + case "final": + case "finally": + case "float": + case "for": + case "function": + case "goto": + case "if": + case "implements": + case "import": + case "in": + case "instanceof": + case "int": + case "interface": + case "long": + case "native": + case "new": + case "null": + case "package": + case "private": + case "protected": + case "public": + case "return": + case "short": + case "static": + case "super": + case "switch": + case "synchronized": + case "this": + case "throw": + case "throws": + case "transient": + case "try": + case "typeof": + case "var": + case "void": + case "volatile": + case "while": + case "with": + return `pb_${name}`; + } + return name; +} + +export function getServiceParameter(service?: string): ServiceParameter { + switch (service) { + case "true": + console.warn("protoc-gen-ts warning: The service=true parameter has been deprecated. Use service=grpc-web instead."); + return ServiceParameter.GrpcWeb; + case "grpc-web": + return ServiceParameter.GrpcWeb; + case "grpc-node": + return ServiceParameter.GrpcNode; + case undefined: + return ServiceParameter.None; + default: + throw new Error(`Unrecognised service parameter: ${service}`); + } +} + +export function getModeParameter(mode?: string): ModeParameter { + switch (mode) { + case "grpc-js": + return ModeParameter.GrpcJs; + case undefined: + return ModeParameter.None; + default: + throw new Error(`Unrecognised mode parameter: ${mode}`); + } +} + +export function getParameterEnums(parameter: string): { + service: ServiceParameter, + mode: ModeParameter +} { + const {service, mode} = parse(parameter, ","); + return { + service: getServiceParameter(service), + mode: getModeParameter(mode) + }; +}