ts-proto
Version:
> `ts-proto` transforms your `.proto` files into strongly-typed, idiomatic TypeScript files!
919 lines (915 loc) • 49.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.contextTypeVar = exports.visit = exports.generateFile = exports.OneofOption = exports.EnvOption = exports.LongOption = void 0;
const ts_poet_1 = require("ts-poet");
const pbjs_1 = require("../build/pbjs");
const types_1 = require("./types");
const sequency_1 = require("sequency");
const sourceInfo_1 = require("./sourceInfo");
const utils_1 = require("./utils");
const case_1 = require("./case");
const generate_nestjs_1 = require("./generate-nestjs");
const generate_services_1 = require("./generate-services");
var FieldDescriptorProto = pbjs_1.google.protobuf.FieldDescriptorProto;
var FileDescriptorProto = pbjs_1.google.protobuf.FileDescriptorProto;
const generate_grpc_web_1 = require("./generate-grpc-web");
var LongOption;
(function (LongOption) {
LongOption["NUMBER"] = "number";
LongOption["LONG"] = "long";
LongOption["STRING"] = "string";
})(LongOption = exports.LongOption || (exports.LongOption = {}));
var EnvOption;
(function (EnvOption) {
EnvOption["NODE"] = "node";
EnvOption["BROWSER"] = "browser";
EnvOption["BOTH"] = "both";
})(EnvOption = exports.EnvOption || (exports.EnvOption = {}));
var OneofOption;
(function (OneofOption) {
OneofOption["PROPERTIES"] = "properties";
OneofOption["UNIONS"] = "unions";
})(OneofOption = exports.OneofOption || (exports.OneofOption = {}));
function generateFile(typeMap, fileDesc, parameter) {
const options = utils_1.optionsFromParameter(parameter);
// Google's protofiles are organized like Java, where package == the folder the file
// is in, and file == a specific service within the package. I.e. you can have multiple
// company/foo.proto and company/bar.proto files, where package would be 'company'.
//
// We'll match that stucture by setting up the module path as:
//
// company/foo.proto --> company/foo.ts
// company/bar.proto --> company/bar.ts
//
// We'll also assume that the fileDesc.name is already the `company/foo.proto` path, with
// the package already implicitly in it, so we won't re-append/strip/etc. it out/back in.
const moduleName = fileDesc.name.replace('.proto', '.ts');
let file = ts_poet_1.FileSpec.create(moduleName);
const sourceInfo = sourceInfo_1.default.fromDescriptor(fileDesc);
// Syntax, unlike most fields, is not repeated and thus does not use an index
const headerComment = sourceInfo.lookup(sourceInfo_1.Fields.file.syntax, undefined);
utils_1.maybeAddComment(headerComment, (text) => (file = file.addComment(text)));
// first make all the type declarations
visit(fileDesc, sourceInfo, (fullName, message, sInfo) => {
file = file.addInterface(generateInterfaceDeclaration(typeMap, fullName, message, sInfo, options));
}, options, (fullName, enumDesc, sInfo) => {
file = file.addCode(generateEnum(options, fullName, enumDesc, sInfo));
});
// If nestJs=true export [package]_PACKAGE_NAME and [service]_SERVICE_NAME const
if (options.nestJs) {
file = file.addCode(ts_poet_1.CodeBlock.empty().add(`export const %L = '%L'`, `${case_1.camelToSnake(fileDesc.package.replace(/\./g, '_'))}_PACKAGE_NAME`, fileDesc.package));
}
if (options.outputEncodeMethods || options.outputJsonMethods) {
// then add the encoder/decoder/base instance
visit(fileDesc, sourceInfo, (fullName, message) => {
file = file.addProperty(generateBaseInstance(typeMap, fullName, message, options));
let staticMethods = ts_poet_1.CodeBlock.empty().add('export const %L = ', fullName).beginHash();
staticMethods = !options.outputEncodeMethods
? staticMethods
: staticMethods
.addHashEntry(generateEncode(typeMap, fullName, message, options))
.addHashEntry(generateDecode(typeMap, fullName, message, options));
staticMethods = !options.outputJsonMethods
? staticMethods
: staticMethods
.addHashEntry(generateFromJson(typeMap, fullName, message, options))
.addHashEntry(generateFromPartial(typeMap, fullName, message, options))
.addHashEntry(generateToJson(typeMap, fullName, message, options));
staticMethods = staticMethods.endHash().add(';').newLine();
file = file.addCode(staticMethods);
}, options);
}
visitServices(fileDesc, sourceInfo, (serviceDesc, sInfo) => {
if (options.nestJs) {
// NestJS is sufficiently different that we special case all of the client/server interfaces
// generate nestjs grpc client interface
file = file.addInterface(generate_nestjs_1.generateNestjsServiceClient(typeMap, fileDesc, sInfo, serviceDesc, options));
// and the service controller interface
file = file.addInterface(generate_nestjs_1.generateNestjsServiceController(typeMap, fileDesc, sInfo, serviceDesc, options));
// generate nestjs grpc service controller decorator
file = file.addFunction(generate_nestjs_1.generateNestjsGrpcServiceMethodsDecorator(serviceDesc, options));
let serviceConstName = `${case_1.camelToSnake(serviceDesc.name)}_NAME`;
if (!serviceDesc.name.toLowerCase().endsWith('service')) {
serviceConstName = `${case_1.camelToSnake(serviceDesc.name)}_SERVICE_NAME`;
}
file = file.addCode(ts_poet_1.CodeBlock.empty().add(`export const %L = '%L';`, serviceConstName, serviceDesc.name));
}
else {
// This could be twirp or grpc-web or JSON (maybe). So far all of their interaces
// are fairly similar.
file = file.addInterface(generate_services_1.generateService(typeMap, fileDesc, sInfo, serviceDesc, options));
if (options.outputClientImpl === true) {
file = file.addClass(generate_services_1.generateServiceClientImpl(typeMap, fileDesc, serviceDesc, options));
}
else if (options.outputClientImpl === 'grpc-web') {
file = file.addClass(generate_grpc_web_1.generateGrpcClientImpl(typeMap, fileDesc, serviceDesc, options));
file = file.addCode(generate_grpc_web_1.generateGrpcServiceDesc(fileDesc, serviceDesc));
serviceDesc.method.forEach((method) => {
file = file.addCode(generate_grpc_web_1.generateGrpcMethodDesc(options, typeMap, serviceDesc, method));
});
}
}
});
if (options.outputClientImpl && fileDesc.service.length > 0) {
if (options.outputClientImpl === true) {
file = file.addInterface(generate_services_1.generateRpcType(options));
}
else if (options.outputClientImpl === 'grpc-web') {
file = generate_grpc_web_1.addGrpcWebMisc(options, file);
}
}
if (options.useContext) {
file = file.addInterface(generate_services_1.generateDataLoadersType());
}
let hasAnyTimestamps = false;
visit(fileDesc, sourceInfo, (_, messageType) => {
hasAnyTimestamps = hasAnyTimestamps || sequency_1.asSequence(messageType.field).any(types_1.isTimestamp);
}, options);
if (hasAnyTimestamps && (options.outputJsonMethods || options.outputEncodeMethods)) {
file = addTimestampMethods(file, options);
}
const initialOutput = file.toString();
// This `.includes(...)` is a pretty fuzzy way of detecting whether we use these utility
// methods (to prevent outputting them if its not necessary). In theory, we should be able
// to lean on the code generation library more to do this sort of "output only if used",
// similar to what it does for auto-imports.
if (initialOutput.includes('longToNumber') ||
initialOutput.includes('numberToLong') ||
initialOutput.includes('longToString')) {
file = addLongUtilityMethod(file, options);
}
if (initialOutput.includes('bytesFromBase64') || initialOutput.includes('base64FromBytes')) {
file = addBytesUtilityMethods(file);
}
if (initialOutput.includes('DeepPartial')) {
file = addDeepPartialType(file, options);
}
return file;
}
exports.generateFile = generateFile;
function addLongUtilityMethod(_file, options) {
// Regardless of which `forceLong` config option we're using, we always use
// the `long` library to either represent or at least sanity-check 64-bit values
const util = ts_poet_1.TypeNames.anyType('util@protobufjs/minimal');
const configure = ts_poet_1.TypeNames.anyType('configure@protobufjs/minimal');
let file = _file.addCode(ts_poet_1.CodeBlock.empty()
.beginControlFlow('if (%T.Long !== %T as any)', util, 'Long*long')
.addStatement('%T.Long = %T as any', util, 'Long*long')
.addStatement('%T()', configure)
.endControlFlow());
if (options.forceLong === LongOption.LONG) {
return file.addFunction(ts_poet_1.FunctionSpec.create('numberToLong')
.addParameter('number', 'number')
.addCodeBlock(ts_poet_1.CodeBlock.empty().addStatement('return %T.fromNumber(number)', 'Long*long')));
}
else if (options.forceLong === LongOption.STRING) {
return file.addFunction(ts_poet_1.FunctionSpec.create('longToString')
.addParameter('long', 'Long*long')
.addCodeBlock(ts_poet_1.CodeBlock.empty().addStatement('return long.toString()')));
}
else {
return file.addFunction(ts_poet_1.FunctionSpec.create('longToNumber').addParameter('long', 'Long*long').addCodeBlock(ts_poet_1.CodeBlock.empty()
.beginControlFlow('if (long.gt(Number.MAX_SAFE_INTEGER))')
// We use globalThis to avoid conflicts on protobuf types named `Error`.
.addStatement('throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER")')
.endControlFlow()
.addStatement('return long.toNumber()')));
}
}
function addBytesUtilityMethods(file) {
return file.addCode(ts_poet_1.CodeBlock.of(`interface WindowBase64 {
atob(b64: string): string;
btoa(bin: string): string;
}
const windowBase64 = (globalThis as unknown as WindowBase64);
const atob = windowBase64.atob || ((b64: string) => Buffer.from(b64, 'base64').toString('binary'));
const btoa = windowBase64.btoa || ((bin: string) => Buffer.from(bin, 'binary').toString('base64'));
function bytesFromBase64(b64: string): Uint8Array {
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
function base64FromBytes(arr: Uint8Array): string {
const bin: string[] = [];
for (let i = 0; i < arr.byteLength; ++i) {
bin.push(String.fromCharCode(arr[i]));
}
return btoa(bin.join(''));
}`));
}
function addDeepPartialType(file, options) {
let oneofCase = '';
if (options.oneof === OneofOption.UNIONS) {
oneofCase = `
: T extends { $case: string }
? { [K in keyof Omit<T, '$case'>]?: DeepPartial<T[K]> } & { $case: T['$case'] }`;
}
// Based on the type from ts-essentials
return file.addCode(ts_poet_1.CodeBlock.empty().add(`type Builtin = Date | Function | Uint8Array | string | number | undefined;
type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>${oneofCase}
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;`));
}
function addTimestampMethods(file, options) {
const timestampType = 'Timestamp@./google/protobuf/timestamp';
let secondsCodeLine = 'const seconds = date.getTime() / 1_000';
let toNumberCode = 't.seconds';
if (options.forceLong === LongOption.LONG) {
toNumberCode = 't.seconds.toNumber()';
secondsCodeLine = 'const seconds = numberToLong(date.getTime() / 1_000)';
}
else if (options.forceLong === LongOption.STRING) {
toNumberCode = 'Number(t.seconds)';
secondsCodeLine = 'const seconds = (date.getTime() / 1_000).toString()';
}
if (options.outputJsonMethods) {
file = file.addFunction(ts_poet_1.FunctionSpec.create('fromJsonTimestamp')
.addParameter('o', 'any')
.returns('Date')
.addCodeBlock(ts_poet_1.CodeBlock.empty()
.beginControlFlow('if (o instanceof Date)')
.addStatement('return o')
.nextControlFlow('else if (typeof o === "string")')
.addStatement('return new Date(o)')
.nextControlFlow('else')
.addStatement('return fromTimestamp(Timestamp.fromJSON(o))')
.endControlFlow()));
}
return file
.addFunction(ts_poet_1.FunctionSpec.create('toTimestamp')
.addParameter('date', 'Date')
.returns(timestampType)
.addCodeBlock(ts_poet_1.CodeBlock.empty()
.addStatement(secondsCodeLine)
.addStatement('const nanos = (date.getTime() %% 1_000) * 1_000_000')
.addStatement('return { seconds, nanos }')))
.addFunction(ts_poet_1.FunctionSpec.create('fromTimestamp')
.addParameter('t', timestampType)
.returns('Date')
.addCodeBlock(ts_poet_1.CodeBlock.empty()
.addStatement('let millis = %L * 1_000', toNumberCode)
.addStatement('millis += t.nanos / 1_000_000')
.addStatement('return new Date(millis)')));
}
const UNRECOGNIZED_ENUM_NAME = 'UNRECOGNIZED';
const UNRECOGNIZED_ENUM_VALUE = -1;
function generateEnum(options, fullName, enumDesc, sourceInfo) {
let code = ts_poet_1.CodeBlock.empty();
// Output the `enum { Foo, A = 0, B = 1 }`
utils_1.maybeAddComment(sourceInfo, (text) => (code = code.add(`/** %L */\n`, text)));
code = code.beginControlFlow('export enum %L', fullName);
enumDesc.value.forEach((valueDesc, index) => {
const info = sourceInfo.lookup(sourceInfo_1.Fields.enum.value, index);
utils_1.maybeAddComment(info, (text) => (code = code.add(`/** ${valueDesc.name} - ${text} */\n`)));
code = code.add('%L = %L,\n', valueDesc.name, valueDesc.number.toString());
});
code = code.add('%L = %L,\n', UNRECOGNIZED_ENUM_NAME, UNRECOGNIZED_ENUM_VALUE.toString());
code = code.endControlFlow();
if (options.outputJsonMethods) {
code = code.add('\n');
code = code.addFunction(generateEnumFromJson(fullName, enumDesc));
code = code.add('\n');
code = code.addFunction(generateEnumToJson(fullName, enumDesc));
}
return code;
}
/** Generates a function with a big switch statement to decode JSON -> our enum. */
function generateEnumFromJson(fullName, enumDesc) {
let func = ts_poet_1.FunctionSpec.create(`${case_1.camelCase(fullName)}FromJSON`)
.addModifiers(ts_poet_1.Modifier.EXPORT)
.addParameter('object', 'any')
.returns(fullName);
let body = ts_poet_1.CodeBlock.empty().beginControlFlow('switch (object)');
for (const valueDesc of enumDesc.value) {
body = body
.add('case %L:\n', valueDesc.number)
.add('case %S:%>\n', valueDesc.name)
.addStatement('return %L.%L%<', fullName, valueDesc.name);
}
body = body
.add('case %L:\n', UNRECOGNIZED_ENUM_VALUE)
.add('case %S:\n', UNRECOGNIZED_ENUM_NAME)
.add('default:%>\n')
.addStatement('return %L.%L%<', fullName, UNRECOGNIZED_ENUM_NAME)
.endControlFlow();
return func.addCodeBlock(body);
}
/** Generates a function with a big switch statement to encode our enum -> JSON. */
function generateEnumToJson(fullName, enumDesc) {
let func = ts_poet_1.FunctionSpec.create(`${case_1.camelCase(fullName)}ToJSON`)
.addModifiers(ts_poet_1.Modifier.EXPORT)
.addParameter('object', fullName)
.returns('string');
let body = ts_poet_1.CodeBlock.empty().beginControlFlow('switch (object)');
for (const valueDesc of enumDesc.value) {
body = body.add('case %L.%L:%>\n', fullName, valueDesc.name).addStatement('return %S%<', valueDesc.name);
}
body = body.add('default:%>\n').addStatement('return "UNKNOWN"%<').endControlFlow();
return func.addCodeBlock(body);
}
// When useOptionals=true, non-scalar fields are translated into optional properties.
function isOptionalProperty(field, options) {
return (options.useOptionals && types_1.isMessage(field) && !types_1.isRepeated(field)) || field.proto3Optional;
}
// Create the interface with properties
function generateInterfaceDeclaration(typeMap, fullName, messageDesc, sourceInfo, options) {
let message = ts_poet_1.InterfaceSpec.create(fullName).addModifiers(ts_poet_1.Modifier.EXPORT);
utils_1.maybeAddComment(sourceInfo, (text) => (message = message.addJavadoc(text)));
let processedOneofs = new Set();
messageDesc.field.forEach((fieldDesc, index) => {
// When oneof=unions, we generate a single property with an algebraic
// datatype (ADT) per `oneof` clause.
if (types_1.isWithinOneOfThatShouldBeUnion(options, fieldDesc)) {
const { oneofIndex } = fieldDesc;
if (!processedOneofs.has(oneofIndex)) {
processedOneofs.add(oneofIndex);
const prop = generateOneofProperty(typeMap, messageDesc, oneofIndex, sourceInfo, options);
message = message.addProperty(prop);
}
return;
}
let prop = ts_poet_1.PropertySpec.create(case_1.maybeSnakeToCamel(fieldDesc.name, options), types_1.toTypeName(typeMap, messageDesc, fieldDesc, options), isOptionalProperty(fieldDesc, options));
const info = sourceInfo.lookup(sourceInfo_1.Fields.message.field, index);
utils_1.maybeAddComment(info, (text) => (prop = prop.addJavadoc(text)));
message = message.addProperty(prop);
});
return message;
}
function generateOneofProperty(typeMap, messageDesc, oneofIndex, sourceInfo, options) {
let fields = messageDesc.field.filter((field) => {
return types_1.isWithinOneOf(field) && field.oneofIndex === oneofIndex;
});
let unionType = ts_poet_1.TypeNames.unionType(...fields.map((f) => {
let fieldName = case_1.maybeSnakeToCamel(f.name, options);
let typeName = types_1.toTypeName(typeMap, messageDesc, f, options);
return ts_poet_1.TypeNames.anonymousType(new ts_poet_1.Member('$case', ts_poet_1.TypeNames.typeLiteral(fieldName), false), new ts_poet_1.Member(fieldName, typeName, /* optional */ false));
}));
let prop = ts_poet_1.PropertySpec.create(case_1.maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, options), unionType, true // optional
);
// Ideally we'd put the comments for each oneof field next to the anonymous
// type we've created in the type union above, but ts-poet currently lacks
// that ability. For now just concatenate all comments into one big one.
let comments = [];
const info = sourceInfo.lookup(sourceInfo_1.Fields.message.oneof_decl, oneofIndex);
utils_1.maybeAddComment(info, (text) => comments.push(text));
messageDesc.field.forEach((field, index) => {
if (!types_1.isWithinOneOf(field) || field.oneofIndex !== oneofIndex) {
return;
}
const info = sourceInfo.lookup(sourceInfo_1.Fields.message.field, index);
const name = case_1.maybeSnakeToCamel(field.name, options);
utils_1.maybeAddComment(info, (text) => comments.push(name + '\n' + text));
});
if (comments.length) {
prop = prop.addJavadoc(comments.join('\n'));
}
return prop;
}
function generateBaseInstance(typeMap, fullName, messageDesc, options) {
// Create a 'base' instance with default values for decode to use as a prototype
let baseMessage = ts_poet_1.PropertySpec.create('base' + fullName, ts_poet_1.TypeNames.anyType('object')).addModifiers(ts_poet_1.Modifier.CONST);
let initialValue = ts_poet_1.CodeBlock.empty().beginHash();
sequency_1.asSequence(messageDesc.field)
.filterNot(types_1.isWithinOneOf)
.forEach((field) => {
let val = types_1.defaultValue(typeMap, field, options);
if (val === 'undefined' || types_1.isBytes(field)) {
return;
}
initialValue = initialValue.addHashEntry(case_1.maybeSnakeToCamel(field.name, options), val);
});
return baseMessage.initializerBlock(initialValue.endHash());
}
function visit(proto, sourceInfo, messageFn, options, enumFn = () => { }, tsPrefix = '', protoPrefix = '') {
const isRootFile = proto instanceof FileDescriptorProto;
const childEnumType = isRootFile ? sourceInfo_1.Fields.file.enum_type : sourceInfo_1.Fields.message.enum_type;
proto.enumType.forEach((enumDesc, index) => {
// I.e. Foo_Bar.Zaz_Inner
const protoFullName = protoPrefix + enumDesc.name;
// I.e. FooBar_ZazInner
const tsFullName = tsPrefix + case_1.maybeSnakeToCamel(enumDesc.name, options);
const nestedSourceInfo = sourceInfo.open(childEnumType, index);
enumFn(tsFullName, enumDesc, nestedSourceInfo, protoFullName);
});
const messages = proto instanceof FileDescriptorProto ? proto.messageType : proto.nestedType;
const childType = isRootFile ? sourceInfo_1.Fields.file.message_type : sourceInfo_1.Fields.message.nested_type;
messages.forEach((message, index) => {
// I.e. Foo_Bar.Zaz_Inner
const protoFullName = protoPrefix + message.name;
// I.e. FooBar_ZazInner
const tsFullName = tsPrefix + case_1.maybeSnakeToCamel(messageName(message), options);
const nestedSourceInfo = sourceInfo.open(childType, index);
messageFn(tsFullName, message, nestedSourceInfo, protoFullName);
visit(message, nestedSourceInfo, messageFn, options, enumFn, tsFullName + '_', protoFullName + '.');
});
}
exports.visit = visit;
function visitServices(proto, sourceInfo, serviceFn) {
proto.service.forEach((serviceDesc, index) => {
const nestedSourceInfo = sourceInfo.open(sourceInfo_1.Fields.file.service, index);
serviceFn(serviceDesc, nestedSourceInfo);
});
}
/** Creates a function to decode a message by loop overing the tags. */
function generateDecode(typeMap, fullName, messageDesc, options) {
// create the basic function declaration
let func = ts_poet_1.FunctionSpec.create('decode')
.addParameter('input', ts_poet_1.TypeNames.unionType('Uint8Array', 'Reader@protobufjs/minimal'))
.addParameter('length?', 'number')
.returns(fullName);
// add the initial end/message
func = func
.addStatement('const reader = input instanceof Uint8Array ? new Reader(input) : input')
.addStatement('let end = length === undefined ? reader.len : reader.pos + length')
.addStatement('const message = { ...base%L } as %L', fullName, fullName);
// initialize all lists
messageDesc.field.filter(types_1.isRepeated).forEach((field) => {
const value = types_1.isMapType(typeMap, messageDesc, field, options) ? '{}' : '[]';
func = func.addStatement('message.%L = %L', case_1.maybeSnakeToCamel(field.name, options), value);
});
// start the tag loop
func = func
.beginControlFlow('while (reader.pos < end)')
.addStatement('const tag = reader.uint32()')
.beginControlFlow('switch (tag >>> 3)');
// add a case for each incoming field
messageDesc.field.forEach((field) => {
const fieldName = case_1.maybeSnakeToCamel(field.name, options);
func = func.addCode('case %L:%>\n', field.number);
// get a generic 'reader.doSomething' bit that is specific to the basic type
let readSnippet;
if (types_1.isPrimitive(field)) {
readSnippet = ts_poet_1.CodeBlock.of('reader.%L()', types_1.toReaderCall(field));
if (types_1.isBytes(field)) {
if (options.env === EnvOption.NODE) {
readSnippet = readSnippet.add(' as Buffer');
}
}
else if (types_1.basicLongWireType(field.type) !== undefined) {
if (options.forceLong === LongOption.LONG) {
readSnippet = ts_poet_1.CodeBlock.of('%L as Long', readSnippet);
}
else if (options.forceLong === LongOption.STRING) {
readSnippet = ts_poet_1.CodeBlock.of('longToString(%L as Long)', readSnippet);
}
else {
readSnippet = ts_poet_1.CodeBlock.of('longToNumber(%L as Long)', readSnippet);
}
}
else if (types_1.isEnum(field)) {
readSnippet = readSnippet.add(' as any');
}
}
else if (types_1.isValueType(field)) {
readSnippet = ts_poet_1.CodeBlock.of('%T.decode(reader, reader.uint32()).value', types_1.basicTypeName(typeMap, field, options, { keepValueType: true }));
}
else if (types_1.isTimestamp(field)) {
readSnippet = ts_poet_1.CodeBlock.of('fromTimestamp(%T.decode(reader, reader.uint32()))', types_1.basicTypeName(typeMap, field, options, { keepValueType: true }));
}
else if (types_1.isMessage(field)) {
readSnippet = ts_poet_1.CodeBlock.of('%T.decode(reader, reader.uint32())', types_1.basicTypeName(typeMap, field, options));
}
else {
throw new Error(`Unhandled field ${field}`);
}
// and then use the snippet to handle repeated fields if necessary
if (types_1.isRepeated(field)) {
if (types_1.isMapType(typeMap, messageDesc, field, options)) {
// We need a unique const within the `cast` statement
const entryVariableName = `entry${field.number}`;
func = func
.addStatement(`const %L = %L`, entryVariableName, readSnippet)
.beginControlFlow('if (%L.value !== undefined)', entryVariableName)
.addStatement('message.%L[%L.key] = %L.value', fieldName, entryVariableName, entryVariableName)
.endControlFlow();
}
else if (types_1.packedType(field.type) === undefined) {
func = func.addStatement(`message.%L.push(%L)`, fieldName, readSnippet);
}
else {
func = func
.beginControlFlow('if ((tag & 7) === 2)')
.addStatement('const end2 = reader.uint32() + reader.pos')
.beginControlFlow('while (reader.pos < end2)')
.addStatement(`message.%L.push(%L)`, fieldName, readSnippet)
.endControlFlow()
.nextControlFlow('else')
.addStatement(`message.%L.push(%L)`, fieldName, readSnippet)
.endControlFlow();
}
}
else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
func = func.addStatement(`message.%L = {$case: '%L', %L: %L}`, oneofName, fieldName, fieldName, readSnippet);
}
else {
func = func.addStatement(`message.%L = %L`, fieldName, readSnippet);
}
func = func.addStatement('break%<');
});
func = func.addCode('default:%>\n').addStatement('reader.skipType(tag & 7)').addStatement('break%<');
// and then wrap up the switch/while/return
func = func.endControlFlow().endControlFlow().addStatement('return message');
return func;
}
/** Creates a function to encode a message by loop overing the tags. */
function generateEncode(typeMap, fullName, messageDesc, options) {
// create the basic function declaration
let func = ts_poet_1.FunctionSpec.create('encode')
.addParameter(messageDesc.field.length > 0 ? 'message' : '_', fullName)
.addParameter('writer', 'Writer@protobufjs/minimal', { defaultValueField: ts_poet_1.CodeBlock.of('Writer.create()') })
.returns('Writer@protobufjs/minimal');
// then add a case for each field
messageDesc.field.forEach((field) => {
const fieldName = case_1.maybeSnakeToCamel(field.name, options);
// get a generic writer.doSomething based on the basic type
let writeSnippet;
if (types_1.isPrimitive(field)) {
const tag = ((field.number << 3) | types_1.basicWireType(field.type)) >>> 0;
writeSnippet = (place) => ts_poet_1.CodeBlock.of('writer.uint32(%L).%L(%L)', tag, types_1.toReaderCall(field), place);
}
else if (types_1.isTimestamp(field)) {
const tag = ((field.number << 3) | 2) >>> 0;
writeSnippet = (place) => ts_poet_1.CodeBlock.of('%T.encode(toTimestamp(%L), writer.uint32(%L).fork()).ldelim()', types_1.basicTypeName(typeMap, field, options, { keepValueType: true }), place, tag);
}
else if (types_1.isValueType(field)) {
const tag = ((field.number << 3) | 2) >>> 0;
writeSnippet = (place) => ts_poet_1.CodeBlock.of('%T.encode({ value: %L! }, writer.uint32(%L).fork()).ldelim()', types_1.basicTypeName(typeMap, field, options, { keepValueType: true }), place, tag);
}
else if (types_1.isMessage(field)) {
const tag = ((field.number << 3) | 2) >>> 0;
writeSnippet = (place) => ts_poet_1.CodeBlock.of('%T.encode(%L, writer.uint32(%L).fork()).ldelim()', types_1.basicTypeName(typeMap, field, options), place, tag);
}
else {
throw new Error(`Unhandled field ${field}`);
}
if (types_1.isRepeated(field)) {
if (types_1.isMapType(typeMap, messageDesc, field, options)) {
func = func
.beginLambda('Object.entries(message.%L).forEach(([key, value]) =>', fieldName)
.addStatement('%L', writeSnippet('{ key: key as any, value }'))
.endLambda(')');
}
else if (types_1.packedType(field.type) === undefined) {
func = func
.beginControlFlow('for (const v of message.%L)', fieldName)
.addStatement('%L', writeSnippet('v!'))
.endControlFlow();
}
else {
const tag = ((field.number << 3) | 2) >>> 0;
func = func
.addStatement('writer.uint32(%L).fork()', tag)
.beginControlFlow('for (const v of message.%L)', fieldName)
.addStatement('writer.%L(v)', types_1.toReaderCall(field))
.endControlFlow()
.addStatement('writer.ldelim()');
}
}
else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
func = func
.beginControlFlow(`if (message.%L?.$case === '%L')`, oneofName, fieldName)
.addStatement('%L', writeSnippet(`message.${oneofName}.${fieldName}`))
.endControlFlow();
}
else if (types_1.isWithinOneOf(field)) {
// Oneofs don't have a default value check b/c they need to denote which-oneof presence
func = func
.beginControlFlow('if (message.%L !== undefined)', fieldName)
.addStatement('%L', writeSnippet(`message.${fieldName}`))
.endControlFlow();
}
else if (types_1.isMessage(field)) {
func = func
.beginControlFlow('if (message.%L !== undefined && message.%L !== %L)', fieldName, fieldName, types_1.defaultValue(typeMap, field, options))
.addStatement('%L', writeSnippet(`message.${fieldName}`))
.endControlFlow();
}
else {
func = func.addStatement('%L', writeSnippet(`message.${fieldName}`));
}
});
return func.addStatement('return writer');
}
/**
* Creates a function to decode a message from JSON.
*
* This is very similar to decode, we loop through looking for properties, with
* a few special cases for https://developers.google.com/protocol-buffers/docs/proto3#json.
* */
function generateFromJson(typeMap, fullName, messageDesc, options) {
// create the basic function declaration
let func = ts_poet_1.FunctionSpec.create('fromJSON')
.addParameter(messageDesc.field.length > 0 ? 'object' : '_', 'any')
.returns(fullName);
// create the message
func = func.addStatement('const message = { ...base%L } as %L', fullName, fullName);
// initialize all lists
messageDesc.field.filter(types_1.isRepeated).forEach((field) => {
const value = types_1.isMapType(typeMap, messageDesc, field, options) ? '{}' : '[]';
func = func.addStatement('message.%L = %L', case_1.maybeSnakeToCamel(field.name, options), value);
});
// add a check for each incoming field
messageDesc.field.forEach((field) => {
const fieldName = case_1.maybeSnakeToCamel(field.name, options);
// get a generic 'reader.doSomething' bit that is specific to the basic type
const readSnippet = (from) => {
if (types_1.isEnum(field)) {
const fromJson = types_1.getEnumMethod(typeMap, field.typeName, 'FromJSON');
return ts_poet_1.CodeBlock.of('%T(%L)', fromJson, from);
}
else if (types_1.isPrimitive(field)) {
// Convert primitives using the String(value)/Number(value)/bytesFromBase64(value)
if (types_1.isBytes(field)) {
if (options.env === EnvOption.NODE) {
return ts_poet_1.CodeBlock.of('Buffer.from(bytesFromBase64(%L))', from);
}
else {
return ts_poet_1.CodeBlock.of('bytesFromBase64(%L)', from);
}
}
else if (types_1.isLong(field) && options.forceLong === LongOption.LONG) {
const cstr = case_1.capitalize(types_1.basicTypeName(typeMap, field, options, { keepValueType: true }).toString());
return ts_poet_1.CodeBlock.of('%L.fromString(%L)', cstr, from);
}
else {
const cstr = case_1.capitalize(types_1.basicTypeName(typeMap, field, options, { keepValueType: true }).toString());
return ts_poet_1.CodeBlock.of('%L(%L)', cstr, from);
}
}
else if (types_1.isTimestamp(field)) {
return ts_poet_1.CodeBlock.of('fromJsonTimestamp(%L)', from);
}
else if (types_1.isValueType(field)) {
return ts_poet_1.CodeBlock.of('%L(%L)', case_1.capitalize(types_1.valueTypeName(field).toString()), from);
}
else if (types_1.isMessage(field)) {
if (types_1.isRepeated(field) && types_1.isMapType(typeMap, messageDesc, field, options)) {
const valueType = typeMap.get(field.typeName)[2].field[1];
if (types_1.isPrimitive(valueType)) {
// TODO Can we not copy/paste this from ^?
if (types_1.isBytes(valueType)) {
if (options.env === EnvOption.NODE) {
return ts_poet_1.CodeBlock.of('Buffer.from(bytesFromBase64(%L as string))', from);
}
else {
return ts_poet_1.CodeBlock.of('bytesFromBase64(%L as string)', from);
}
}
else {
const cstr = case_1.capitalize(types_1.basicTypeName(typeMap, FieldDescriptorProto.create({ type: valueType.type }), options).toString());
return ts_poet_1.CodeBlock.of('%L(%L)', cstr, from);
}
}
else if (types_1.isTimestamp(valueType)) {
return ts_poet_1.CodeBlock.of('fromJsonTimestamp(%L)', from);
}
else {
return ts_poet_1.CodeBlock.of('%T.fromJSON(%L)', types_1.basicTypeName(typeMap, valueType, options).toString(), from);
}
}
else {
return ts_poet_1.CodeBlock.of('%T.fromJSON(%L)', types_1.basicTypeName(typeMap, field, options), from);
}
}
else {
throw new Error(`Unhandled field ${field}`);
}
};
// and then use the snippet to handle repeated fields if necessary
func = func.beginControlFlow('if (object.%L !== undefined && object.%L !== null)', fieldName, fieldName);
if (types_1.isRepeated(field)) {
if (types_1.isMapType(typeMap, messageDesc, field, options)) {
func = func
.beginLambda('Object.entries(object.%L).forEach(([key, value]) =>', fieldName)
.addStatement(`message.%L[%L] = %L`, fieldName, maybeCastToNumber(typeMap, messageDesc, field, 'key', options), readSnippet('value'))
.endLambda(')');
}
else {
func = func
.beginControlFlow('for (const e of object.%L)', fieldName)
.addStatement(`message.%L.push(%L)`, fieldName, readSnippet('e'))
.endControlFlow();
}
}
else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
func = func.addStatement(`message.%L = {$case: '%L', %L: %L}`, oneofName, fieldName, fieldName, readSnippet(`object.${fieldName}`));
}
else {
func = func.addStatement(`message.%L = %L`, fieldName, readSnippet(`object.${fieldName}`));
}
// set the default value (TODO Support bytes)
if (!types_1.isRepeated(field) &&
field.type !== FieldDescriptorProto.Type.TYPE_BYTES &&
options.oneof !== OneofOption.UNIONS) {
func = func.nextControlFlow('else');
func = func.addStatement(`message.%L = %L`, fieldName, types_1.isWithinOneOf(field) ? 'undefined' : types_1.defaultValue(typeMap, field, options));
}
func = func.endControlFlow();
});
// and then wrap up the switch/while/return
func = func.addStatement('return message');
return func;
}
function generateToJson(typeMap, fullName, messageDesc, options) {
// create the basic function declaration
let func = ts_poet_1.FunctionSpec.create('toJSON')
.addParameter(messageDesc.field.length > 0 ? 'message' : '_', fullName)
.returns('unknown');
func = func.addCodeBlock(ts_poet_1.CodeBlock.empty().addStatement('const obj: any = {}'));
// then add a case for each field
messageDesc.field.forEach((field) => {
const fieldName = case_1.maybeSnakeToCamel(field.name, options);
const readSnippet = (from) => {
if (types_1.isEnum(field)) {
const toJson = types_1.getEnumMethod(typeMap, field.typeName, 'ToJSON');
return types_1.isWithinOneOf(field)
? ts_poet_1.CodeBlock.of('%L !== undefined ? %T(%L) : undefined', from, toJson, from)
: ts_poet_1.CodeBlock.of('%T(%L)', toJson, from);
}
else if (types_1.isTimestamp(field)) {
return ts_poet_1.CodeBlock.of('%L !== undefined ? %L.toISOString() : null', from, from);
}
else if (types_1.isMapType(typeMap, messageDesc, field, options)) {
// For map types, drill-in and then admittedly re-hard-code our per-value-type logic
const valueType = typeMap.get(field.typeName)[2].field[1];
if (types_1.isEnum(valueType)) {
const toJson = types_1.getEnumMethod(typeMap, field.typeName, 'ToJSON');
return ts_poet_1.CodeBlock.of('%T(%L)', toJson, from);
}
else if (types_1.isBytes(valueType)) {
return ts_poet_1.CodeBlock.of('base64FromBytes(%L)', from);
}
else if (types_1.isTimestamp(valueType)) {
return ts_poet_1.CodeBlock.of('%L.toISOString()', from);
}
else if (types_1.isPrimitive(valueType)) {
return ts_poet_1.CodeBlock.of('%L', from);
}
else {
return ts_poet_1.CodeBlock.of('%T.toJSON(%L)', types_1.basicTypeName(typeMap, valueType, options).toString(), from);
}
}
else if (types_1.isMessage(field) && !types_1.isValueType(field) && !types_1.isMapType(typeMap, messageDesc, field, options)) {
return ts_poet_1.CodeBlock.of('%L ? %T.toJSON(%L) : %L', from, types_1.basicTypeName(typeMap, field, options, { keepValueType: true }), from, types_1.defaultValue(typeMap, field, options));
}
else if (types_1.isBytes(field)) {
if (types_1.isWithinOneOf(field)) {
return ts_poet_1.CodeBlock.of('%L !== undefined ? base64FromBytes(%L) : undefined', from, from);
}
else {
return ts_poet_1.CodeBlock.of('base64FromBytes(%L !== undefined ? %L : %L)', from, from, types_1.defaultValue(typeMap, field, options));
}
}
else if (types_1.isLong(field) && options.forceLong === LongOption.LONG) {
return ts_poet_1.CodeBlock.of('(%L || %L).toString()', from, types_1.isWithinOneOf(field) ? 'undefined' : types_1.defaultValue(typeMap, field, options));
}
else {
return ts_poet_1.CodeBlock.of('%L', from);
}
};
if (types_1.isMapType(typeMap, messageDesc, field, options)) {
// Maps might need their values transformed, i.e. bytes --> base64
func = func
.addStatement('obj.%L = {}', fieldName)
.beginControlFlow('if (message.%L)', fieldName)
.beginLambda('Object.entries(message.%L).forEach(([k, v]) =>', fieldName)
.addStatement('obj.%L[k] = %L', fieldName, readSnippet('v'))
.endLambda(')')
.endControlFlow();
}
else if (types_1.isRepeated(field)) {
// Arrays might need their elements transformed
func = func
.beginControlFlow('if (message.%L)', fieldName)
.addStatement('obj.%L = message.%L.map(e => %L)', fieldName, fieldName, readSnippet('e'))
.nextControlFlow('else')
.addStatement('obj.%L = []', fieldName)
.endControlFlow();
}
else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
// oneofs in a union are only output as `oneof name = ...`
let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
func = func.addStatement(`message.%L?.$case === '%L' && (obj.%L = %L)`, oneofName, fieldName, fieldName, readSnippet(`message.${oneofName}?.${fieldName}`));
}
else {
func = func.addStatement('message.%L !== undefined && (obj.%L = %L)', fieldName, fieldName, readSnippet(`message.${fieldName}`));
}
});
return func.addStatement('return obj');
}
function generateFromPartial(typeMap, fullName, messageDesc, options) {
// create the basic function declaration
let func = ts_poet_1.FunctionSpec.create('fromPartial')
.addParameter(messageDesc.field.length > 0 ? 'object' : '_', `DeepPartial<${fullName}>`)
.returns(fullName);
// create the message
func = func.addStatement('const message = { ...base%L } as %L', fullName, fullName);
// initialize all lists
messageDesc.field.filter(types_1.isRepeated).forEach((field) => {
const value = types_1.isMapType(typeMap, messageDesc, field, options) ? '{}' : '[]';
func = func.addStatement('message.%L = %L', case_1.maybeSnakeToCamel(field.name, options), value);
});
// add a check for each incoming field
messageDesc.field.forEach((field) => {
const fieldName = case_1.maybeSnakeToCamel(field.name, options);
const readSnippet = (from) => {
if (types_1.isEnum(field) || types_1.isPrimitive(field) || types_1.isTimestamp(field) || types_1.isValueType(field)) {
return ts_poet_1.CodeBlock.of(from);
}
else if (types_1.isMessage(field)) {
if (types_1.isRepeated(field) && types_1.isMapType(typeMap, messageDesc, field, options)) {
const valueType = typeMap.get(field.typeName)[2].field[1];
if (types_1.isPrimitive(valueType)) {
if (types_1.isBytes(valueType)) {
return ts_poet_1.CodeBlock.of('%L', from);
}
else {
const cstr = case_1.capitalize(types_1.basicTypeName(typeMap, FieldDescriptorProto.create({ type: valueType.type }), options).toString());
return ts_poet_1.CodeBlock.of('%L(%L)', cstr, from);
}
}
else if (types_1.isTimestamp(valueType)) {
return ts_poet_1.CodeBlock.of('%L', from);
}
else {
return ts_poet_1.CodeBlock.of('%T.fromPartial(%L)', types_1.basicTypeName(typeMap, valueType, options).toString(), from);
}
}
else {
return ts_poet_1.CodeBlock.of('%T.fromPartial(%L)', types_1.basicTypeName(typeMap, field, options), from);
}
}
else {
throw new Error(`Unhandled field ${field}`);
}
};
// and then use the snippet to handle repeated fields if necessary
if (types_1.isRepeated(field)) {
func = func.beginControlFlow('if (object.%L !== undefined && object.%L !== null)', fieldName, fieldName);
if (types_1.isMapType(typeMap, messageDesc, field, options)) {
func = func
.beginLambda('Object.entries(object.%L).forEach(([key, value]) =>', fieldName)
.beginControlFlow('if (value !== undefined)')
.addStatement(`message.%L[%L] = %L`, fieldName, maybeCastToNumber(typeMap, messageDesc, field, 'key', options), readSnippet('value'))
.endControlFlow()
.endLambda(')');
}
else {
func = func
.beginControlFlow('for (const e of object.%L)', fieldName)
.addStatement(`message.%L.push(%L)`, fieldName, readSnippet('e'))
.endControlFlow();
}
}
else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
func = func
.beginControlFlow(`if (object.%L?.$case === '%L' && object.%L?.%L !== undefined && object.%L?.%L !== null)`, oneofName, fieldName, oneofName, fieldName, oneofName, fieldName)
.addStatement(`message.%L = {$case: '%L', %L: %L}`, oneofName, fieldName, fieldName, readSnippet(`object.${oneofName}.${fieldName}`));
}
else {
func = func.beginControlFlow('if (object.%L !== undefined && object.%L !== null)', fieldName, fieldName);
if (types_1.isLong(field) && options.forceLong === LongOption.LONG) {
func = func.addStatement(`message.%L = %L as %L`, fieldName, readSnippet(`object.${fieldName}`), types_1.basicTypeName(typeMap, field, options));
}
else {
func = func.addStatement(`message.%L = %L`, fieldName, readSnippet(`object.${fieldName}`));
}
}
if (!types_1.isRepeated(field) && options.oneof !== OneofOption.UNIONS) {
func = func.nextControlFlow('else');
func = func.addStatement(`message.%L = %L`, fieldName, types_1.isWithinOneOf(field) ? 'undefined' : types_1.defaultValue(typeMap, field, options));
}
func = func.endControlFlow();
});
// and then wrap up the switch/while/return
return func.addStatement('return message');
}
exports.contextTypeVar = ts_poet_1.TypeNames.typeVariable('Context', ts_poet_1.TypeNames.bound('DataLoaders'));
function maybeCastToNumber(typeMap, messageDesc, field, variableName, options) {
const { keyType } = types_1.detectMapType(typeMap, messageDesc, field, options);
if (keyType === ts_poet_1.TypeNames.STRING) {
return variableName;
}
else {
return `Number(${variableName})`;
}
}
const builtInNames = ['Date'];
/** Potentially suffixes `Message` to names to avoid conflicts, i.e. with `Date`. */
function messageName(message) {
const { name } = message;
return builtInNames.includes(name) ? `${name}Message` : name;
}