From 587bf0e5be0947eaa236b376a62ad1591a84bfc1 Mon Sep 17 00:00:00 2001 From: Yo-mah-Ya Date: Mon, 9 Oct 2023 17:39:26 -0700 Subject: [PATCH] add test --- .../google/protobuf/timestamp.ts | 214 ++++++++++ integration/use-json-name/json_name.bin | Bin 0 -> 8424 bytes integration/use-json-name/json_name.proto | 29 ++ integration/use-json-name/json_name.ts | 377 ++++++++++++++++++ integration/use-json-name/parameters.txt | 1 + src/main.ts | 161 ++++---- src/utils.ts | 42 +- 7 files changed, 747 insertions(+), 77 deletions(-) create mode 100644 integration/use-json-name/google/protobuf/timestamp.ts create mode 100644 integration/use-json-name/json_name.bin create mode 100644 integration/use-json-name/json_name.proto create mode 100644 integration/use-json-name/json_name.ts create mode 100644 integration/use-json-name/parameters.txt diff --git a/integration/use-json-name/google/protobuf/timestamp.ts b/integration/use-json-name/google/protobuf/timestamp.ts new file mode 100644 index 000000000..0cf501b86 --- /dev/null +++ b/integration/use-json-name/google/protobuf/timestamp.ts @@ -0,0 +1,214 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; +import Long = require("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; +} + +function createBaseTimestamp(): Timestamp { + return { seconds: 0, nanos: 0 }; +} + +export const Timestamp = { + encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.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: _m0.Reader | Uint8Array, length?: number): Timestamp { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTimestamp(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 8) { + break; + } + + message.seconds = longToNumber(reader.int64() as Long); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.nanos = reader.int32(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Timestamp { + return { + seconds: isSet(object.seconds) ? globalThis.Number(object.seconds) : 0, + nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0, + }; + }, + + toJSON(message: Timestamp): unknown { + const obj: any = {}; + if (message.seconds !== 0) { + obj.seconds = Math.round(message.seconds); + } + if (message.nanos !== 0) { + obj.nanos = Math.round(message.nanos); + } + return obj; + }, + + create, I>>(base?: I): Timestamp { + return Timestamp.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Timestamp { + const message = createBaseTimestamp(); + message.seconds = object.seconds ?? 0; + message.nanos = object.nanos ?? 0; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.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 } & { [K in Exclude>]: never }; + +function longToNumber(long: Long): number { + if (long.gt(globalThis.Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + return long.toNumber(); +} + +if (_m0.util.Long !== Long) { + _m0.util.Long = Long as any; + _m0.configure(); +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/use-json-name/json_name.bin b/integration/use-json-name/json_name.bin new file mode 100644 index 0000000000000000000000000000000000000000..9a807dbeb5714625d87dbfe9a2f4a0f31acc0a9b GIT binary patch literal 8424 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 3) { + case 1: + if (tag !== 10) { + break; + } + + message.other_name = reader.string(); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.other_age = reader.int32(); + continue; + case 9: + if (tag !== 74) { + break; + } + + message.createdAt = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + continue; + case 3: + if (tag !== 26) { + break; + } + + message["hyphened-name"] = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message["name with spaces"] = reader.string(); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.$dollar = reader.string(); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.dollar$ = reader.string(); + continue; + case 7: + if (tag !== 58) { + break; + } + + message["hyphen-list"].push(reader.string()); + continue; + case 10: + if (tag !== 82) { + break; + } + + message.A = reader.string(); + continue; + case 11: + if (tag !== 90) { + break; + } + + message.b = reader.string(); + continue; + case 12: + if (tag !== 98) { + break; + } + + message._C = reader.string(); + continue; + case 13: + if (tag !== 106) { + break; + } + + message.d = NstedOneOf.decode(reader, reader.uint32()); + continue; + case 14: + if (tag !== 114) { + break; + } + + message.noJsonName = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): JsonName { + return { + other_name: isSet(object.other_name) ? globalThis.String(object.other_name) : "", + other_age: isSet(object.other_age) ? globalThis.Number(object.other_age) : undefined, + createdAt: isSet(object.createdAt) ? fromJsonTimestamp(object.createdAt) : undefined, + "hyphened-name": isSet(object["hyphened-name"]) ? globalThis.String(object["hyphened-name"]) : "", + "name with spaces": isSet(object["name with spaces"]) ? globalThis.String(object["name with spaces"]) : "", + $dollar: isSet(object.$dollar) ? globalThis.String(object.$dollar) : "", + dollar$: isSet(object.dollar$) ? globalThis.String(object.dollar$) : "", + "hyphen-list": globalThis.Array.isArray(object?.["hyphen-list"]) + ? object["hyphen-list"].map((e: any) => globalThis.String(e)) + : [], + A: isSet(object.A) ? globalThis.String(object.A) : undefined, + b: isSet(object.b) ? globalThis.String(object.b) : undefined, + _C: isSet(object._C) ? globalThis.String(object._C) : undefined, + d: isSet(object.d) ? NstedOneOf.fromJSON(object.d) : undefined, + noJsonName: isSet(object.noJsonName) ? globalThis.String(object.noJsonName) : "", + }; + }, + + toJSON(message: JsonName): unknown { + const obj: any = {}; + if (message.other_name !== "") { + obj.other_name = message.other_name; + } + if (message.other_age !== undefined) { + obj.other_age = Math.round(message.other_age); + } + if (message.createdAt !== undefined) { + obj.createdAt = message.createdAt.toISOString(); + } + if (message["hyphened-name"] !== "") { + obj["hyphened-name"] = message["hyphened-name"]; + } + if (message["name with spaces"] !== "") { + obj["name with spaces"] = message["name with spaces"]; + } + if (message.$dollar !== "") { + obj.$dollar = message.$dollar; + } + if (message.dollar$ !== "") { + obj.dollar$ = message.dollar$; + } + if (message["hyphen-list"]?.length) { + obj["hyphen-list"] = message["hyphen-list"]; + } + if (message.A !== undefined) { + obj.A = message.A; + } + if (message.b !== undefined) { + obj.b = message.b; + } + if (message._C !== undefined) { + obj._C = message._C; + } + if (message.d !== undefined) { + obj.d = NstedOneOf.toJSON(message.d); + } + if (message.noJsonName !== "") { + obj.noJsonName = message.noJsonName; + } + return obj; + }, + + create, I>>(base?: I): JsonName { + return JsonName.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): JsonName { + const message = createBaseJsonName(); + message.other_name = object.other_name ?? ""; + message.other_age = object.other_age ?? undefined; + message.createdAt = object.createdAt ?? undefined; + message["hyphened-name"] = object["hyphened-name"] ?? ""; + message["name with spaces"] = object["name with spaces"] ?? ""; + message.$dollar = object.$dollar ?? ""; + message.dollar$ = object.dollar$ ?? ""; + message["hyphen-list"] = object["hyphen-list"]?.map((e) => e) || []; + message.A = object.A ?? undefined; + message.b = object.b ?? undefined; + message._C = object._C ?? undefined; + message.d = (object.d !== undefined && object.d !== null) ? NstedOneOf.fromPartial(object.d) : undefined; + message.noJsonName = object.noJsonName ?? ""; + return message; + }, +}; + +function createBaseNstedOneOf(): NstedOneOf { + return { nestedOneOfField: undefined }; +} + +export const NstedOneOf = { + encode(message: NstedOneOf, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.nestedOneOfField !== undefined) { + writer.uint32(10).string(message.nestedOneOfField); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): NstedOneOf { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseNstedOneOf(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.nestedOneOfField = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): NstedOneOf { + return { + nestedOneOfField: isSet(object.nestedOneOfField) ? globalThis.String(object.nestedOneOfField) : undefined, + }; + }, + + toJSON(message: NstedOneOf): unknown { + const obj: any = {}; + if (message.nestedOneOfField !== undefined) { + obj.nestedOneOfField = message.nestedOneOfField; + } + return obj; + }, + + create, I>>(base?: I): NstedOneOf { + return NstedOneOf.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): NstedOneOf { + const message = createBaseNstedOneOf(); + message.nestedOneOfField = object.nestedOneOfField ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.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 } & { [K in Exclude>]: 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 || 0) * 1_000; + millis += (t.nanos || 0) / 1_000_000; + return new globalThis.Date(millis); +} + +function fromJsonTimestamp(o: any): Date { + if (o instanceof globalThis.Date) { + return o; + } else if (typeof o === "string") { + return new globalThis.Date(o); + } else { + return fromTimestamp(Timestamp.fromJSON(o)); + } +} + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/use-json-name/parameters.txt b/integration/use-json-name/parameters.txt new file mode 100644 index 000000000..d891d8ea5 --- /dev/null +++ b/integration/use-json-name/parameters.txt @@ -0,0 +1 @@ +useJsonName=true diff --git a/src/main.ts b/src/main.ts index f49641fdd..4591d2fe4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -46,12 +46,13 @@ import SourceInfo, { Fields } from "./sourceInfo"; import { assertInstanceOf, FormattedMethodDescriptor, - getFieldJsonName, getPropertyAccessor, + propertyNameComposition, impFile, impProto, maybeAddComment, maybePrefixPackage, + getFieldJsonName, } from "./utils"; import { camelToSnake, capitalize, maybeSnakeToCamel } from "./case"; import { @@ -873,11 +874,10 @@ function generateInterfaceDeclaration( const info = sourceInfo.lookup(Fields.message.field, index); maybeAddComment(info, chunks, fieldDesc.options?.deprecated); - - const name = maybeSnakeToCamel(options.useJsonName ? fieldDesc.jsonName : fieldDesc.name, options); + const { validatedPropertyName } = propertyNameComposition(fieldDesc, options); const isOptional = isOptionalProperty(fieldDesc, messageDesc.options, options); const type = toTypeName(ctx, messageDesc, fieldDesc, isOptional); - chunks.push(code`${maybeReadonly(options)}${name}${isOptional ? "?" : ""}: ${type}, `); + chunks.push(code`${maybeReadonly(options)}${validatedPropertyName}${isOptional ? "?" : ""}: ${type}, `); }); if (ctx.options.unknownFields) { @@ -950,7 +950,9 @@ function generateBaseInstanceFactory( if (!processedOneofs.has(oneofIndex)) { processedOneofs.add(oneofIndex); - const name = maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, ctx.options); + const name = options.useJsonName + ? propertyNameComposition(field, options).validatedPropertyName + : maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, ctx.options); fields.push(code`${name}: undefined`); } continue; @@ -960,7 +962,7 @@ function generateBaseInstanceFactory( continue; } - const name = maybeSnakeToCamel(field.name, ctx.options); + const { validatedPropertyName } = propertyNameComposition(field, options); const val = isWithinOneOf(field) ? "undefined" : isMapType(ctx, messageDesc, field) @@ -971,7 +973,7 @@ function generateBaseInstanceFactory( ? "[]" : defaultValue(ctx, field); - fields.push(code`${name}: ${val}`); + fields.push(code`${validatedPropertyName}: ${val}`); } if (addTypeToMessages(options)) { @@ -1082,7 +1084,8 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP // add a case for each incoming field messageDesc.field.forEach((field) => { - const fieldName = maybeSnakeToCamel(field.name, options); + const { propertyName } = propertyNameComposition(field, options); + const messageProperty = getPropertyAccessor("message", propertyName); chunks.push(code`case ${field.number}:`); const tag = ((field.number << 3) | basicWireType(field.type)) >>> 0; @@ -1110,14 +1113,14 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP let valueSetterSnippet: string; if (generateMapType) { - valueSetterSnippet = `message.${fieldName}${maybeNonNullAssertion}.set(${varName}.key, ${varName}.value)`; + valueSetterSnippet = `${messageProperty}${maybeNonNullAssertion}.set(${varName}.key, ${varName}.value)`; } else { - valueSetterSnippet = `message.${fieldName}${maybeNonNullAssertion}[${varName}.key] = ${varName}.value`; + valueSetterSnippet = `${messageProperty}${maybeNonNullAssertion}[${varName}.key] = ${varName}.value`; } const initializerSnippet = initializerNecessary ? ` - if (message.${fieldName} === undefined) { - message.${fieldName} = ${generateMapType ? "new Map()" : "{}"}; + if (${messageProperty} === undefined) { + ${messageProperty} = ${generateMapType ? "new Map()" : "{}"}; }` : ""; chunks.push(code` @@ -1131,15 +1134,15 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP } else { const initializerSnippet = initializerNecessary ? ` - if (message.${fieldName} === undefined) { - message.${fieldName} = []; + if (${messageProperty} === undefined) { + ${messageProperty} = []; }` : ""; if (packedType(field.type) === undefined) { chunks.push(code` ${tagCheck} ${initializerSnippet} - message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet}); + ${messageProperty}${maybeNonNullAssertion}.push(${readSnippet}); `); } else { const packedTag = ((field.number << 3) | 2) >>> 0; @@ -1147,7 +1150,7 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP chunks.push(code` if (tag === ${tag}) { ${initializerSnippet} - message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet}); + ${messageProperty}${maybeNonNullAssertion}.push(${readSnippet}); continue; } @@ -1156,7 +1159,7 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP ${initializerSnippet} const end2 = reader.uint32() + reader.pos; while (reader.pos < end2) { - message.${fieldName}${maybeNonNullAssertion}.push(${readSnippet}); + ${messageProperty}${maybeNonNullAssertion}.push(${readSnippet}); } continue; @@ -1167,15 +1170,17 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP } } } else if (isWithinOneOfThatShouldBeUnion(options, field)) { - let oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); + const oneofNameWithMessage = options.useJsonName + ? messageProperty + : getPropertyAccessor("message", maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options)); chunks.push(code` ${tagCheck} - message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet} }; + ${oneofNameWithMessage} = { $case: '${propertyName}', ${propertyName}: ${readSnippet} }; `); } else { chunks.push(code` ${tagCheck} - message.${fieldName} = ${readSnippet}; + ${messageProperty} = ${readSnippet}; `); } @@ -1323,7 +1328,8 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP // then add a case for each field messageDesc.field.forEach((field) => { - const fieldName = maybeSnakeToCamel(field.name, options); + const { propertyName, validatedPropertyName } = propertyNameComposition(field, options); + const messageProperty = getPropertyAccessor("message", propertyName); // get a generic writer.doSomething based on the basic type const writeSnippet = getEncodeWriteSnippet(ctx, field); @@ -1345,26 +1351,26 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP if (useMapType) { chunks.push(code` - (message.${fieldName}${optionalAlternative}).forEach((value, key) => { + (${messageProperty}${optionalAlternative}).forEach((value, key) => { ${entryWriteSnippet} }); `); } else { chunks.push(code` - Object.entries(message.${fieldName}${optionalAlternative}).forEach(([key, value]) => { + Object.entries(${messageProperty}${optionalAlternative}).forEach(([key, value]) => { ${entryWriteSnippet} }); `); } } else if (packedType(field.type) === undefined) { const listWriteSnippet = code` - for (const v of message.${fieldName}) { + for (const v of ${messageProperty}) { ${writeSnippet("v!")}; } `; if (isOptional) { chunks.push(code` - if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { + if (${messageProperty} !== undefined && ${messageProperty}.length !== 0) { ${listWriteSnippet} } `); @@ -1380,14 +1386,14 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP const toNumber = getEnumMethod(ctx, field.typeName, "ToNumber"); const listWriteSnippet = code` writer.uint32(${tag}).fork(); - for (const v of message.${fieldName}) { + for (const v of ${messageProperty}) { writer.${toReaderCall(field)}(${toNumber}(v)); } writer.ldelim(); `; if (isOptional) { chunks.push(code` - if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { + if (${messageProperty} !== undefined && ${messageProperty}.length !== 0) { ${listWriteSnippet} } `); @@ -1400,7 +1406,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP const rhs = (x: string) => (isLong(field) && options.forceLong === LongOption.BIGINT ? `${x}.toString()` : x); let listWriteSnippet = code` writer.uint32(${tag}).fork(); - for (const v of message.${fieldName}) { + for (const v of ${messageProperty}) { writer.${toReaderCall(field)}(${rhs("v")}); } writer.ldelim(); @@ -1414,9 +1420,9 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP case "sfixed64": listWriteSnippet = code` writer.uint32(${tag}).fork(); - for (const v of message.${fieldName}) { + for (const v of ${messageProperty}) { if (BigInt.asIntN(64, v) !== v) { - throw new Error('a value provided in array field ${fieldName} of type ${fieldType} is too large'); + throw new Error('a value provided in array field ${propertyName} of type ${fieldType} is too large'); } writer.${toReaderCall(field)}(${rhs("v")}); } @@ -1427,9 +1433,9 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP case "fixed64": listWriteSnippet = code` writer.uint32(${tag}).fork(); - for (const v of message.${fieldName}) { + for (const v of ${messageProperty}) { if (BigInt.asUintN(64, v) !== v) { - throw new Error('a value provided in array field ${fieldName} of type ${fieldType} is too large'); + throw new Error('a value provided in array field ${propertyName} of type ${fieldType} is too large'); } writer.${toReaderCall(field)}(${rhs("v")}); } @@ -1442,7 +1448,7 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP } if (isOptional) { chunks.push(code` - if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) { + if (${messageProperty} !== undefined && ${messageProperty}.length !== 0) { ${listWriteSnippet} } `); @@ -1454,13 +1460,15 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP if (!processedOneofs.has(field.oneofIndex)) { processedOneofs.add(field.oneofIndex); - const oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); - chunks.push(code`switch (message.${oneofName}?.$case) {`); + const oneofNameWithMessage = options.useJsonName + ? messageProperty + : getPropertyAccessor("message", maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options)); + chunks.push(code`switch (${oneofNameWithMessage}?.$case) {`); for (const oneOfField of oneOfFieldsDict[field.oneofIndex]) { const writeSnippet = getEncodeWriteSnippet(ctx, oneOfField); const oneOfFieldName = maybeSnakeToCamel(oneOfField.name, ctx.options); chunks.push(code`case "${oneOfFieldName}": - ${writeSnippet(`message.${oneofName}.${oneOfFieldName}`)}; + ${writeSnippet(`${oneofNameWithMessage}.${oneOfFieldName}`)}; break;`); } chunks.push(code`}`); @@ -1468,24 +1476,24 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP } else if (isWithinOneOf(field)) { // Oneofs don't have a default value check b/c they need to denote which-oneof presence chunks.push(code` - if (message.${fieldName} !== undefined) { - ${writeSnippet(`message.${fieldName}`)}; + if (${messageProperty} !== undefined) { + ${writeSnippet(`${messageProperty}`)}; } `); } else if (isMessage(field)) { chunks.push(code` - if (message.${fieldName} !== undefined) { - ${writeSnippet(`message.${fieldName}`)}; + if (${messageProperty} !== undefined) { + ${writeSnippet(`${messageProperty}`)}; } `); } else if (isScalar(field) || isEnum(field)) { chunks.push(code` - if (${notDefaultCheck(ctx, field, messageDesc.options, `message.${fieldName}`)}) { - ${writeSnippet(`message.${fieldName}`)}; + if (${notDefaultCheck(ctx, field, messageDesc.options, `${messageProperty}`)}) { + ${writeSnippet(`${messageProperty}`)}; } `); } else { - chunks.push(code`${writeSnippet(`message.${fieldName}`)};`); + chunks.push(code`${writeSnippet(`${messageProperty}`)};`); } }); @@ -1783,7 +1791,7 @@ function generateFromJson(ctx: Context, fullName: string, fullTypeName: string, // add a check for each incoming field messageDesc.field.forEach((field) => { - const fieldName = maybeSnakeToCamel(field.name, options); + const { validatedPropertyName: fieldName } = propertyNameComposition(field, options); const jsonName = getFieldJsonName(field, options); const jsonProperty = getPropertyAccessor("object", jsonName); const jsonPropertyOptional = getPropertyAccessor("object", jsonName, true); @@ -2016,9 +2024,10 @@ function generateToJson( // then add a case for each field messageDesc.field.forEach((field) => { - const fieldName = maybeSnakeToCamel(field.name, options); + const { validatedPropertyName: fieldName, propertyName } = propertyNameComposition(field, options); const jsonName = getFieldJsonName(field, options); const jsonProperty = getPropertyAccessor("obj", jsonName); + const messageProperty = getPropertyAccessor("message", propertyName); const readSnippet = (from: string): Code => { if (isEnum(field)) { @@ -2089,17 +2098,17 @@ function generateToJson( if (shouldGenerateJSMapType(ctx, messageDesc, field)) { chunks.push(code` - if (message.${fieldName}?.size) { + if (${messageProperty}?.size) { ${jsonProperty} = {}; - message.${fieldName}.forEach((v, k) => { + ${messageProperty}.forEach((v, k) => { ${jsonProperty}[${i}] = ${readSnippet("v")}; }); } `); } else { chunks.push(code` - if (message.${fieldName}) { - const entries = Object.entries(message.${fieldName}); + if (${messageProperty}) { + const entries = Object.entries(${messageProperty}); if (entries.length > 0) { ${jsonProperty} = {}; entries.forEach(([k, v]) => { @@ -2114,28 +2123,30 @@ function generateToJson( const transformElement = readSnippet("e"); const maybeMap = transformElement.toCodeString([]) !== "e" ? code`.map(e => ${transformElement})` : ""; chunks.push(code` - if (message.${fieldName}?.length) { - ${jsonProperty} = message.${fieldName}${maybeMap}; + if (${messageProperty}?.length) { + ${jsonProperty} = ${messageProperty}${maybeMap}; } `); } 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 oneofNameWithMessage = options.useJsonName + ? messageProperty + : getPropertyAccessor("message", maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options)); chunks.push(code` - if (message.${oneofName}?.$case === '${fieldName}') { - ${jsonProperty} = ${readSnippet(`message.${oneofName}.${fieldName}`)}; + if (${oneofNameWithMessage}?.$case === '${fieldName}') { + ${jsonProperty} = ${readSnippet(`${oneofNameWithMessage}.${fieldName}`)}; } `); } else { let emitDefaultValuesForJson = ctx.options.emitDefaultValues.includes("json-methods"); const check = (isScalar(field) || isEnum(field)) && !(isWithinOneOf(field) || emitDefaultValuesForJson) - ? notDefaultCheck(ctx, field, messageDesc.options, `message.${fieldName}`) - : `message.${fieldName} !== undefined`; + ? notDefaultCheck(ctx, field, messageDesc.options, `${messageProperty}`) + : `${messageProperty} !== undefined`; chunks.push(code` if (${check}) { - ${jsonProperty} = ${readSnippet(`message.${fieldName}`)}; + ${jsonProperty} = ${readSnippet(`${messageProperty}`)}; } `); } @@ -2186,7 +2197,9 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri // add a check for each incoming field messageDesc.field.forEach((field) => { - const fieldName = maybeSnakeToCamel(field.name, options); + const { propertyName } = propertyNameComposition(field, options); + const messageProperty = getPropertyAccessor("message", propertyName); + const objectProperty = getPropertyAccessor("object", propertyName); const readSnippet = (from: string): Code => { if ((isLong(field) || isLongValueType(field)) && options.forceLong === LongOption.LONG) { @@ -2251,14 +2264,14 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const i = convertFromObjectKey(ctx, messageDesc, field, "key"); const noValueSnippet = noDefaultValue - ? `(object.${fieldName} === undefined || object.${fieldName} === null) ? undefined : ` + ? `(${objectProperty} === undefined || ${objectProperty} === null) ? undefined : ` : ""; if (shouldGenerateJSMapType(ctx, messageDesc, field)) { chunks.push(code` - message.${fieldName} = ${noValueSnippet} (() => { + ${messageProperty} = ${noValueSnippet} (() => { const m = new Map(); - (object.${fieldName} as ${fieldType} ?? new Map()).forEach((value, key) => { + (${objectProperty} as ${fieldType} ?? new Map()).forEach((value, key) => { if (value !== undefined) { m.set(key, ${readSnippet("value")}); } @@ -2268,7 +2281,7 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri `); } else { chunks.push(code` - message.${fieldName} = ${noValueSnippet} Object.entries(object.${fieldName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => { + ${messageProperty} = ${noValueSnippet} Object.entries(${objectProperty} ?? {}).reduce<${fieldType}>((acc, [key, value]) => { if (value !== undefined) { acc[${i}] = ${readSnippet("value")}; } @@ -2280,30 +2293,34 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const fallback = noDefaultValue ? "undefined" : "[]"; chunks.push(code` - message.${fieldName} = object.${fieldName}?.map((e) => ${readSnippet("e")}) || ${fallback}; + ${messageProperty} = ${objectProperty}?.map((e) => ${readSnippet("e")}) || ${fallback}; `); } } else if (isWithinOneOfThatShouldBeUnion(options, field)) { - let oneofName = maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); - const v = readSnippet(`object.${oneofName}.${fieldName}`); + const oneofName = options.useJsonName + ? propertyName + : maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options); + const oneofNameWithMessage = getPropertyAccessor("message", oneofName); + const oneofNameWithObject = getPropertyAccessor("object", oneofName); + const v = readSnippet(`${oneofNameWithObject}.${propertyName}`); chunks.push(code` if ( - object.${oneofName}?.$case === '${fieldName}' - && object.${oneofName}?.${fieldName} !== undefined - && object.${oneofName}?.${fieldName} !== null + ${oneofNameWithObject}?.$case === '${propertyName}' + && ${oneofNameWithObject}?.${propertyName} !== undefined + && ${oneofNameWithObject}?.${propertyName} !== null ) { - message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${v} }; + ${oneofNameWithMessage} = { $case: '${propertyName}', ${propertyName}: ${v} }; } `); } else if (readSnippet(`x`).toCodeString([]) == "x") { // An optimized case of the else below that works when `readSnippet` returns the plain input const fallback = isWithinOneOf(field) || noDefaultValue ? "undefined" : defaultValue(ctx, field); - chunks.push(code`message.${fieldName} = object.${fieldName} ?? ${fallback};`); + chunks.push(code`${messageProperty} = ${objectProperty} ?? ${fallback};`); } else { const fallback = isWithinOneOf(field) || noDefaultValue ? "undefined" : defaultValue(ctx, field); chunks.push(code` - message.${fieldName} = (object.${fieldName} !== undefined && object.${fieldName} !== null) - ? ${readSnippet(`object.${fieldName}`)} + ${messageProperty} = (${objectProperty} !== undefined && ${objectProperty} !== null) + ? ${readSnippet(`${objectProperty}`)} : ${fallback}; `); } diff --git a/src/utils.ts b/src/utils.ts index c27baea46..dafeadd11 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,7 +10,7 @@ import { import ReadStream = NodeJS.ReadStream; import { SourceDescription } from "./sourceInfo"; import { Options, ServiceOption } from "./options"; -import { camelCaseGrpc, snakeToCamel } from "./case"; +import { camelCaseGrpc, maybeSnakeToCamel, snakeToCamel } from "./case"; export function protoFilesToGenerate(request: CodeGeneratorRequest): FileDescriptorProto[] { return request.protoFile.filter((f) => request.fileToGenerate.includes(f.name)); @@ -219,8 +219,12 @@ export class FormattedMethodDescriptor implements MethodDescriptorProto { export function getFieldJsonName( field: Pick, - options: Pick, + options: Pick, ): string { + // use "json_name" defined in a proto file + if (options.useJsonName) { + return field.jsonName; + } // jsonName will be camelCased by the protocol compiler, plus can be overridden by the user, // so just use that instead of our own maybeSnakeToCamel if (options.snakeToCamel.includes("json")) { @@ -234,6 +238,35 @@ export function getFieldJsonName( } } +export function propertyNameComposition( + field: Pick, + options: Pick, +): { propertyName: string; validatedPropertyName: string } { + if (options.useJsonName) { + const jsonName = field.jsonName; + return { + propertyName: jsonName, + validatedPropertyName: validateObjectProperty(jsonName), + }; + } + const propertyName = maybeSnakeToCamel(field.name, options); + return { + propertyName, + validatedPropertyName: propertyName, + }; +} + +/** + * https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/the-dangers-of-square-bracket-notation.md + */ +function isValidateObjectProperty(propertyName: string): boolean { + return /^[a-zA-Z_$][\w$]*$/.test(propertyName); +} + +function validateObjectProperty(propertyName: string): string { + return isValidateObjectProperty(propertyName) ? propertyName : JSON.stringify(propertyName); +} + /** * Returns a snippet for reading an object's property, such as `foo.bar`, or `foo['bar']` if the property name contains unusual characters. * For simplicity, we don't match the ECMA 5/6 rules for valid identifiers exactly, and return array syntax liberally. @@ -242,10 +275,9 @@ export function getFieldJsonName( * @param optional */ export function getPropertyAccessor(objectName: string, propertyName: string, optional: boolean = false): string { - let validIdentifier = /^[a-zA-Z_$][\w$]*$/; - return validIdentifier.test(propertyName) + return isValidateObjectProperty(propertyName) ? `${objectName}${optional ? "?" : ""}.${propertyName}` - : `${objectName}${optional ? "?." : ""}[${JSON.stringify(propertyName)}]`; + : `${objectName}${optional ? "?." : ""}[${validateObjectProperty(propertyName)}]`; } export function impFile(options: Options, spec: string) {