UNPKG

ts-proto

Version:

[![npm](https://img.shields.io/npm/v/ts-proto)](https://www.npmjs.com/package/ts-proto) [![build](https://github.com/stephenh/ts-proto/workflows/Build/badge.svg)](https://github.com/stephenh/ts-proto/actions)

1,022 lines (1,021 loc) 75.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.contextTypeVar = exports.makeUtils = exports.generateFile = void 0; const ts_poet_1 = require("ts-poet"); const types_1 = require("./types"); 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"); const generate_grpc_web_1 = require("./generate-grpc-web"); const enums_1 = require("./enums"); const visit_1 = require("./visit"); const options_1 = require("./options"); const schema_1 = require("./schema"); const ConditionalOutput_1 = require("ts-poet/build/ConditionalOutput"); const generate_grpc_js_1 = require("./generate-grpc-js"); const generate_generic_service_definition_1 = require("./generate-generic-service-definition"); const generate_nice_grpc_1 = require("./generate-nice-grpc"); function generateFile(ctx, fileDesc) { var _a; const { options, utils } = ctx; if (options.useOptionals === false) { console.warn("ts-proto: Passing useOptionals as a boolean option is deprecated and will be removed in a future version. Please pass the string 'none' instead of false."); options.useOptionals = 'none'; } else if (options.useOptionals === true) { console.warn("ts-proto: Passing useOptionals as a boolean option is deprecated and will be removed in a future version. Please pass the string 'messages' instead of true."); options.useOptionals = 'messages'; } // 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 structure 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 suffix = `${options.fileSuffix}.ts`; const moduleName = fileDesc.name.replace('.proto', suffix); const chunks = []; // Indicate this file's source protobuf package for reflective use with google.protobuf.Any if (options.exportCommonSymbols) { chunks.push(ts_poet_1.code `export const protobufPackage = '${fileDesc.package}';`); } // Syntax, unlike most fields, is not repeated and thus does not use an index const sourceInfo = sourceInfo_1.default.fromDescriptor(fileDesc); const headerComment = sourceInfo.lookup(sourceInfo_1.Fields.file.syntax, undefined); utils_1.maybeAddComment(headerComment, chunks, (_a = fileDesc.options) === null || _a === void 0 ? void 0 : _a.deprecated); // Apply formatting to methods here, so they propagate globally for (let svc of fileDesc.service) { for (let i = 0; i < svc.method.length; i++) { svc.method[i] = new utils_1.FormattedMethodDescriptor(svc.method[i], options); } } // first make all the type declarations visit_1.visit(fileDesc, sourceInfo, (fullName, message, sInfo, fullProtoTypeName) => { chunks.push(generateInterfaceDeclaration(ctx, fullName, message, sInfo, utils_1.maybePrefixPackage(fileDesc, fullProtoTypeName))); }, options, (fullName, enumDesc, sInfo) => { chunks.push(enums_1.generateEnum(ctx, fullName, enumDesc, sInfo)); }); // If nestJs=true export [package]_PACKAGE_NAME and [service]_SERVICE_NAME const if (options.nestJs) { const prefix = case_1.camelToSnake(fileDesc.package.replace(/\./g, '_')); chunks.push(ts_poet_1.code `export const ${prefix}_PACKAGE_NAME = '${fileDesc.package}';`); } if (options.outputEncodeMethods || options.outputJsonMethods || options.outputTypeRegistry) { // then add the encoder/decoder/base instance visit_1.visit(fileDesc, sourceInfo, (fullName, message, sInfo, fullProtoTypeName) => { const fullTypeName = utils_1.maybePrefixPackage(fileDesc, fullProtoTypeName); chunks.push(generateBaseInstanceFactory(ctx, fullName, message, fullTypeName)); const staticMembers = []; if (options.outputTypeRegistry) { staticMembers.push(ts_poet_1.code `$type: '${fullTypeName}' as const`); } if (options.outputEncodeMethods) { staticMembers.push(generateEncode(ctx, fullName, message)); staticMembers.push(generateDecode(ctx, fullName, message)); } if (options.outputJsonMethods) { staticMembers.push(generateFromJson(ctx, fullName, fullTypeName, message)); staticMembers.push(generateToJson(ctx, fullName, fullTypeName, message)); } if (options.outputPartialMethods) { staticMembers.push(generateFromPartial(ctx, fullName, message)); } const structFieldNames = { nullValue: case_1.maybeSnakeToCamel('null_value', ctx.options), numberValue: case_1.maybeSnakeToCamel('number_value', ctx.options), stringValue: case_1.maybeSnakeToCamel('string_value', ctx.options), boolValue: case_1.maybeSnakeToCamel('bool_value', ctx.options), structValue: case_1.maybeSnakeToCamel('struct_value', ctx.options), listValue: case_1.maybeSnakeToCamel('list_value', ctx.options), }; staticMembers.push(...generateWrap(ctx, fullTypeName, structFieldNames)); staticMembers.push(...generateUnwrap(ctx, fullTypeName, structFieldNames)); chunks.push(ts_poet_1.code ` export const ${ts_poet_1.def(fullName)} = { ${ts_poet_1.joinCode(staticMembers, { on: ',\n\n' })} }; `); if (options.outputTypeRegistry) { const messageTypeRegistry = ts_poet_1.imp('messageTypeRegistry@./typeRegistry'); chunks.push(ts_poet_1.code ` ${messageTypeRegistry}.set(${fullName}.$type, ${fullName}); `); } }, options); } let hasServerStreamingMethods = false; let hasStreamingMethods = false; visit_1.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 chunks.push(generate_nestjs_1.generateNestjsServiceClient(ctx, fileDesc, sInfo, serviceDesc)); // and the service controller interface chunks.push(generate_nestjs_1.generateNestjsServiceController(ctx, fileDesc, sInfo, serviceDesc)); // generate nestjs grpc service controller decorator chunks.push(generate_nestjs_1.generateNestjsGrpcServiceMethodsDecorator(ctx, serviceDesc)); let serviceConstName = `${case_1.camelToSnake(serviceDesc.name)}_NAME`; if (!serviceDesc.name.toLowerCase().endsWith('service')) { serviceConstName = `${case_1.camelToSnake(serviceDesc.name)}_SERVICE_NAME`; } chunks.push(ts_poet_1.code `export const ${serviceConstName} = "${serviceDesc.name}";`); } else { const uniqueServices = [...new Set(options.outputServices)].sort(); uniqueServices.forEach((outputService) => { if (outputService === options_1.ServiceOption.GRPC) { chunks.push(generate_grpc_js_1.generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc)); } else if (outputService === options_1.ServiceOption.NICE_GRPC) { chunks.push(generate_nice_grpc_1.generateNiceGrpcService(ctx, fileDesc, sInfo, serviceDesc)); } else if (outputService === options_1.ServiceOption.GENERIC) { chunks.push(generate_generic_service_definition_1.generateGenericServiceDefinition(ctx, fileDesc, sInfo, serviceDesc)); } else if (outputService === options_1.ServiceOption.DEFAULT) { // This service could be Twirp or grpc-web or JSON (maybe). So far all of their // interfaces are fairly similar so we share the same service interface. chunks.push(generate_services_1.generateService(ctx, fileDesc, sInfo, serviceDesc)); if (options.outputClientImpl === true) { chunks.push(generate_services_1.generateServiceClientImpl(ctx, fileDesc, serviceDesc)); } else if (options.outputClientImpl === 'grpc-web') { chunks.push(generate_grpc_web_1.generateGrpcClientImpl(ctx, fileDesc, serviceDesc)); chunks.push(generate_grpc_web_1.generateGrpcServiceDesc(fileDesc, serviceDesc)); serviceDesc.method.forEach((method) => { if (!method.clientStreaming) { chunks.push(generate_grpc_web_1.generateGrpcMethodDesc(ctx, serviceDesc, method)); } if (method.serverStreaming) { hasServerStreamingMethods = true; } }); } } }); } serviceDesc.method.forEach((methodDesc, index) => { if (methodDesc.serverStreaming || methodDesc.clientStreaming) { hasStreamingMethods = true; } }); }); if (options.outputServices.includes(options_1.ServiceOption.DEFAULT) && options.outputClientImpl && fileDesc.service.length > 0) { if (options.outputClientImpl === true) { chunks.push(generate_services_1.generateRpcType(ctx, hasStreamingMethods)); } else if (options.outputClientImpl === 'grpc-web') { chunks.push(generate_grpc_web_1.addGrpcWebMisc(ctx, hasServerStreamingMethods)); } } if (options.context) { chunks.push(generate_services_1.generateDataLoaderOptionsType()); chunks.push(generate_services_1.generateDataLoadersType()); } if (options.outputSchema) { chunks.push(...schema_1.generateSchema(ctx, fileDesc, sourceInfo)); } chunks.push(...Object.values(utils).map((v) => { if (v instanceof ConditionalOutput_1.ConditionalOutput) { return ts_poet_1.code `${v.ifUsed}`; } else if (v instanceof ts_poet_1.Code) { return v; } else { return ts_poet_1.code ``; } })); // Finally, reset method definitions to their original state (unformatted) // This is mainly so that the `meta-typings` tests pass for (let svc of fileDesc.service) { for (let i = 0; i < svc.method.length; i++) { const methodInfo = svc.method[i]; utils_1.assertInstanceOf(methodInfo, utils_1.FormattedMethodDescriptor); svc.method[i] = methodInfo.getSource(); } } return [moduleName, ts_poet_1.joinCode(chunks, { on: '\n\n' })]; } exports.generateFile = generateFile; /** These are runtime utility methods used by the generated code. */ function makeUtils(options) { const bytes = makeByteUtils(); const longs = makeLongUtils(options, bytes); return { ...bytes, ...makeDeepPartial(options, longs), ...makeObjectIdMethods(options), ...makeTimestampMethods(options, longs), ...longs, ...makeComparisonUtils(), ...makeNiceGrpcServerStreamingMethodResult(), }; } exports.makeUtils = makeUtils; function makeLongUtils(options, bytes) { // 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.imp('util@protobufjs/minimal'); const configure = ts_poet_1.imp('configure@protobufjs/minimal'); // Before esModuleInterop, we had to use 'import * as Long from long` b/c long is // an `export =` module and exports only the Long constructor (which is callable). // See https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require. // // With esModuleInterop on, `* as Long` is no longer the constructor, it's the module, // so we want to go back to `import { Long } from long`, which is specifically forbidden // due to `export =` w/o esModuleInterop. // // I.e there is not an import for long that "just works" in both esModuleInterop and // not esModuleInterop. const Long = options.esModuleInterop ? ts_poet_1.imp('Long=long') : ts_poet_1.imp('Long*long'); const disclaimer = options.esModuleInterop ? '' : ` // If you get a compile-error about 'Constructor<Long> and ... have no overlap', // add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'.`; // Kinda hacky, but we always init long unless in onlyTypes mode. I'd rather do // this more implicitly, like if `Long@long` is imported or something like that. const longInit = options.onlyTypes ? ts_poet_1.code `` : ts_poet_1.code ` ${disclaimer} if (${util}.Long !== ${Long}) { ${util}.Long = ${Long} as any; ${configure}(); } `; // TODO This is unused? const numberToLong = ts_poet_1.conditionalOutput('numberToLong', ts_poet_1.code ` function numberToLong(number: number) { return ${Long}.fromNumber(number); } `); const longToString = ts_poet_1.conditionalOutput('longToString', ts_poet_1.code ` function longToString(long: ${Long}) { return long.toString(); } `); const longToNumber = ts_poet_1.conditionalOutput('longToNumber', ts_poet_1.code ` function longToNumber(long: ${Long}): number { if (long.gt(Number.MAX_SAFE_INTEGER)) { throw new ${bytes.globalThis}.Error("Value is larger than Number.MAX_SAFE_INTEGER") } return long.toNumber(); } `); return { numberToLong, longToNumber, longToString, longInit, Long }; } function makeByteUtils() { const globalThis = ts_poet_1.conditionalOutput('globalThis', ts_poet_1.code ` declare var self: any | undefined; declare var window: any | undefined; declare var global: any | undefined; var globalThis: any = (() => { if (typeof globalThis !== "undefined") return globalThis; if (typeof self !== "undefined") return self; if (typeof window !== "undefined") return window; if (typeof global !== "undefined") return global; throw "Unable to locate global object"; })(); `); const bytesFromBase64 = ts_poet_1.conditionalOutput('bytesFromBase64', ts_poet_1.code ` const atob: (b64: string) => string = ${globalThis}.atob || ((b64) => ${globalThis}.Buffer.from(b64, 'base64').toString('binary')); 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; } `); const base64FromBytes = ts_poet_1.conditionalOutput('base64FromBytes', ts_poet_1.code ` const btoa : (bin: string) => string = ${globalThis}.btoa || ((bin) => ${globalThis}.Buffer.from(bin, 'binary').toString('base64')); function base64FromBytes(arr: Uint8Array): string { const bin: string[] = []; arr.forEach((byte) => { bin.push(String.fromCharCode(byte)); }); return btoa(bin.join('')); } `); return { globalThis, bytesFromBase64, base64FromBytes }; } function makeDeepPartial(options, longs) { let oneofCase = ''; if (options.oneof === options_1.OneofOption.UNIONS) { oneofCase = ` : T extends { $case: string } ? { [K in keyof Omit<T, '$case'>]?: DeepPartial<T[K]> } & { $case: T['$case'] } `; } const maybeExport = options.exportCommonSymbols ? 'export' : ''; // Allow passing longs as numbers or strings, nad we'll convert them const maybeLong = options.forceLong === options_1.LongOption.LONG ? ts_poet_1.code ` : T extends ${longs.Long} ? string | number | Long ` : ''; const Builtin = ts_poet_1.conditionalOutput('Builtin', ts_poet_1.code `type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;`); // Based on https://github.com/sindresorhus/type-fest/pull/259 const maybeExcludeType = options.outputTypeRegistry ? `| '$type'` : ''; const Exact = ts_poet_1.conditionalOutput('Exact', ts_poet_1.code ` type KeysOfUnion<T> = T extends T ? keyof T : never; ${maybeExport} type Exact<P, I extends P> = P extends ${Builtin} ? P : P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P> ${maybeExcludeType}>, never>; `); // Based on the type from ts-essentials const keys = options.outputTypeRegistry ? ts_poet_1.code `Exclude<keyof T, '$type'>` : ts_poet_1.code `keyof T`; const DeepPartial = ts_poet_1.conditionalOutput('DeepPartial', ts_poet_1.code ` ${maybeExport} type DeepPartial<T> = T extends ${Builtin} ? T ${maybeLong} : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>${oneofCase} : T extends {} ? { [K in ${keys}]?: DeepPartial<T[K]> } : Partial<T>; `); return { Builtin, DeepPartial, Exact }; } function makeObjectIdMethods(options) { const mongodb = ts_poet_1.imp('mongodb*mongodb'); const fromProtoObjectId = ts_poet_1.conditionalOutput('fromProtoObjectId', ts_poet_1.code ` function fromProtoObjectId(oid: ObjectId): ${mongodb}.ObjectId { return new ${mongodb}.ObjectId(oid.value); } `); const fromJsonObjectId = ts_poet_1.conditionalOutput('fromJsonObjectId', ts_poet_1.code ` function fromJsonObjectId(o: any): ${mongodb}.ObjectId { if (o instanceof ${mongodb}.ObjectId) { return o; } else if (typeof o === "string") { return new ${mongodb}.ObjectId(o); } else { return ${fromProtoObjectId}(ObjectId.fromJSON(o)); } } `); const toProtoObjectId = ts_poet_1.conditionalOutput('toProtoObjectId', ts_poet_1.code ` function toProtoObjectId(oid: ${mongodb}.ObjectId): ObjectId { const value = oid.toString(); return { value }; } `); return { fromJsonObjectId, fromProtoObjectId, toProtoObjectId }; } function makeTimestampMethods(options, longs) { const Timestamp = utils_1.impProto(options, 'google/protobuf/timestamp', 'Timestamp'); let seconds = 'date.getTime() / 1_000'; let toNumberCode = 't.seconds'; if (options.forceLong === options_1.LongOption.LONG) { toNumberCode = 't.seconds.toNumber()'; seconds = ts_poet_1.code `${longs.numberToLong}(date.getTime() / 1_000)`; } else if (options.forceLong === options_1.LongOption.STRING) { toNumberCode = 'Number(t.seconds)'; // Must discard the fractional piece here // Otherwise the fraction ends up on the seconds when parsed as a Long // (note this only occurs when the string is > 8 characters) seconds = 'Math.trunc(date.getTime() / 1_000).toString()'; } const maybeTypeField = options.outputTypeRegistry ? `$type: 'google.protobuf.Timestamp',` : ''; const toTimestamp = ts_poet_1.conditionalOutput('toTimestamp', options.useDate === options_1.DateOption.STRING ? ts_poet_1.code ` function toTimestamp(dateStr: string): ${Timestamp} { const date = new Date(dateStr); const seconds = ${seconds}; const nanos = (date.getTime() % 1_000) * 1_000_000; return { ${maybeTypeField} seconds, nanos }; } ` : ts_poet_1.code ` function toTimestamp(date: Date): ${Timestamp} { const seconds = ${seconds}; const nanos = (date.getTime() % 1_000) * 1_000_000; return { ${maybeTypeField} seconds, nanos }; } `); const fromTimestamp = ts_poet_1.conditionalOutput('fromTimestamp', options.useDate === options_1.DateOption.STRING ? ts_poet_1.code ` function fromTimestamp(t: ${Timestamp}): string { let millis = ${toNumberCode} * 1_000; millis += t.nanos / 1_000_000; return new Date(millis).toISOString(); } ` : ts_poet_1.code ` function fromTimestamp(t: ${Timestamp}): Date { let millis = ${toNumberCode} * 1_000; millis += t.nanos / 1_000_000; return new Date(millis); } `); const fromJsonTimestamp = ts_poet_1.conditionalOutput('fromJsonTimestamp', options.useDate === options_1.DateOption.DATE ? ts_poet_1.code ` function fromJsonTimestamp(o: any): Date { if (o instanceof Date) { return o; } else if (typeof o === "string") { return new Date(o); } else { return ${fromTimestamp}(Timestamp.fromJSON(o)); } } ` : ts_poet_1.code ` function fromJsonTimestamp(o: any): Timestamp { if (o instanceof Date) { return ${toTimestamp}(o); } else if (typeof o === "string") { return ${toTimestamp}(new Date(o)); } else { return Timestamp.fromJSON(o); } } `); return { toTimestamp, fromTimestamp, fromJsonTimestamp }; } function makeComparisonUtils() { const isObject = ts_poet_1.conditionalOutput('isObject', ts_poet_1.code ` function isObject(value: any): boolean { return typeof value === 'object' && value !== null; }`); const isSet = ts_poet_1.conditionalOutput('isSet', ts_poet_1.code ` function isSet(value: any): boolean { return value !== null && value !== undefined; }`); return { isObject, isSet }; } function makeNiceGrpcServerStreamingMethodResult() { const NiceGrpcServerStreamingMethodResult = ts_poet_1.conditionalOutput('ServerStreamingMethodResult', ts_poet_1.code ` export type ServerStreamingMethodResult<Response> = { [Symbol.asyncIterator](): AsyncIterator<Response, void>; }; `); return { NiceGrpcServerStreamingMethodResult }; } // Create the interface with properties function generateInterfaceDeclaration(ctx, fullName, messageDesc, sourceInfo, fullTypeName) { var _a; const { options } = ctx; const chunks = []; utils_1.maybeAddComment(sourceInfo, chunks, (_a = messageDesc.options) === null || _a === void 0 ? void 0 : _a.deprecated); // interface name should be defined to avoid import collisions chunks.push(ts_poet_1.code `export interface ${ts_poet_1.def(fullName)} {`); if (ctx.options.outputTypeRegistry) { chunks.push(ts_poet_1.code `$type: '${fullTypeName}',`); } // When oneof=unions, we generate a single property with an ADT per `oneof` clause. const processedOneofs = new Set(); messageDesc.field.forEach((fieldDesc, index) => { var _a; if (types_1.isWithinOneOfThatShouldBeUnion(options, fieldDesc)) { const { oneofIndex } = fieldDesc; if (!processedOneofs.has(oneofIndex)) { processedOneofs.add(oneofIndex); chunks.push(generateOneofProperty(ctx, messageDesc, oneofIndex, sourceInfo)); } return; } const info = sourceInfo.lookup(sourceInfo_1.Fields.message.field, index); utils_1.maybeAddComment(info, chunks, (_a = fieldDesc.options) === null || _a === void 0 ? void 0 : _a.deprecated); const name = case_1.maybeSnakeToCamel(fieldDesc.name, options); const type = types_1.toTypeName(ctx, messageDesc, fieldDesc); const q = types_1.isOptionalProperty(fieldDesc, messageDesc.options, options) ? '?' : ''; chunks.push(ts_poet_1.code `${name}${q}: ${type}, `); }); chunks.push(ts_poet_1.code `}`); return ts_poet_1.joinCode(chunks, { on: '\n' }); } function generateOneofProperty(ctx, messageDesc, oneofIndex, sourceInfo) { const { options } = ctx; const fields = messageDesc.field.filter((field) => types_1.isWithinOneOf(field) && field.oneofIndex === oneofIndex); const unionType = ts_poet_1.joinCode(fields.map((f) => { let fieldName = case_1.maybeSnakeToCamel(f.name, options); let typeName = types_1.toTypeName(ctx, messageDesc, f); return ts_poet_1.code `{ $case: '${fieldName}', ${fieldName}: ${typeName} }`; }), { on: ' | ' }); const name = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, options); return ts_poet_1.code `${name}?: ${unionType},`; /* // 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: Array<string> = []; const info = sourceInfo.lookup(Fields.message.oneof_decl, oneofIndex); maybeAddComment(info, (text) => comments.push(text)); messageDesc.field.forEach((field, index) => { if (!isWithinOneOf(field) || field.oneofIndex !== oneofIndex) { return; } const info = sourceInfo.lookup(Fields.message.field, index); const name = maybeSnakeToCamel(field.name, options); maybeAddComment(info, (text) => comments.push(name + '\n' + text)); }); if (comments.length) { prop = prop.addJavadoc(comments.join('\n')); } return prop; */ } // Create a function that constructs 'base' instance with default values for decode to use as a prototype function generateBaseInstanceFactory(ctx, fullName, messageDesc, fullTypeName) { const fields = []; // When oneof=unions, we generate a single property with an ADT per `oneof` clause. const processedOneofs = new Set(); for (const field of messageDesc.field) { if (types_1.isWithinOneOfThatShouldBeUnion(ctx.options, field)) { const { oneofIndex } = field; if (!processedOneofs.has(oneofIndex)) { processedOneofs.add(oneofIndex); const name = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, ctx.options); fields.push(ts_poet_1.code `${name}: undefined`); } continue; } const name = case_1.maybeSnakeToCamel(field.name, ctx.options); const val = types_1.isWithinOneOf(field) ? 'undefined' : types_1.isMapType(ctx, messageDesc, field) ? '{}' : types_1.isRepeated(field) ? '[]' : types_1.defaultValue(ctx, field); fields.push(ts_poet_1.code `${name}: ${val}`); } if (ctx.options.outputTypeRegistry) { fields.unshift(ts_poet_1.code `$type: '${fullTypeName}'`); } return ts_poet_1.code ` function createBase${fullName}(): ${fullName} { return { ${ts_poet_1.joinCode(fields, { on: ',' })} }; } `; } /** Creates a function to decode a message by loop overing the tags. */ function generateDecode(ctx, fullName, messageDesc) { const { options, utils, typeMap } = ctx; const chunks = []; let createBase = ts_poet_1.code `createBase${fullName}()`; if (options.usePrototypeForDefaults) { createBase = ts_poet_1.code `Object.create(${createBase}) as ${fullName}`; } // create the basic function declaration chunks.push(ts_poet_1.code ` decode( input: ${Reader} | Uint8Array, length?: number, ): ${fullName} { const reader = input instanceof ${Reader} ? input : new ${Reader}(input); let end = length === undefined ? reader.len : reader.pos + length; const message = ${createBase}; `); if (options.unknownFields) { chunks.push(ts_poet_1.code `(message as any)._unknownFields = {}`); } // start the tag loop chunks.push(ts_poet_1.code ` while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { `); // add a case for each incoming field messageDesc.field.forEach((field) => { const fieldName = case_1.maybeSnakeToCamel(field.name, options); chunks.push(ts_poet_1.code `case ${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.code `reader.${types_1.toReaderCall(field)}()`; if (types_1.isBytes(field)) { if (options.env === options_1.EnvOption.NODE) { readSnippet = ts_poet_1.code `${readSnippet} as Buffer`; } } else if (types_1.basicLongWireType(field.type) !== undefined) { if (options.forceLong === options_1.LongOption.LONG) { readSnippet = ts_poet_1.code `${readSnippet} as Long`; } else if (options.forceLong === options_1.LongOption.STRING) { readSnippet = ts_poet_1.code `${utils.longToString}(${readSnippet} as Long)`; } else { readSnippet = ts_poet_1.code `${utils.longToNumber}(${readSnippet} as Long)`; } } else if (types_1.isEnum(field)) { if (options.stringEnums) { const fromJson = types_1.getEnumMethod(ctx, field.typeName, 'FromJSON'); readSnippet = ts_poet_1.code `${fromJson}(${readSnippet})`; } else { readSnippet = ts_poet_1.code `${readSnippet} as any`; } } } else if (types_1.isValueType(ctx, field)) { const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); const unwrap = (decodedValue) => { if (types_1.isListValueType(field) || types_1.isStructType(field) || types_1.isAnyValueType(field) || types_1.isFieldMaskType(field)) { return ts_poet_1.code `${type}.unwrap(${decodedValue})`; } return ts_poet_1.code `${decodedValue}.value`; }; const decoder = ts_poet_1.code `${type}.decode(reader, reader.uint32())`; readSnippet = ts_poet_1.code `${unwrap(decoder)}`; } else if (types_1.isTimestamp(field) && (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.STRING)) { const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); readSnippet = ts_poet_1.code `${utils.fromTimestamp}(${type}.decode(reader, reader.uint32()))`; } else if (types_1.isObjectId(field) && options.useMongoObjectId) { const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); readSnippet = ts_poet_1.code `${utils.fromProtoObjectId}(${type}.decode(reader, reader.uint32()))`; } else if (types_1.isMessage(field)) { const type = types_1.basicTypeName(ctx, field); readSnippet = ts_poet_1.code `${type}.decode(reader, reader.uint32())`; } else { throw new Error(`Unhandled field ${field}`); } // and then use the snippet to handle repeated fields if necessary if (types_1.isRepeated(field)) { const maybeNonNullAssertion = ctx.options.useOptionals === 'all' ? '!' : ''; if (types_1.isMapType(ctx, messageDesc, field)) { // We need a unique const within the `cast` statement const varName = `entry${field.number}`; chunks.push(ts_poet_1.code ` const ${varName} = ${readSnippet}; if (${varName}.value !== undefined) { message.${fieldName}${maybeNonNullAssertion}[${varName}.key] = ${varName}.value; } `); } else if (types_1.packedType(field.type) === undefined) { chunks.push(ts_poet_1.code `message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet});`); } else { chunks.push(ts_poet_1.code ` if ((tag & 7) === 2) { const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet}); } } else { message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet}); } `); } } else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) { let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); chunks.push(ts_poet_1.code `message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet} };`); } else { chunks.push(ts_poet_1.code `message.${fieldName} = ${readSnippet};`); } chunks.push(ts_poet_1.code `break;`); }); if (options.unknownFields) { chunks.push(ts_poet_1.code ` default: const startPos = reader.pos; reader.skipType(tag & 7); (message as any)._unknownFields[tag] = [...((message as any)._unknownFields[tag] || []), reader.buf.slice(startPos, reader.pos)]; break; `); } else { chunks.push(ts_poet_1.code ` default: reader.skipType(tag & 7); break; `); } // and then wrap up the switch/while/return chunks.push(ts_poet_1.code `}`); chunks.push(ts_poet_1.code `}`); chunks.push(ts_poet_1.code `return message;`); chunks.push(ts_poet_1.code `}`); return ts_poet_1.joinCode(chunks, { on: '\n' }); } const Writer = ts_poet_1.imp('Writer@protobufjs/minimal'); const Reader = ts_poet_1.imp('Reader@protobufjs/minimal'); /** Creates a function to encode a message by loop overing the tags. */ function generateEncode(ctx, fullName, messageDesc) { const { options, utils, typeMap } = ctx; const chunks = []; // create the basic function declaration chunks.push(ts_poet_1.code ` encode( ${messageDesc.field.length > 0 || options.unknownFields ? 'message' : '_'}: ${fullName}, writer: ${Writer} = ${Writer}.create(), ): ${Writer} { `); // 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.isEnum(field) && options.stringEnums) { const tag = ((field.number << 3) | types_1.basicWireType(field.type)) >>> 0; const toNumber = types_1.getEnumMethod(ctx, field.typeName, 'ToNumber'); writeSnippet = (place) => ts_poet_1.code `writer.uint32(${tag}).${types_1.toReaderCall(field)}(${toNumber}(${place}))`; } else if (types_1.isScalar(field) || types_1.isEnum(field)) { const tag = ((field.number << 3) | types_1.basicWireType(field.type)) >>> 0; writeSnippet = (place) => ts_poet_1.code `writer.uint32(${tag}).${types_1.toReaderCall(field)}(${place})`; } else if (types_1.isObjectId(field) && options.useMongoObjectId) { const tag = ((field.number << 3) | 2) >>> 0; const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); writeSnippet = (place) => ts_poet_1.code `${type}.encode(${utils.toProtoObjectId}(${place}), writer.uint32(${tag}).fork()).ldelim()`; } else if (types_1.isTimestamp(field) && (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.STRING)) { const tag = ((field.number << 3) | 2) >>> 0; const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); writeSnippet = (place) => ts_poet_1.code `${type}.encode(${utils.toTimestamp}(${place}), writer.uint32(${tag}).fork()).ldelim()`; } else if (types_1.isValueType(ctx, field)) { const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : ''; const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); const wrappedValue = (place) => { if (types_1.isAnyValueType(field) || types_1.isListValueType(field) || types_1.isStructType(field) || types_1.isFieldMaskType(field)) { return ts_poet_1.code `${type}.wrap(${place})`; } return ts_poet_1.code `{${maybeTypeField} value: ${place}!}`; }; const tag = ((field.number << 3) | 2) >>> 0; writeSnippet = (place) => ts_poet_1.code `${type}.encode(${wrappedValue(place)}, writer.uint32(${tag}).fork()).ldelim()`; } else if (types_1.isMessage(field)) { const tag = ((field.number << 3) | 2) >>> 0; const type = types_1.basicTypeName(ctx, field); writeSnippet = (place) => ts_poet_1.code `${type}.encode(${place}, writer.uint32(${tag}).fork()).ldelim()`; } else { throw new Error(`Unhandled field ${field}`); } const isOptional = types_1.isOptionalProperty(field, messageDesc.options, options); if (types_1.isRepeated(field)) { if (types_1.isMapType(ctx, messageDesc, field)) { const valueType = typeMap.get(field.typeName)[2].field[1]; const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : ''; const entryWriteSnippet = types_1.isValueType(ctx, valueType) ? ts_poet_1.code ` if (value !== undefined) { ${writeSnippet(`{ ${maybeTypeField} key: key as any, value }`)}; } ` : writeSnippet(`{ ${maybeTypeField} key: key as any, value }`); const optionalAlternative = isOptional ? ' || {}' : ''; chunks.push(ts_poet_1.code ` Object.entries(message.${fieldName}${optionalAlternative}).forEach(([key, value]) => { ${entryWriteSnippet} }); `); } else if (types_1.packedType(field.type) === undefined) { const listWriteSnippet = ts_poet_1.code ` for (const v of message.${fieldName}) { ${writeSnippet('v!')}; } `; if (isOptional) { chunks.push(ts_poet_1.code ` if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { ${listWriteSnippet} } `); } else { chunks.push(listWriteSnippet); } } else if (types_1.isEnum(field) && options.stringEnums) { // This is a lot like the `else` clause, but we wrap `fooToNumber` around it. // Ideally we'd reuse `writeSnippet` here, but `writeSnippet` has the `writer.uint32(tag)` // embedded inside of it, and we want to drop that so that we can encode it packed // (i.e. just one tag and multiple values). const tag = ((field.number << 3) | 2) >>> 0; const toNumber = types_1.getEnumMethod(ctx, field.typeName, 'ToNumber'); const listWriteSnippet = ts_poet_1.code ` writer.uint32(${tag}).fork(); for (const v of message.${fieldName}) { writer.${types_1.toReaderCall(field)}(${toNumber}(v)); } writer.ldelim(); `; if (isOptional) { chunks.push(ts_poet_1.code ` if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { ${listWriteSnippet} } `); } else { chunks.push(listWriteSnippet); } } else { // Ideally we'd reuse `writeSnippet` but it has tagging embedded inside of it. const tag = ((field.number << 3) | 2) >>> 0; const listWriteSnippet = ts_poet_1.code ` writer.uint32(${tag}).fork(); for (const v of message.${fieldName}) { writer.${types_1.toReaderCall(field)}(v); } writer.ldelim(); `; if (isOptional) { chunks.push(ts_poet_1.code ` if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { ${listWriteSnippet} } `); } else { chunks.push(listWriteSnippet); } } } else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) { let oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); chunks.push(ts_poet_1.code ` if (message.${oneofName}?.$case === '${fieldName}') { ${writeSnippet(`message.${oneofName}.${fieldName}`)}; } `); } else if (types_1.isWithinOneOf(field)) { // Oneofs don't have a default value check b/c they need to denote which-oneof presence chunks.push(ts_poet_1.code ` if (message.${fieldName} !== undefined) { ${writeSnippet(`message.${fieldName}`)}; } `); } else if (types_1.isMessage(field)) { chunks.push(ts_poet_1.code ` if (message.${fieldName} !== undefined) { ${writeSnippet(`message.${fieldName}`)}; } `); } else if (types_1.isScalar(field) || types_1.isEnum(field)) { chunks.push(ts_poet_1.code ` if (${types_1.notDefaultCheck(ctx, field, messageDesc.options, `message.${fieldName}`)}) { ${writeSnippet(`message.${fieldName}`)}; } `); } else { chunks.push(ts_poet_1.code `${writeSnippet(`message.${fieldName}`)};`); } }); if (options.unknownFields) { chunks.push(ts_poet_1.code `if ('_unknownFields' in message) { for (const key of Object.keys(message['_unknownFields'])) { const values = message['_unknownFields'][key] as Uint8Array[]; for (const value of values) { writer.uint32(parseInt(key, 10)); (writer as any)['_push']( (val: Uint8Array, buf: Buffer, pos: number) => buf.set(val, pos), value.length, value ); } } }`); } chunks.push(ts_poet_1.code `return writer;`); chunks.push(ts_poet_1.code `}`); return ts_poet_1.joinCode(chunks, { on: '\n' }); } /** * 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(ctx, fullName, fullTypeName, messageDesc) { const { options, utils, typeMap } = ctx; const chunks = []; // create the basic function declaration chunks.push(ts_poet_1.code ` fromJSON(${messageDesc.field.length > 0 ? 'object' : '_'}: any): ${fullName} { return { `); if (ctx.options.outputTypeRegistry) { chunks.push(ts_poet_1.code `$type: ${fullName}.$type,`); } const oneofFieldsCases = messageDesc.oneofDecl.map((oneof, oneofIndex) => messageDesc.field.filter(types_1.isWithinOneOf).filter((field) => field.oneofIndex === oneofIndex)); const canonicalFromJson = { ['google.protobuf.FieldMask']: { paths: (from) => ts_poet_1.code `typeof(${from}) === 'string' ? ${from}.split(",").filter(Boolean) : Array.isArray(${from}?.paths) ? ${from}.paths.map(String) : []`, }, }; // add a check for each incoming field messageDesc.field.forEach((field) => { var _a; const fieldName = case_1.maybeSnakeToCamel(field.name, options); const jsonName = utils_1.getFieldJsonName(field, options); const jsonProperty = utils_1.getPropertyAccessor('object', jsonName); const jsonPropertyOptional = utils_1.getPropertyAccessor('object', jsonName, true); // get code that extracts value from incoming object const readSnippet = (from) => { if (types_1.isEnum(field)) { const fromJson = types_1.getEnumMethod(ctx, field.typeName, 'FromJSON'); return ts_poet_1.code `${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 === options_1.EnvOption.NODE) { return ts_poet_1.code `Buffer.from(${utils.bytesFromBase64}(${from}))`; } else { return ts_poet_1.code `${utils.bytesFromBase64}(${from})`; } } else if (types_1.isLong(field) && options.forceLong === options_1.LongOption.LONG) { const cstr = case_1.capitalize(types_1.basicTypeName(ctx, field, { keepValueType: true }).toCodeString()); return ts_poet_1.code `${cstr}.fromValue(${from})`; } else { const cstr = case_1.capitalize(types_1.basicTypeName(ctx, field, { keepValueType: true }).toCodeString()); return ts_poet_1.code `${cstr}(${from})`; } } else if (types_1.isObjectId(field) && options.useMongoObjectId) { return ts_poet_1.code `${utils.fromJsonObjectId}(${from})`; } else if (types_1.isTimestamp(field) && options.useDate === options_1.DateOption.STRING) { return ts_poet_1.code `String(${from})`; } else if (types_1.isTimestamp(field) && (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.TIMESTAMP)) { return ts_poet_1.code `${utils.fromJsonTimestamp}(${from})`; } else if (types_1.isAnyValueType(field) || types_1.isStructType(field)) { return ts_poet_1.code `${from}`; } else if (types_1.isFieldMaskType(field)) { const type = types_1.basicTypeName(ctx, field, { keepValueType: true }); return ts_poet_1.code `${type}.unwrap(${type}.fromJSON(${from}))`; } else if (types_1.isListValueType(field)) { return ts_poet_1.code `[...${from}]`; } else if (types_1.isValueType(ctx, field)) { const valueType = types_1.valueTypeName(ctx, field.typeName); if (types_1.isLongValueType(field) && options.forceLong === options_1.LongOption.LONG) { return ts_poet_1.code `${case_1.capitalize(valueType.toCodeString())}.fromValue(${from})`; } else if (types_1.isBytesValueType(field)) { return ts_poet_1.code `new ${case_1.capitalize(valueType.toCodeString())}(${from})`; } else { return ts_poet_1.code `${case_1.capitalize(valueType.toCodeString())}(${from})`; } } else if (types_1.isMessage(field)) { if (types_1.isRepeated(field) && types_1.isMapType(ctx, messageDesc, field)) { const { valueField, valueType } = types_1.detectMapType(ctx, messageDesc, field); if (types_1.isPrimitive(valueField)) { // TODO Can we not copy/paste this from ^? if (types_1.isBytes(valueField)) { if (options.env === options_1.EnvOption.NODE) { return ts_poet_1.code `Buffer.from(${utils.bytesFromBase64}(${from} as string))`; } else { return ts_poet_1.code `${utils.bytesFromBase64}(${from} as string)`; } } else if (types_1.isLong(valueField) && options.forceLong === options_1.LongOption.LONG) { return ts_poet_1.code `Long.fromValue(${from} as Long | string)`; } else if (types_1.isEnum(valueField)) { const fromJson = types_1.getEnumMethod(ctx, valueField.typeName, 'FromJSON'); return ts_poet_1.code `${fromJson}(${from})`; } else { const cstr = case_1.capitalize(valueType.toCodeString()); return ts_poet_1.code `${cstr}(${from})`; }