diff --git a/README.markdown b/README.markdown index 41fd67635..28ccc375b 100644 --- a/README.markdown +++ b/README.markdown @@ -348,6 +348,8 @@ Generated code will be placed in the Gradle build directory. - With `--ts_proto_opt=emitImportedFiles=false`, ts-proto will not emit `google/protobuf/*` files unless you explicit add files to `protoc` like this `protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto my_message.proto google/protobuf/duration.proto` +- With `--ts_proto_opt=fileSuffix=`, ts-proto will emit generated files using the specified suffix. A `helloworld.proto` file with `fileSuffix=.pb` would be generated as `helloworld.pb.ts`. This is common behavior in other protoc plugins and provides a way to quickly glob all the generated files. + ### Only Types If you're looking for `ts-proto` to generate only types for your Protobuf types then passing all three of `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` as `false` is probably what you want, i.e.: diff --git a/integration/file-suffix/child.bin b/integration/file-suffix/child.bin new file mode 100644 index 000000000..7c9268976 Binary files /dev/null and b/integration/file-suffix/child.bin differ diff --git a/integration/file-suffix/child.pb.ts b/integration/file-suffix/child.pb.ts new file mode 100644 index 000000000..32e1b9ce9 --- /dev/null +++ b/integration/file-suffix/child.pb.ts @@ -0,0 +1,112 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; + +export const protobufPackage = 'file_suffix'; + +export enum ChildEnum { + DEFAULT = 0, + FOO = 1, + UNRECOGNIZED = -1, +} + +export function childEnumFromJSON(object: any): ChildEnum { + switch (object) { + case 0: + case 'DEFAULT': + return ChildEnum.DEFAULT; + case 1: + case 'FOO': + return ChildEnum.FOO; + case -1: + case 'UNRECOGNIZED': + default: + return ChildEnum.UNRECOGNIZED; + } +} + +export function childEnumToJSON(object: ChildEnum): string { + switch (object) { + case ChildEnum.DEFAULT: + return 'DEFAULT'; + case ChildEnum.FOO: + return 'FOO'; + default: + return 'UNKNOWN'; + } +} + +export interface Child { + name: string; +} + +const baseChild: object = { name: '' }; + +export const Child = { + encode(message: Child, writer: Writer = Writer.create()): Writer { + if (message.name !== '') { + writer.uint32(10).string(message.name); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Child { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseChild } as Child; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.name = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Child { + const message = { ...baseChild } as Child; + message.name = object.name !== undefined && object.name !== null ? String(object.name) : ''; + return message; + }, + + toJSON(message: Child): unknown { + const obj: any = {}; + message.name !== undefined && (obj.name = message.name); + return obj; + }, + + fromPartial, I>>(object: I): Child { + const message = { ...baseChild } as Child; + message.name = object.name ?? ''; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & Record>, never>; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/integration/file-suffix/child.proto b/integration/file-suffix/child.proto new file mode 100644 index 000000000..40244dd55 --- /dev/null +++ b/integration/file-suffix/child.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package file_suffix; + +message Child { + string name = 1; +} + +enum ChildEnum { + DEFAULT = 0; + FOO = 1; +} diff --git a/integration/file-suffix/file-suffix-test.ts b/integration/file-suffix/file-suffix-test.ts new file mode 100644 index 000000000..6630ad0aa --- /dev/null +++ b/integration/file-suffix/file-suffix-test.ts @@ -0,0 +1,15 @@ +import { Parent } from './parent.pb'; +import { ChildEnum } from './child.pb'; + +describe('file-suffix', () => { + it('generates types correctly', () => { + const parent: Parent = { + child: { + name: 'child name' + }, + childEnum: ChildEnum.FOO, + createdAt: new Date('2020-01-01T00:00:00.000Z'), + } + expect(parent.child?.name).toEqual('child name'); + }); +}); diff --git a/integration/file-suffix/google/protobuf/timestamp.pb.ts b/integration/file-suffix/google/protobuf/timestamp.pb.ts new file mode 100644 index 000000000..0670c17fd --- /dev/null +++ b/integration/file-suffix/google/protobuf/timestamp.pb.ts @@ -0,0 +1,212 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; + +export const protobufPackage = 'google.protobuf'; + +/** + * A Timestamp represents a point in time independent of any time zone or local + * calendar, encoded as a count of seconds and fractions of seconds at + * nanosecond resolution. The count is relative to an epoch at UTC midnight on + * January 1, 1970, in the proleptic Gregorian calendar which extends the + * Gregorian calendar backwards to year one. + * + * All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + * second table is needed for interpretation, using a [24-hour linear + * smear](https://developers.google.com/time/smear). + * + * The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + * restricting to that range, we ensure that we can convert to and from [RFC + * 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + * + * # Examples + * + * Example 1: Compute Timestamp from POSIX `time()`. + * + * Timestamp timestamp; + * timestamp.set_seconds(time(NULL)); + * timestamp.set_nanos(0); + * + * Example 2: Compute Timestamp from POSIX `gettimeofday()`. + * + * struct timeval tv; + * gettimeofday(&tv, NULL); + * + * Timestamp timestamp; + * timestamp.set_seconds(tv.tv_sec); + * timestamp.set_nanos(tv.tv_usec * 1000); + * + * Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + * + * FILETIME ft; + * GetSystemTimeAsFileTime(&ft); + * UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + * + * // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + * // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + * Timestamp timestamp; + * timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + * timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + * + * Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + * + * long millis = System.currentTimeMillis(); + * + * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + * .setNanos((int) ((millis % 1000) * 1000000)).build(); + * + * + * Example 5: Compute Timestamp from Java `Instant.now()`. + * + * Instant now = Instant.now(); + * + * Timestamp timestamp = + * Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + * .setNanos(now.getNano()).build(); + * + * + * Example 6: Compute Timestamp from current time in Python. + * + * timestamp = Timestamp() + * timestamp.GetCurrentTime() + * + * # JSON Mapping + * + * In JSON format, the Timestamp type is encoded as a string in the + * [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + * format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + * where {year} is always expressed using four digits while {month}, {day}, + * {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + * seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + * are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + * is required. A proto3 JSON serializer should always use UTC (as indicated by + * "Z") when printing the Timestamp type and a proto3 JSON parser should be + * able to accept both UTC and other timezones (as indicated by an offset). + * + * For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + * 01:30 UTC on January 15, 2017. + * + * In JavaScript, one can convert a Date object to this format using the + * standard + * [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + * method. In Python, a standard `datetime.datetime` object can be converted + * to this format using + * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + * the Joda Time's [`ISODateTimeFormat.dateTime()`]( + * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D + * ) to obtain a formatter capable of generating timestamps in this format. + */ +export interface Timestamp { + /** + * Represents seconds of UTC time since Unix epoch + * 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + * 9999-12-31T23:59:59Z inclusive. + */ + seconds: number; + /** + * Non-negative fractions of a second at nanosecond resolution. Negative + * second values with fractions must still have non-negative nanos values + * that count forward in time. Must be from 0 to 999,999,999 + * inclusive. + */ + nanos: number; +} + +const baseTimestamp: object = { seconds: 0, nanos: 0 }; + +export const Timestamp = { + encode(message: Timestamp, writer: Writer = Writer.create()): Writer { + if (message.seconds !== 0) { + writer.uint32(8).int64(message.seconds); + } + if (message.nanos !== 0) { + writer.uint32(16).int32(message.nanos); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Timestamp { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseTimestamp } as Timestamp; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.seconds = longToNumber(reader.int64() as Long); + break; + case 2: + message.nanos = reader.int32(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Timestamp { + const message = { ...baseTimestamp } as Timestamp; + message.seconds = object.seconds !== undefined && object.seconds !== null ? Number(object.seconds) : 0; + message.nanos = object.nanos !== undefined && object.nanos !== null ? Number(object.nanos) : 0; + return message; + }, + + toJSON(message: Timestamp): unknown { + const obj: any = {}; + message.seconds !== undefined && (obj.seconds = message.seconds); + message.nanos !== undefined && (obj.nanos = message.nanos); + return obj; + }, + + fromPartial, I>>(object: I): Timestamp { + const message = { ...baseTimestamp } as Timestamp; + message.seconds = object.seconds ?? 0; + message.nanos = object.nanos ?? 0; + return message; + }, +}; + +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'; +})(); + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & Record>, never>; + +function longToNumber(long: Long): number { + if (long.gt(Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error('Value is larger than Number.MAX_SAFE_INTEGER'); + } + return long.toNumber(); +} + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/integration/file-suffix/parameters.txt b/integration/file-suffix/parameters.txt new file mode 100644 index 000000000..6e4d5be34 --- /dev/null +++ b/integration/file-suffix/parameters.txt @@ -0,0 +1 @@ +fileSuffix=.pb diff --git a/integration/file-suffix/parent.bin b/integration/file-suffix/parent.bin new file mode 100644 index 000000000..222ccda6a Binary files /dev/null and b/integration/file-suffix/parent.bin differ diff --git a/integration/file-suffix/parent.pb.ts b/integration/file-suffix/parent.pb.ts new file mode 100644 index 000000000..522dac480 --- /dev/null +++ b/integration/file-suffix/parent.pb.ts @@ -0,0 +1,126 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; +import { ChildEnum, Child, childEnumFromJSON, childEnumToJSON } from './child.pb'; +import { Timestamp } from './google/protobuf/timestamp.pb'; + +export const protobufPackage = 'file_suffix'; + +export interface Parent { + child: Child | undefined; + childEnum: ChildEnum; + createdAt: Date | undefined; +} + +const baseParent: object = { childEnum: 0 }; + +export const Parent = { + encode(message: Parent, writer: Writer = Writer.create()): Writer { + if (message.child !== undefined) { + Child.encode(message.child, writer.uint32(10).fork()).ldelim(); + } + if (message.childEnum !== 0) { + writer.uint32(16).int32(message.childEnum); + } + if (message.createdAt !== undefined) { + Timestamp.encode(toTimestamp(message.createdAt), writer.uint32(26).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Parent { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseParent } as Parent; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.child = Child.decode(reader, reader.uint32()); + break; + case 2: + message.childEnum = reader.int32() as any; + break; + case 3: + message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Parent { + const message = { ...baseParent } as Parent; + message.child = object.child !== undefined && object.child !== null ? Child.fromJSON(object.child) : undefined; + message.childEnum = + object.childEnum !== undefined && object.childEnum !== null ? childEnumFromJSON(object.childEnum) : 0; + message.createdAt = + object.createdAt !== undefined && object.createdAt !== null ? fromJsonTimestamp(object.createdAt) : undefined; + return message; + }, + + toJSON(message: Parent): unknown { + const obj: any = {}; + message.child !== undefined && (obj.child = message.child ? Child.toJSON(message.child) : undefined); + message.childEnum !== undefined && (obj.childEnum = childEnumToJSON(message.childEnum)); + message.createdAt !== undefined && (obj.createdAt = message.createdAt.toISOString()); + return obj; + }, + + fromPartial, I>>(object: I): Parent { + const message = { ...baseParent } as Parent; + message.child = object.child !== undefined && object.child !== null ? Child.fromPartial(object.child) : undefined; + message.childEnum = object.childEnum ?? 0; + message.createdAt = object.createdAt ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin + ? P + : P & { [K in keyof P]: Exact } & Record>, never>; + +function toTimestamp(date: Date): Timestamp { + const seconds = date.getTime() / 1_000; + const nanos = (date.getTime() % 1_000) * 1_000_000; + return { seconds, nanos }; +} + +function fromTimestamp(t: Timestamp): Date { + let millis = t.seconds * 1_000; + millis += t.nanos / 1_000_000; + return new Date(millis); +} + +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)); + } +} + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/integration/file-suffix/parent.proto b/integration/file-suffix/parent.proto new file mode 100644 index 000000000..90ed7629f --- /dev/null +++ b/integration/file-suffix/parent.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "child.proto"; + +package file_suffix; + +message Parent { + Child child = 1; + ChildEnum child_enum = 2; + google.protobuf.Timestamp created_at = 3; +} diff --git a/src/encode.ts b/src/encode.ts index ad629bafa..9c81f79d4 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -1,7 +1,8 @@ import { Context } from './context'; -import { code, Code, imp, Import } from 'ts-poet'; +import { code, Code, Import } from 'ts-poet'; import { messageToTypeName, wrapperTypeName } from './types'; import { LongOption } from './options'; +import { impProto } from './utils'; export function generateEncoder(ctx: Context, typeName: string): Code { const name = wrapperTypeName(typeName); @@ -10,22 +11,22 @@ export function generateEncoder(ctx: Context, typeName: string): Code { } if (name == 'Timestamp') { - const TimestampValue = imp(`${name}@./google/protobuf/timestamp`); + const TimestampValue = impProto(ctx.options, 'google/protobuf/timestamp', name); return code`${TimestampValue}.encode(${ctx.utils.toTimestamp}(value)).finish()`; } if (name == 'Struct') { - const StructType = imp(`${name}@./google/protobuf/struct`); + const StructType = impProto(ctx.options, 'google/protobuf/struct', name); return code`${StructType}.encode(${StructType}.wrap(value)).finish()`; } if (name == 'ListValue') { - const ListValueType = imp(`${name}@./google/protobuf/struct`); + const ListValueType = impProto(ctx.options, 'google/protobuf/struct', name); return code`${ListValueType}.encode({values: value ?? []}).finish()`; } - const TypeValue = imp(`${name}@./google/protobuf/wrappers`); + const TypeValue = impProto(ctx.options, 'google/protobuf/wrappers', name); switch (name) { case 'StringValue': @@ -52,6 +53,7 @@ export function generateEncoder(ctx: Context, typeName: string): Code { } export function generateDecoder(ctx: Context, typeName: string): Code { + const { options } = ctx; let name = wrapperTypeName(typeName); if (!name) { return code`${messageToTypeName(ctx, typeName, { keepValueType: true })}.decode(value)`; @@ -60,16 +62,16 @@ export function generateDecoder(ctx: Context, typeName: string): Code { let TypeValue: Import; if (name == 'Timestamp') { - TypeValue = imp(`${name}@./google/protobuf/timestamp`); + TypeValue = impProto(ctx.options, 'google/protobuf/timestamp', name); return code`${TypeValue}.decode(value)`; } if (name == 'Struct' || name == 'ListValue') { - TypeValue = imp(`${name}@./google/protobuf/struct`); + TypeValue = impProto(ctx.options, 'google/protobuf/struct', name); return code`${TypeValue}.unwrap(${TypeValue}.decode(value))`; } - TypeValue = imp(`${name}@./google/protobuf/wrappers`); + TypeValue = impProto(ctx.options, 'google/protobuf/wrappers', name); return code`${TypeValue}.decode(value).value`; } diff --git a/src/main.ts b/src/main.ts index 6d9d4a1ab..ed3522f32 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,7 @@ import { assertInstanceOf, determineFieldJsonName, FormattedMethodDescriptor, + impProto, maybeAddComment, maybePrefixPackage, } from './utils'; @@ -89,7 +90,8 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri // // 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'); + const suffix = `${options.fileSuffix}.ts`; + const moduleName = fileDesc.name.replace('.proto', suffix); const chunks: Code[] = []; // Indicate this file's source protobuf package for reflective use with google.protobuf.Any @@ -458,7 +460,7 @@ function makeDeepPartial(options: Options, longs: ReturnType) { - const Timestamp = imp('Timestamp@./google/protobuf/timestamp'); + const Timestamp = impProto(options, 'google/protobuf/timestamp', 'Timestamp'); let seconds: string | Code = 'date.getTime() / 1_000'; let toNumberCode = 't.seconds'; @@ -721,7 +723,7 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP } } else if (isEnum(field)) { if (options.stringEnums) { - const fromJson = getEnumMethod(typeMap, field.typeName, 'FromJSON'); + const fromJson = getEnumMethod(ctx, field.typeName, 'FromJSON'); readSnippet = code`${fromJson}(${readSnippet})`; } else { readSnippet = code`${readSnippet} as any`; @@ -820,7 +822,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP let writeSnippet: (place: string) => Code; if (isEnum(field) && options.stringEnums) { const tag = ((field.number << 3) | basicWireType(field.type)) >>> 0; - const toNumber = getEnumMethod(typeMap, field.typeName, 'ToNumber'); + const toNumber = getEnumMethod(ctx, field.typeName, 'ToNumber'); writeSnippet = (place) => code`writer.uint32(${tag}).${toReaderCall(field)}(${toNumber}(${place}))`; } else if (isScalar(field) || isEnum(field)) { const tag = ((field.number << 3) | basicWireType(field.type)) >>> 0; @@ -879,7 +881,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP // 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 = getEnumMethod(typeMap, field.typeName, 'ToNumber'); + const toNumber = getEnumMethod(ctx, field.typeName, 'ToNumber'); chunks.push(code` writer.uint32(${tag}).fork(); for (const v of message.${fieldName}) { @@ -958,7 +960,7 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto // get a generic 'reader.doSomething' bit that is specific to the basic type const readSnippet = (from: string): Code => { if (isEnum(field)) { - const fromJson = getEnumMethod(typeMap, field.typeName, 'FromJSON'); + const fromJson = getEnumMethod(ctx, field.typeName, 'FromJSON'); return code`${fromJson}(${from})`; } else if (isPrimitive(field)) { // Convert primitives using the String(value)/Number(value)/bytesFromBase64(value) @@ -1111,7 +1113,7 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP const readSnippet = (from: string): Code => { if (isEnum(field)) { - const toJson = getEnumMethod(typeMap, field.typeName, 'ToJSON'); + const toJson = getEnumMethod(ctx, field.typeName, 'ToJSON'); return isWithinOneOf(field) ? code`${from} !== undefined ? ${toJson}(${from}) : undefined` : code`${toJson}(${from})`; @@ -1125,7 +1127,7 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP // For map types, drill-in and then admittedly re-hard-code our per-value-type logic const valueType = (typeMap.get(field.typeName)![2] as DescriptorProto).field[1]; if (isEnum(valueType)) { - const toJson = getEnumMethod(typeMap, valueType.typeName, 'ToJSON'); + const toJson = getEnumMethod(ctx, valueType.typeName, 'ToJSON'); return code`${toJson}(${from})`; } else if (isBytes(valueType)) { return code`${utils.base64FromBytes}(${from})`; @@ -1201,7 +1203,7 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP function generateFromPartial(ctx: Context, fullName: string, messageDesc: DescriptorProto): Code { const { options, utils, typeMap } = ctx; const chunks: Code[] = []; - const Timestamp = imp('Timestamp@./google/protobuf/timestamp'); + const Timestamp = impProto(options, 'google/protobuf/timestamp', 'Timestamp'); // create the basic function declaration const paramName = messageDesc.field.length > 0 ? 'object' : '_'; diff --git a/src/options.ts b/src/options.ts index 7201311ab..4b1b326ec 100644 --- a/src/options.ts +++ b/src/options.ts @@ -36,6 +36,7 @@ export type Options = { useDate: DateOption; oneof: OneofOption; esModuleInterop: boolean; + fileSuffix: string; outputEncodeMethods: boolean; outputJsonMethods: boolean; outputPartialMethods: boolean; @@ -67,6 +68,7 @@ export function defaultOptions(): Options { useDate: DateOption.DATE, oneof: OneofOption.PROPERTIES, esModuleInterop: false, + fileSuffix: '', lowerCaseServiceMethods: false, outputEncodeMethods: true, outputJsonMethods: true, diff --git a/src/types.ts b/src/types.ts index 25f3c7970..cc7f19e41 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ import { import { code, Code, imp, Import } from 'ts-poet'; import { DateOption, EnvOption, LongOption, OneofOption, Options } from './options'; import { visit } from './visit'; -import { fail, FormattedMethodDescriptor, maybePrefixPackage } from './utils'; +import { fail, FormattedMethodDescriptor, impProto, maybePrefixPackage } from './utils'; import SourceInfo from './sourceInfo'; import { camelCase } from './case'; import { Context } from './context'; @@ -496,7 +496,7 @@ export function messageToTypeName( } } const [module, type] = toModuleAndType(typeMap, protoType); - return code`${imp(`${type}@./${module}`)}`; + return code`${impProto(options, module, type)}`; } /** Breaks `.some_proto_namespace.Some.Message` into `['some_proto_namespace', 'Some_Message', Descriptor]. */ @@ -504,9 +504,9 @@ function toModuleAndType(typeMap: TypeMap, protoType: string): [string, string, return typeMap.get(protoType) || fail(`No type found for ${protoType}`); } -export function getEnumMethod(typeMap: TypeMap, enumProtoType: string, methodSuffix: string): Import { - const [module, type] = toModuleAndType(typeMap, enumProtoType); - return imp(`${camelCase(type)}${methodSuffix}@./${module}`); +export function getEnumMethod(ctx: Context, enumProtoType: string, methodSuffix: string): Import { + const [module, type] = toModuleAndType(ctx.typeMap, enumProtoType); + return impProto(ctx.options, module, `${camelCase(type)}${methodSuffix}`); } /** Return the TypeName for any field (primitive/message/etc.) as exposed in the interface. */ diff --git a/src/utils.ts b/src/utils.ts index f4fdfb993..6552b4843 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { code, Code } from 'ts-poet'; +import { code, Code, imp, Import } from 'ts-poet'; import { CodeGeneratorRequest, FieldDescriptorProto, @@ -182,3 +182,7 @@ export function determineFieldJsonName(field: FieldDescriptorProto, options: Opt } return maybeSnakeToCamel(field.name, options); } + +export function impProto(options: Options, module: string, type: string): Import { + return imp(`${type}@./${module}${options.fileSuffix}`); +} diff --git a/tests/options-test.ts b/tests/options-test.ts index 80f066593..357cf880a 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -12,6 +12,7 @@ describe('options', () => { "env": "both", "esModuleInterop": false, "exportCommonSymbols": true, + "fileSuffix": "", "forceLong": "number", "lowerCaseServiceMethods": true, "nestJs": true, @@ -42,6 +43,13 @@ describe('options', () => { }); }); + it('can set fileSuffix', () => { + const options = optionsFromParameter('fileSuffix=.pb'); + expect(options).toMatchObject({ + fileSuffix: '.pb', + }); + }); + it('can set outputServices to false', () => { const options = optionsFromParameter('outputServices=false'); expect(options).toMatchObject({