ts-proto
Version:
[](https://www.npmjs.com/package/ts-proto) [](https://github.com/stephenh/ts-proto/actions)
1,011 lines • 82.8 kB
JavaScript
"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 generate_async_iterable_1 = require("./generate-async-iterable");
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((0, 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);
(0, 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
(0, visit_1.visit)(fileDesc, sourceInfo, (fullName, message, sInfo, fullProtoTypeName) => {
chunks.push(generateInterfaceDeclaration(ctx, fullName, message, sInfo, (0, utils_1.maybePrefixPackage)(fileDesc, fullProtoTypeName)));
}, options, (fullName, enumDesc, sInfo) => {
chunks.push((0, enums_1.generateEnum)(ctx, fullName, enumDesc, sInfo));
});
// If nestJs=true export [package]_PACKAGE_NAME and [service]_SERVICE_NAME const
if (options.nestJs) {
const prefix = (0, case_1.camelToSnake)(fileDesc.package.replace(/\./g, "_"));
chunks.push((0, ts_poet_1.code) `export const ${prefix}_PACKAGE_NAME = '${fileDesc.package}';`);
if (options.useDate === options_1.DateOption.DATE &&
fileDesc.messageType.find((message) => message.field.find((field) => field.typeName === ".google.protobuf.Timestamp"))) {
chunks.push(makeProtobufTimestampWrapper());
}
}
if (options.outputEncodeMethods || options.outputJsonMethods || options.outputTypeRegistry) {
// then add the encoder/decoder/base instance
(0, visit_1.visit)(fileDesc, sourceInfo, (fullName, message, _sInfo, fullProtoTypeName) => {
const fullTypeName = (0, utils_1.maybePrefixPackage)(fileDesc, fullProtoTypeName);
chunks.push(generateBaseInstanceFactory(ctx, fullName, message, fullTypeName));
const staticMembers = [];
if (options.outputTypeRegistry) {
staticMembers.push((0, 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.useAsyncIterable) {
staticMembers.push((0, generate_async_iterable_1.generateEncodeTransform)(fullName));
staticMembers.push((0, generate_async_iterable_1.generateDecodeTransform)(fullName));
}
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: (0, case_1.maybeSnakeToCamel)("null_value", ctx.options),
numberValue: (0, case_1.maybeSnakeToCamel)("number_value", ctx.options),
stringValue: (0, case_1.maybeSnakeToCamel)("string_value", ctx.options),
boolValue: (0, case_1.maybeSnakeToCamel)("bool_value", ctx.options),
structValue: (0, case_1.maybeSnakeToCamel)("struct_value", ctx.options),
listValue: (0, case_1.maybeSnakeToCamel)("list_value", ctx.options),
};
staticMembers.push(...generateWrap(ctx, fullTypeName, structFieldNames));
staticMembers.push(...generateUnwrap(ctx, fullTypeName, structFieldNames));
chunks.push((0, ts_poet_1.code) `
export const ${(0, ts_poet_1.def)(fullName)} = {
${(0, ts_poet_1.joinCode)(staticMembers, { on: ",\n\n" })}
};
`);
if (options.outputTypeRegistry) {
const messageTypeRegistry = (0, utils_1.impFile)(options, "messageTypeRegistry@./typeRegistry");
chunks.push((0, ts_poet_1.code) `
${messageTypeRegistry}.set(${fullName}.$type, ${fullName});
`);
}
}, options);
}
let hasServerStreamingMethods = false;
let hasStreamingMethods = false;
(0, 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((0, generate_nestjs_1.generateNestjsServiceClient)(ctx, fileDesc, sInfo, serviceDesc));
// and the service controller interface
chunks.push((0, generate_nestjs_1.generateNestjsServiceController)(ctx, fileDesc, sInfo, serviceDesc));
// generate nestjs grpc service controller decorator
chunks.push((0, generate_nestjs_1.generateNestjsGrpcServiceMethodsDecorator)(ctx, serviceDesc));
let serviceConstName = `${(0, case_1.camelToSnake)(serviceDesc.name)}_NAME`;
if (!serviceDesc.name.toLowerCase().endsWith("service")) {
serviceConstName = `${(0, case_1.camelToSnake)(serviceDesc.name)}_SERVICE_NAME`;
}
chunks.push((0, 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((0, generate_grpc_js_1.generateGrpcJsService)(ctx, fileDesc, sInfo, serviceDesc));
}
else if (outputService === options_1.ServiceOption.NICE_GRPC) {
chunks.push((0, generate_nice_grpc_1.generateNiceGrpcService)(ctx, fileDesc, sInfo, serviceDesc));
}
else if (outputService === options_1.ServiceOption.GENERIC) {
chunks.push((0, 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((0, generate_services_1.generateService)(ctx, fileDesc, sInfo, serviceDesc));
if (options.outputClientImpl === true) {
chunks.push((0, generate_services_1.generateServiceClientImpl)(ctx, fileDesc, serviceDesc));
}
else if (options.outputClientImpl === "grpc-web") {
chunks.push((0, generate_grpc_web_1.generateGrpcClientImpl)(ctx, fileDesc, serviceDesc));
chunks.push((0, generate_grpc_web_1.generateGrpcServiceDesc)(fileDesc, serviceDesc));
serviceDesc.method.forEach((method) => {
if (!method.clientStreaming) {
chunks.push((0, 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((0, generate_services_1.generateRpcType)(ctx, hasStreamingMethods));
}
else if (options.outputClientImpl === "grpc-web") {
chunks.push((0, generate_grpc_web_1.addGrpcWebMisc)(ctx, hasServerStreamingMethods));
}
}
if (options.context) {
chunks.push((0, generate_services_1.generateDataLoaderOptionsType)());
chunks.push((0, generate_services_1.generateDataLoadersType)());
}
if (options.outputSchema) {
chunks.push(...(0, schema_1.generateSchema)(ctx, fileDesc, sourceInfo));
}
chunks.push(...Object.values(utils).map((v) => {
if (v instanceof ConditionalOutput_1.ConditionalOutput) {
return (0, ts_poet_1.code) `${v.ifUsed}`;
}
else {
return (0, 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];
(0, utils_1.assertInstanceOf)(methodInfo, utils_1.FormattedMethodDescriptor);
svc.method[i] = methodInfo.getSource();
}
}
return [moduleName, (0, 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(),
...makeTimestampMethods(options, longs),
...longs,
...makeComparisonUtils(),
...makeNiceGrpcServerStreamingMethodResult(),
...makeGrpcWebErrorClass(),
};
}
exports.makeUtils = makeUtils;
function makeProtobufTimestampWrapper() {
const wrappers = (0, ts_poet_1.imp)("wrappers@protobufjs");
return (0, ts_poet_1.code) `
${wrappers}['.google.protobuf.Timestamp'] = {
fromObject(value: Date) {
return {
seconds: value.getTime() / 1000,
nanos: (value.getTime() % 1000) * 1e6,
};
},
toObject(message: { seconds: number; nanos: number }) {
return new Date(message.seconds * 1000 + message.nanos / 1e6);
},
} as any;`;
}
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 = (0, utils_1.impFile)(options, "util@protobufjs/minimal");
const configure = (0, utils_1.impFile)(options, "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 LongImp = options.esModuleInterop ? (0, ts_poet_1.imp)("Long=long") : (0, 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'.`;
// Instead of exposing `LongImp` directly, let callers think that they are getting the
// `imp(Long)` but really it is that + our long initialization snippet. This means the
// initialization code will only be emitted in files that actually use the Long import.
const Long = (0, ts_poet_1.conditionalOutput)("Long", (0, ts_poet_1.code) `
${disclaimer}
if (${util}.Long !== ${LongImp}) {
${util}.Long = ${LongImp} as any;
${configure}();
}
`);
// TODO This is unused?
const numberToLong = (0, ts_poet_1.conditionalOutput)("numberToLong", (0, ts_poet_1.code) `
function numberToLong(number: number) {
return ${Long}.fromNumber(number);
}
`);
const longToString = (0, ts_poet_1.conditionalOutput)("longToString", (0, ts_poet_1.code) `
function longToString(long: ${Long}) {
return long.toString();
}
`);
const longToNumber = (0, ts_poet_1.conditionalOutput)("longToNumber", (0, 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, Long };
}
function makeByteUtils() {
const globalThis = (0, ts_poet_1.conditionalOutput)("globalThis", (0, 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 = (0, ts_poet_1.conditionalOutput)("bytesFromBase64", (0, ts_poet_1.code) `
function bytesFromBase64(b64: string): Uint8Array {
if (${globalThis}.Buffer) {
return Uint8Array.from(${globalThis}.Buffer.from(b64, 'base64'));
} else {
const bin = ${globalThis}.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 = (0, ts_poet_1.conditionalOutput)("base64FromBytes", (0, ts_poet_1.code) `
function base64FromBytes(arr: Uint8Array): string {
if (${globalThis}.Buffer) {
return ${globalThis}.Buffer.from(arr).toString('base64')
} else {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(String.fromCharCode(byte));
});
return ${globalThis}.btoa(bin.join(''));
}
}
`);
return { globalThis, bytesFromBase64, base64FromBytes };
}
function makeDeepPartial(options, longs) {
let oneofCase = "";
if (options.oneof === options_1.OneofOption.UNIONS) {
oneofCase = `
: T extends { ${maybeReadonly(options)}$case: string }
? { [K in keyof Omit<T, '$case'>]?: DeepPartial<T[K]> } & { ${maybeReadonly(options)}$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 ? (0, ts_poet_1.code) ` : T extends ${longs.Long} ? string | number | Long ` : "";
const Builtin = (0, ts_poet_1.conditionalOutput)("Builtin", (0, 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 = (0, ts_poet_1.conditionalOutput)("Exact", (0, 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]> } & { [K in Exclude<keyof I, KeysOfUnion<P> ${maybeExcludeType}>]: never };
`);
// Based on the type from ts-essentials
const keys = options.outputTypeRegistry ? (0, ts_poet_1.code) `Exclude<keyof T, '$type'>` : (0, ts_poet_1.code) `keyof T`;
const DeepPartial = (0, ts_poet_1.conditionalOutput)("DeepPartial", (0, 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() {
const mongodb = (0, ts_poet_1.imp)("mongodb*mongodb");
const fromProtoObjectId = (0, ts_poet_1.conditionalOutput)("fromProtoObjectId", (0, ts_poet_1.code) `
function fromProtoObjectId(oid: ObjectId): ${mongodb}.ObjectId {
return new ${mongodb}.ObjectId(oid.value);
}
`);
const fromJsonObjectId = (0, ts_poet_1.conditionalOutput)("fromJsonObjectId", (0, 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 = (0, ts_poet_1.conditionalOutput)("toProtoObjectId", (0, 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 = (0, 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 = (0, 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 = (0, ts_poet_1.conditionalOutput)("toTimestamp", options.useDate === options_1.DateOption.STRING
? (0, 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 };
}
`
: (0, 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 = (0, ts_poet_1.conditionalOutput)("fromTimestamp", options.useDate === options_1.DateOption.STRING
? (0, 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();
}
`
: (0, 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 = (0, ts_poet_1.conditionalOutput)("fromJsonTimestamp", options.useDate === options_1.DateOption.DATE
? (0, 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));
}
}
`
: (0, 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 = (0, ts_poet_1.conditionalOutput)("isObject", (0, ts_poet_1.code) `
function isObject(value: any): boolean {
return typeof value === 'object' && value !== null;
}`);
const isSet = (0, ts_poet_1.conditionalOutput)("isSet", (0, ts_poet_1.code) `
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}`);
return { isObject, isSet };
}
function makeNiceGrpcServerStreamingMethodResult() {
const NiceGrpcServerStreamingMethodResult = (0, ts_poet_1.conditionalOutput)("ServerStreamingMethodResult", (0, ts_poet_1.code) `
export type ServerStreamingMethodResult<Response> = {
[Symbol.asyncIterator](): AsyncIterator<Response, void>;
};
`);
return { NiceGrpcServerStreamingMethodResult };
}
function makeGrpcWebErrorClass() {
const GrpcWebError = (0, ts_poet_1.conditionalOutput)("GrpcWebError", (0, ts_poet_1.code) `
export class GrpcWebError extends globalThis.Error {
constructor(message: string, public code: grpc.Code, public metadata: grpc.Metadata) {
super(message);
}
}
`);
return { GrpcWebError };
}
// Create the interface with properties
function generateInterfaceDeclaration(ctx, fullName, messageDesc, sourceInfo, fullTypeName) {
var _a;
const { options } = ctx;
const chunks = [];
(0, 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((0, ts_poet_1.code) `export interface ${(0, ts_poet_1.def)(fullName)} {`);
if (ctx.options.outputTypeRegistry) {
chunks.push((0, 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 ((0, 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);
(0, utils_1.maybeAddComment)(info, chunks, (_a = fieldDesc.options) === null || _a === void 0 ? void 0 : _a.deprecated);
const name = (0, case_1.maybeSnakeToCamel)(fieldDesc.name, options);
const type = (0, types_1.toTypeName)(ctx, messageDesc, fieldDesc);
const q = (0, types_1.isOptionalProperty)(fieldDesc, messageDesc.options, options) ? "?" : "";
chunks.push((0, ts_poet_1.code) `${maybeReadonly(options)}${name}${q}: ${type}, `);
});
chunks.push((0, ts_poet_1.code) `}`);
return (0, ts_poet_1.joinCode)(chunks, { on: "\n" });
}
function generateOneofProperty(ctx, messageDesc, oneofIndex, sourceInfo) {
const { options } = ctx;
const fields = messageDesc.field.filter((field) => (0, types_1.isWithinOneOf)(field) && field.oneofIndex === oneofIndex);
const mbReadonly = maybeReadonly(options);
const unionType = (0, ts_poet_1.joinCode)(fields.map((f) => {
let fieldName = (0, case_1.maybeSnakeToCamel)(f.name, options);
let typeName = (0, types_1.toTypeName)(ctx, messageDesc, f);
return (0, ts_poet_1.code) `{ ${mbReadonly}$case: '${fieldName}', ${mbReadonly}${fieldName}: ${typeName} }`;
}), { on: " | " });
const name = (0, case_1.maybeSnakeToCamel)(messageDesc.oneofDecl[oneofIndex].name, options);
return (0, ts_poet_1.code) `${mbReadonly}${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 { options } = ctx;
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 ((0, types_1.isWithinOneOfThatShouldBeUnion)(ctx.options, field)) {
const { oneofIndex } = field;
if (!processedOneofs.has(oneofIndex)) {
processedOneofs.add(oneofIndex);
const name = (0, case_1.maybeSnakeToCamel)(messageDesc.oneofDecl[oneofIndex].name, ctx.options);
fields.push((0, ts_poet_1.code) `${name}: undefined`);
}
continue;
}
if (!options.initializeFieldsAsUndefined && (0, types_1.isOptionalProperty)(field, messageDesc.options, options)) {
continue;
}
const name = (0, case_1.maybeSnakeToCamel)(field.name, ctx.options);
const val = (0, types_1.isWithinOneOf)(field)
? "undefined"
: (0, types_1.isMapType)(ctx, messageDesc, field)
? ctx.options.useMapType
? "new Map()"
: "{}"
: (0, types_1.isRepeated)(field)
? "[]"
: (0, types_1.defaultValue)(ctx, field);
fields.push((0, ts_poet_1.code) `${name}: ${val}`);
}
if (ctx.options.outputTypeRegistry) {
fields.unshift((0, ts_poet_1.code) `$type: '${fullTypeName}'`);
}
return (0, ts_poet_1.code) `
function createBase${fullName}(): ${fullName} {
return { ${(0, 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 = (0, ts_poet_1.code) `createBase${fullName}()`;
if (options.usePrototypeForDefaults) {
createBase = (0, ts_poet_1.code) `Object.create(${createBase}) as ${fullName}`;
}
const Reader = (0, utils_1.impFile)(ctx.options, "Reader@protobufjs/minimal");
// create the basic function declaration
chunks.push((0, 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;
`);
chunks.push((0, ts_poet_1.code) `const message = ${createBase}${maybeAsAny(options)};`);
if (options.unknownFields) {
chunks.push((0, ts_poet_1.code) `(message as any)._unknownFields = {}`);
}
// start the tag loop
chunks.push((0, 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 = (0, case_1.maybeSnakeToCamel)(field.name, options);
chunks.push((0, ts_poet_1.code) `case ${field.number}:`);
// get a generic 'reader.doSomething' bit that is specific to the basic type
let readSnippet;
if ((0, types_1.isPrimitive)(field)) {
readSnippet = (0, ts_poet_1.code) `reader.${(0, types_1.toReaderCall)(field)}()`;
if ((0, types_1.isBytes)(field)) {
if (options.env === options_1.EnvOption.NODE) {
readSnippet = (0, ts_poet_1.code) `${readSnippet} as Buffer`;
}
}
else if ((0, types_1.basicLongWireType)(field.type) !== undefined) {
if (options.forceLong === options_1.LongOption.LONG) {
readSnippet = (0, ts_poet_1.code) `${readSnippet} as Long`;
}
else if (options.forceLong === options_1.LongOption.STRING) {
readSnippet = (0, ts_poet_1.code) `${utils.longToString}(${readSnippet} as Long)`;
}
else {
readSnippet = (0, ts_poet_1.code) `${utils.longToNumber}(${readSnippet} as Long)`;
}
}
else if ((0, types_1.isEnum)(field)) {
if (options.stringEnums) {
const fromJson = (0, types_1.getEnumMethod)(ctx, field.typeName, "FromJSON");
readSnippet = (0, ts_poet_1.code) `${fromJson}(${readSnippet})`;
}
else {
readSnippet = (0, ts_poet_1.code) `${readSnippet} as any`;
}
}
}
else if ((0, types_1.isValueType)(ctx, field)) {
const type = (0, types_1.basicTypeName)(ctx, field, { keepValueType: true });
const unwrap = (decodedValue) => {
if ((0, types_1.isListValueType)(field) || (0, types_1.isStructType)(field) || (0, types_1.isAnyValueType)(field) || (0, types_1.isFieldMaskType)(field)) {
return (0, ts_poet_1.code) `${type}.unwrap(${decodedValue})`;
}
return (0, ts_poet_1.code) `${decodedValue}.value`;
};
const decoder = (0, ts_poet_1.code) `${type}.decode(reader, reader.uint32())`;
readSnippet = (0, ts_poet_1.code) `${unwrap(decoder)}`;
}
else if ((0, types_1.isTimestamp)(field) && (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.STRING)) {
const type = (0, types_1.basicTypeName)(ctx, field, { keepValueType: true });
readSnippet = (0, ts_poet_1.code) `${utils.fromTimestamp}(${type}.decode(reader, reader.uint32()))`;
}
else if ((0, types_1.isObjectId)(field) && options.useMongoObjectId) {
const type = (0, types_1.basicTypeName)(ctx, field, { keepValueType: true });
readSnippet = (0, ts_poet_1.code) `${utils.fromProtoObjectId}(${type}.decode(reader, reader.uint32()))`;
}
else if ((0, types_1.isMessage)(field)) {
const type = (0, types_1.basicTypeName)(ctx, field);
readSnippet = (0, 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 ((0, types_1.isRepeated)(field)) {
const maybeNonNullAssertion = ctx.options.useOptionals === "all" ? "!" : "";
if ((0, types_1.isMapType)(ctx, messageDesc, field)) {
// We need a unique const within the `cast` statement
const varName = `entry${field.number}`;
const valueSetterSnippet = ctx.options.useMapType
? `message.${fieldName}${maybeNonNullAssertion}.set(${varName}.key, ${varName}.value)`
: `message.${fieldName}${maybeNonNullAssertion}[${varName}.key] = ${varName}.value`;
chunks.push((0, ts_poet_1.code) `
const ${varName} = ${readSnippet};
if (${varName}.value !== undefined) {
${valueSetterSnippet};
}
`);
}
else if ((0, types_1.packedType)(field.type) === undefined) {
chunks.push((0, ts_poet_1.code) `message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet});`);
}
else {
chunks.push((0, 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 ((0, types_1.isWithinOneOfThatShouldBeUnion)(options, field)) {
let oneofName = (0, case_1.maybeSnakeToCamel)(messageDesc.oneofDecl[field.oneofIndex].name, options);
chunks.push((0, ts_poet_1.code) `message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet} };`);
}
else {
chunks.push((0, ts_poet_1.code) `message.${fieldName} = ${readSnippet};`);
}
chunks.push((0, ts_poet_1.code) `break;`);
});
if (options.unknownFields) {
chunks.push((0, 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((0, ts_poet_1.code) `
default:
reader.skipType(tag & 7);
break;
`);
}
// and then wrap up the switch/while/return
chunks.push((0, ts_poet_1.code) `}`);
chunks.push((0, ts_poet_1.code) `}`);
chunks.push((0, ts_poet_1.code) `return message;`);
chunks.push((0, ts_poet_1.code) `}`);
return (0, ts_poet_1.joinCode)(chunks, { on: "\n" });
}
/** Creates a function to encode a message by loop overing the tags. */
function generateEncode(ctx, fullName, messageDesc) {
const { options, utils, typeMap } = ctx;
const chunks = [];
const Writer = (0, utils_1.impFile)(ctx.options, "Writer@protobufjs/minimal");
// create the basic function declaration
chunks.push((0, 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 = (0, case_1.maybeSnakeToCamel)(field.name, options);
// get a generic writer.doSomething based on the basic type
let writeSnippet;
if ((0, types_1.isEnum)(field) && options.stringEnums) {
const tag = ((field.number << 3) | (0, types_1.basicWireType)(field.type)) >>> 0;
const toNumber = (0, types_1.getEnumMethod)(ctx, field.typeName, "ToNumber");
writeSnippet = (place) => (0, ts_poet_1.code) `writer.uint32(${tag}).${(0, types_1.toReaderCall)(field)}(${toNumber}(${place}))`;
}
else if ((0, types_1.isScalar)(field) || (0, types_1.isEnum)(field)) {
const tag = ((field.number << 3) | (0, types_1.basicWireType)(field.type)) >>> 0;
writeSnippet = (place) => (0, ts_poet_1.code) `writer.uint32(${tag}).${(0, types_1.toReaderCall)(field)}(${place})`;
}
else if ((0, types_1.isObjectId)(field) && options.useMongoObjectId) {
const tag = ((field.number << 3) | 2) >>> 0;
const type = (0, types_1.basicTypeName)(ctx, field, { keepValueType: true });
writeSnippet = (place) => (0, ts_poet_1.code) `${type}.encode(${utils.toProtoObjectId}(${place}), writer.uint32(${tag}).fork()).ldelim()`;
}
else if ((0, 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 = (0, types_1.basicTypeName)(ctx, field, { keepValueType: true });
writeSnippet = (place) => (0, ts_poet_1.code) `${type}.encode(${utils.toTimestamp}(${place}), writer.uint32(${tag}).fork()).ldelim()`;
}
else if ((0, types_1.isValueType)(ctx, field)) {
const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : "";
const type = (0, types_1.basicTypeName)(ctx, field, { keepValueType: true });
const wrappedValue = (place) => {
if ((0, types_1.isAnyValueType)(field) || (0, types_1.isListValueType)(field) || (0, types_1.isStructType)(field) || (0, types_1.isFieldMaskType)(field)) {
return (0, ts_poet_1.code) `${type}.wrap(${place})`;
}
return (0, ts_poet_1.code) `{${maybeTypeField} value: ${place}!}`;
};
const tag = ((field.number << 3) | 2) >>> 0;
writeSnippet = (place) => (0, ts_poet_1.code) `${type}.encode(${wrappedValue(place)}, writer.uint32(${tag}).fork()).ldelim()`;
}
else if ((0, types_1.isMessage)(field)) {
const tag = ((field.number << 3) | 2) >>> 0;
const type = (0, types_1.basicTypeName)(ctx, field);
writeSnippet = (place) => (0, ts_poet_1.code) `${type}.encode(${place}, writer.uint32(${tag}).fork()).ldelim()`;
}
else {
throw new Error(`Unhandled field ${field}`);
}
const isOptional = (0, types_1.isOptionalProperty)(field, messageDesc.options, options);
if ((0, types_1.isRepeated)(field)) {
if ((0, 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 = (0, types_1.isValueType)(ctx, valueType)
? (0, ts_poet_1.code) `
if (value !== undefined) {
${writeSnippet(`{ ${maybeTypeField} key: key as any, value }`)};
}
`
: writeSnippet(`{ ${maybeTypeField} key: key as any, value }`);
const optionalAlternative = isOptional ? " || {}" : "";
if (ctx.options.useMapType) {
chunks.push((0, ts_poet_1.code) `
message.${fieldName}${optionalAlternative}.forEach((value, key) => {
${entryWriteSnippet}
});
`);
}
else {
chunks.push((0, ts_poet_1.code) `
Object.entries(message.${fieldName}${optionalAlternative}).forEach(([key, value]) => {
${entryWriteSnippet}
});
`);
}
}
else if ((0, types_1.packedType)(field.type) === undefined) {
const listWriteSnippet = (0, ts_poet_1.code) `
for (const v of message.${fieldName}) {
${writeSnippet("v!")};
}
`;
if (isOptional) {
chunks.push((0, ts_poet_1.code) `
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
${listWriteSnippet}
}
`);
}
else {
chunks.push(listWriteSnippet);
}
}
else if ((0, 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 = (0, types_1.getEnumMethod)(ctx, field.typeName, "ToNumber");
const listWriteSnippet = (0, ts_poet_1.code) `
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${(0, types_1.toReaderCall)(field)}(${toNumber}(v));
}
writer.ldelim();
`;
if (isOptional) {
chunks.push((0, 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 = (0, ts_poet_1.code) `
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${(0, types_1.toReaderCall)(field)}(v);
}
writer.ldelim();
`;
if (isOptional) {
chunks.push((0, ts_poet_1.code) `
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
${listWriteSnippet}
}
`);
}
else {
chunks.push(listWriteSnippet);
}
}
}
else if ((0, types_1.isWithinOneOfThatShouldBeUnion)(options, field)) {
let oneofName = (0, case_1.maybeSnakeToCamel)(messageDesc.oneofDecl[field.oneofIndex].name, options);
chunks.push((0, ts_poet_1.code) `
if (message.${oneofName}?.$case === '${fieldName}') {
${writeSnippet(`message.${oneofName}.${fieldName}`)};
}
`);
}
else if ((0, types_1.isWithinOneOf)(field)) {
// Oneofs don't have a default value check b/c they need to denote which-oneof presence
chunks.push((0, ts_poet_1.code) `
if (message.${fieldName} !== undefined) {
${writeSnippet(`message.${fieldName}`)};
}
`);
}
else if ((0, types_1.isMessage)(field)) {
chunks.push((0, ts_poet_1.code) `
if (message.${fieldName} !== undefined) {
${writeSnippet(`message.${fieldName}`)};
}
`);
}
else if ((0, types_1.isScalar)(field) || (0, types_1.isEnum)(field)) {
chunks.push((0, ts_poet_1.code) `
if (${(0, types_1.notDefaultCheck)(ctx, field, messageDesc.options, `message.${fieldName}`)}) {
${writeSnippet(`message.${fieldName}`)};
}
`);
}
else {
chunks.push((0, ts_poet_1.code) `${writeSnippet(`message.${fieldName}`)};`);
}
});
if (options.unknownFields) {
chunks.push((0, ts_poet_1.code) `if ('_unknownFields' in message) {
const msgUnknownFields: any = (message as any)['_unknownFields']
for (const key of Object.keys(msgUnknownFields)) {
const values = msgUnknownFields[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((0, ts_poet_1.code) `return writer;`);
chunks.push((0, ts_poet_1.code) `}`);
return (0, 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((0, ts_poet_1.code) `
fromJSON(${messageDesc.field.length > 0 ? "object" : "_"}: any): ${fullName} {
return {
`);
if (ctx.options.outputTypeRegistry) {
chunks.push((0, 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) => (0, 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 = (0, case_1.maybeSnakeToCamel)(field.name, options);
const jsonName = (0, utils_1.getFieldJsonName)(field, options);
const jsonProperty = (0, utils_1.getPropertyAccessor)("object", jsonName);
const jsonPropertyOptional = (0, utils_1.getPropertyAccessor)("object", jsonName, true);
// get code that extracts value from incoming object
const readSnippet = (from) => {
if ((0, types_1.isEnum)(field)) {
const fromJson = (0, types_1.getEnumMethod)(ctx, field.typeName, "FromJSON");
return (0, ts_poet_1.code) `${fromJson}(${from})`;
}
else if ((0, types_1.isPrimitive)(field)) {
// Convert primitives using the String(value)/Number(value)/bytesFromBase64(value)
if ((0, types_1.isBytes)(field)) {
if (options.env === options_1.EnvOption.NODE) {
return (0, ts_poet_1.code) `Buffer.from(${utils.bytesFromBase64}(${from}))`;
}
else {
return (0, ts_poet_1.code) `${utils.bytesFro