diff --git a/README.markdown b/README.markdown index b3adc1ec4..64db0208d 100644 --- a/README.markdown +++ b/README.markdown @@ -379,7 +379,6 @@ The test suite's proto files (i.e. `simple.proto`, `batching.proto`, etc.) curre # Todo - Support the string-based encoding of duration in `fromJSON`/`toJSON` -- Support the `json_name` annotation - Make `oneof=unions` the default behavior in 2.0 - Probably change `forceLong` default in 2.0, should default to `forceLong=long` - Make `esModuleInterop=true` the default in 2.0 diff --git a/integration/simple-json-name/google/protobuf/timestamp.ts b/integration/simple-json-name/google/protobuf/timestamp.ts new file mode 100644 index 000000000..e4600a73e --- /dev/null +++ b/integration/simple-json-name/google/protobuf/timestamp.ts @@ -0,0 +1,206 @@ +/* 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(object: DeepPartial): 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; + +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/simple-json-name/simple-json-name-test.ts b/integration/simple-json-name/simple-json-name-test.ts new file mode 100644 index 000000000..fe0ddf0dc --- /dev/null +++ b/integration/simple-json-name/simple-json-name-test.ts @@ -0,0 +1,25 @@ +import { Simple } from './simple'; + +describe('simple', () => { + it('generates json field names correctly', () => { + const simple: Simple = Simple.fromPartial({ name: 'test' }); + const convertJsonObject: any = Simple.toJSON(simple); + // Check if the generated json field name is correct + expect(Object.prototype.hasOwnProperty.call(convertJsonObject, 'other_name')).toBe(true); + + if (typeof convertJsonObject?.other_name === 'string') { + expect(convertJsonObject.other_name).toBe('test'); + } + + // Check if field names from parsed json are correct + const jsonObject = { + other_name: 'test', + other_age: 10, + createdAt: '2020-01-01T00:00:00.000Z', + }; + const simple2: Simple = Simple.fromJSON(jsonObject); + expect(simple2.name).toBe('test'); + expect(typeof simple2.age).toBe('number'); + expect(typeof simple2.createdAt).toBe(typeof new Date()); + }); +}); diff --git a/integration/simple-json-name/simple.bin b/integration/simple-json-name/simple.bin new file mode 100644 index 000000000..97d15a687 Binary files /dev/null and b/integration/simple-json-name/simple.bin differ diff --git a/integration/simple-json-name/simple.proto b/integration/simple-json-name/simple.proto new file mode 100644 index 000000000..48bf36f0c --- /dev/null +++ b/integration/simple-json-name/simple.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +package simple; + +message Simple { + string name = 1 [ json_name = "other_name" ]; + optional int32 age = 2 [ json_name = "other_age" ]; + optional google.protobuf.Timestamp created_at = 9 [ json_name="createdAt" ]; +} diff --git a/integration/simple-json-name/simple.ts b/integration/simple-json-name/simple.ts new file mode 100644 index 000000000..db9761249 --- /dev/null +++ b/integration/simple-json-name/simple.ts @@ -0,0 +1,118 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; +import { Timestamp } from './google/protobuf/timestamp'; + +export const protobufPackage = 'simple'; + +export interface Simple { + name: string; + age?: number | undefined; + createdAt?: Date | undefined; +} + +const baseSimple: object = { name: '' }; + +export const Simple = { + encode(message: Simple, writer: Writer = Writer.create()): Writer { + if (message.name !== '') { + writer.uint32(10).string(message.name); + } + if (message.age !== undefined) { + writer.uint32(16).int32(message.age); + } + if (message.createdAt !== undefined) { + Timestamp.encode(toTimestamp(message.createdAt), writer.uint32(74).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Simple { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseSimple } as Simple; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.name = reader.string(); + break; + case 2: + message.age = reader.int32(); + break; + case 9: + message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Simple { + const message = { ...baseSimple } as Simple; + message.name = object.other_name !== undefined && object.other_name !== null ? String(object.other_name) : ''; + message.age = object.other_age !== undefined && object.other_age !== null ? Number(object.other_age) : undefined; + message.createdAt = + object.createdAt !== undefined && object.createdAt !== null ? fromJsonTimestamp(object.createdAt) : undefined; + return message; + }, + + toJSON(message: Simple): unknown { + const obj: any = {}; + message.name !== undefined && (obj.other_name = message.name); + message.age !== undefined && (obj.other_age = message.age); + message.createdAt !== undefined && (obj.createdAt = message.createdAt.toISOString()); + return obj; + }, + + fromPartial(object: DeepPartial): Simple { + const message = { ...baseSimple } as Simple; + message.name = object.name ?? ''; + message.age = object.age ?? undefined; + 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; + +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/simple-snake/import_dir/thing.ts b/integration/simple-snake/import_dir/thing.ts index e45e55d4e..3a013304a 100644 --- a/integration/simple-snake/import_dir/thing.ts +++ b/integration/simple-snake/import_dir/thing.ts @@ -40,13 +40,13 @@ export const ImportedThing = { fromJSON(object: any): ImportedThing { const message = { ...baseImportedThing } as ImportedThing; message.created_at = - object.created_at !== undefined && object.created_at !== null ? fromJsonTimestamp(object.created_at) : undefined; + object.createdAt !== undefined && object.createdAt !== null ? fromJsonTimestamp(object.createdAt) : undefined; return message; }, toJSON(message: ImportedThing): unknown { const obj: any = {}; - message.created_at !== undefined && (obj.created_at = message.created_at.toISOString()); + message.created_at !== undefined && (obj.createdAt = message.created_at.toISOString()); return obj; }, diff --git a/integration/simple-snake/simple.ts b/integration/simple-snake/simple.ts index 4ce1a42aa..ca2844a71 100644 --- a/integration/simple-snake/simple.ts +++ b/integration/simple-snake/simple.ts @@ -344,13 +344,13 @@ export const Simple = { message.name = object.name !== undefined && object.name !== null ? String(object.name) : ''; message.age = object.age !== undefined && object.age !== null ? Number(object.age) : 0; message.created_at = - object.created_at !== undefined && object.created_at !== null ? fromJsonTimestamp(object.created_at) : undefined; + object.createdAt !== undefined && object.createdAt !== null ? fromJsonTimestamp(object.createdAt) : undefined; message.child = object.child !== undefined && object.child !== null ? Child.fromJSON(object.child) : undefined; message.state = object.state !== undefined && object.state !== null ? stateEnumFromJSON(object.state) : 0; - message.grand_children = (object.grand_children ?? []).map((e: any) => Child.fromJSON(e)); + message.grand_children = (object.grandChildren ?? []).map((e: any) => Child.fromJSON(e)); message.coins = (object.coins ?? []).map((e: any) => Number(e)); message.snacks = (object.snacks ?? []).map((e: any) => String(e)); - message.old_states = (object.old_states ?? []).map((e: any) => stateEnumFromJSON(e)); + message.old_states = (object.oldStates ?? []).map((e: any) => stateEnumFromJSON(e)); message.thing = object.thing !== undefined && object.thing !== null ? ImportedThing.fromJSON(object.thing) : undefined; return message; @@ -360,13 +360,13 @@ export const Simple = { const obj: any = {}; message.name !== undefined && (obj.name = message.name); message.age !== undefined && (obj.age = message.age); - message.created_at !== undefined && (obj.created_at = message.created_at.toISOString()); + message.created_at !== undefined && (obj.createdAt = message.created_at.toISOString()); message.child !== undefined && (obj.child = message.child ? Child.toJSON(message.child) : undefined); message.state !== undefined && (obj.state = stateEnumToJSON(message.state)); if (message.grand_children) { - obj.grand_children = message.grand_children.map((e) => (e ? Child.toJSON(e) : undefined)); + obj.grandChildren = message.grand_children.map((e) => (e ? Child.toJSON(e) : undefined)); } else { - obj.grand_children = []; + obj.grandChildren = []; } if (message.coins) { obj.coins = message.coins.map((e) => e); @@ -379,9 +379,9 @@ export const Simple = { obj.snacks = []; } if (message.old_states) { - obj.old_states = message.old_states.map((e) => stateEnumToJSON(e)); + obj.oldStates = message.old_states.map((e) => stateEnumToJSON(e)); } else { - obj.old_states = []; + obj.oldStates = []; } message.thing !== undefined && (obj.thing = message.thing ? ImportedThing.toJSON(message.thing) : undefined); return obj; @@ -1174,7 +1174,7 @@ export const SimpleWithSnakeCaseMap = { fromJSON(object: any): SimpleWithSnakeCaseMap { const message = { ...baseSimpleWithSnakeCaseMap } as SimpleWithSnakeCaseMap; - message.entities_by_id = Object.entries(object.entities_by_id ?? {}).reduce<{ [key: number]: Entity }>( + message.entities_by_id = Object.entries(object.entitiesById ?? {}).reduce<{ [key: number]: Entity }>( (acc, [key, value]) => { acc[Number(key)] = Entity.fromJSON(value); return acc; @@ -1186,10 +1186,10 @@ export const SimpleWithSnakeCaseMap = { toJSON(message: SimpleWithSnakeCaseMap): unknown { const obj: any = {}; - obj.entities_by_id = {}; + obj.entitiesById = {}; if (message.entities_by_id) { Object.entries(message.entities_by_id).forEach(([k, v]) => { - obj.entities_by_id[k] = Entity.toJSON(v); + obj.entitiesById[k] = Entity.toJSON(v); }); } return obj; diff --git a/src/main.ts b/src/main.ts index e9f196a9e..25bb44d7c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,13 @@ import { isStructType, } from './types'; import SourceInfo, { Fields } from './sourceInfo'; -import { assertInstanceOf, FormattedMethodDescriptor, maybeAddComment, maybePrefixPackage } from './utils'; +import { + assertInstanceOf, + determineFieldJsonName, + FormattedMethodDescriptor, + maybeAddComment, + maybePrefixPackage, +} from './utils'; import { camelToSnake, capitalize, maybeSnakeToCamel } from './case'; import { generateNestjsGrpcServiceMethodsDecorator, @@ -994,6 +1000,7 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto // add a check for each incoming field messageDesc.field.forEach((field) => { const fieldName = maybeSnakeToCamel(field.name, options); + const jsonName = determineFieldJsonName(field, options); // get a generic 'reader.doSomething' bit that is specific to the basic type const readSnippet = (from: string): Code => { @@ -1085,26 +1092,26 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto const fieldType = toTypeName(ctx, messageDesc, field); const i = maybeCastToNumber(ctx, messageDesc, field, 'key'); chunks.push(code` - message.${fieldName} = Object.entries(object.${fieldName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => { + message.${fieldName} = Object.entries(object.${jsonName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => { acc[${i}] = ${readSnippet('value')}; return acc; }, {}); `); } else if (isAnyValueType(field)) { chunks.push(code` - message.${fieldName} = Array.isArray(object?.${fieldName}) ? [...object.${fieldName}] : []; + message.${fieldName} = Array.isArray(object?.${jsonName}) ? [...object.${jsonName}] : []; `); } else { // Explicit `any` type required to make TS with noImplicitAny happy. `object` is also `any` here. chunks.push(code` - message.${fieldName} = (object.${fieldName} ?? []).map((e: any) => ${readSnippet('e')}); + message.${fieldName} = (object.${jsonName} ?? []).map((e: any) => ${readSnippet('e')}); `); } } else if (isWithinOneOfThatShouldBeUnion(options, field)) { - chunks.push(code`if (object.${fieldName} !== undefined && object.${fieldName} !== null) {`); + chunks.push(code`if (object.${jsonName} !== undefined && object.${jsonName} !== null) {`); const oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); chunks.push(code` - message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${fieldName}`)} } + message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${jsonName}`)} } `); chunks.push(code`}`); } else if (isAnyValueType(field)) { @@ -1122,8 +1129,8 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto } else { const fallback = isWithinOneOf(field) ? 'undefined' : defaultValue(ctx, field); chunks.push(code` - message.${fieldName} = (object.${fieldName} !== undefined && object.${fieldName} !== null) - ? ${readSnippet(`object.${fieldName}`)} + message.${fieldName} = (object.${jsonName} !== undefined && object.${jsonName} !== null) + ? ${readSnippet(`object.${jsonName}`)} : ${fallback}; `); } @@ -1147,6 +1154,7 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP // then add a case for each field messageDesc.field.forEach((field) => { const fieldName = maybeSnakeToCamel(field.name, options); + const jsonName = determineFieldJsonName(field, options); const readSnippet = (from: string): Code => { if (isEnum(field)) { @@ -1206,10 +1214,10 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP if (isMapType(ctx, messageDesc, field)) { // Maps might need their values transformed, i.e. bytes --> base64 chunks.push(code` - obj.${fieldName} = {}; + obj.${jsonName} = {}; if (message.${fieldName}) { Object.entries(message.${fieldName}).forEach(([k, v]) => { - obj.${fieldName}[k] = ${readSnippet('v')}; + obj.${jsonName}[k] = ${readSnippet('v')}; }); } `); @@ -1217,19 +1225,19 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP // Arrays might need their elements transformed chunks.push(code` if (message.${fieldName}) { - obj.${fieldName} = message.${fieldName}.map(e => ${readSnippet('e')}); + obj.${jsonName} = message.${fieldName}.map(e => ${readSnippet('e')}); } else { - obj.${fieldName} = []; + obj.${jsonName} = []; } `); } else if (isWithinOneOfThatShouldBeUnion(options, field)) { // oneofs in a union are only output as `oneof name = ...` const oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); const v = readSnippet(`message.${oneofName}?.${fieldName}`); - chunks.push(code`message.${oneofName}?.$case === '${fieldName}' && (obj.${fieldName} = ${v});`); + chunks.push(code`message.${oneofName}?.$case === '${fieldName}' && (obj.${jsonName} = ${v});`); } else { const v = readSnippet(`message.${fieldName}`); - chunks.push(code`message.${fieldName} !== undefined && (obj.${fieldName} = ${v});`); + chunks.push(code`message.${fieldName} !== undefined && (obj.${jsonName} = ${v});`); } }); chunks.push(code`return obj;`); diff --git a/src/utils.ts b/src/utils.ts index 5b3f8c669..f4fdfb993 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,15 @@ import { code, Code } from 'ts-poet'; -import { CodeGeneratorRequest, FileDescriptorProto, MethodDescriptorProto, MethodOptions } from 'ts-proto-descriptors'; +import { + CodeGeneratorRequest, + FieldDescriptorProto, + FileDescriptorProto, + MethodDescriptorProto, + MethodOptions, +} from 'ts-proto-descriptors'; import ReadStream = NodeJS.ReadStream; import { SourceDescription } from './sourceInfo'; import { Options, ServiceOption } from './options'; -import { camelCase } from './case'; +import { camelCase, maybeSnakeToCamel } from './case'; export function protoFilesToGenerate(request: CodeGeneratorRequest): FileDescriptorProto[] { return request.protoFile.filter((f) => request.fileToGenerate.includes(f.name)); @@ -167,3 +173,12 @@ export class FormattedMethodDescriptor implements MethodDescriptorProto { return result; } } + +export function determineFieldJsonName(field: FieldDescriptorProto, options: Options): string { + // By default jsonName is camelCased by the protocol compilier unless the user has + // set a "json_name" option on this field. + if (field.jsonName.length > 0) { + return field.jsonName; + } + return maybeSnakeToCamel(field.name, options); +}