From 7bc20cd1741ebd9efb44b26c610e352ac8aacd7e Mon Sep 17 00:00:00 2001 From: Timo Stamm Date: Thu, 16 Sep 2021 16:23:07 +0200 Subject: [PATCH 1/2] Start updating oneof representation First steps for #143. --- packages/runtime/src/oneof.ts | 12 ++++++------ packages/runtime/src/unknown-types.ts | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/oneof.ts b/packages/runtime/src/oneof.ts index 87feebcf..882e32f1 100644 --- a/packages/runtime/src/oneof.ts +++ b/packages/runtime/src/oneof.ts @@ -13,16 +13,16 @@ import type {UnknownEnum, UnknownMessage, UnknownOneofGroup, UnknownScalar} from * * 1) Must be an object. * - * 2) Must have a "oneofKind" discriminator property. + * 2) Must have a "kind" discriminator property. * - * 3) If "oneofKind" is `undefined`, no member field is selected. The object + * 3) If "kind" is `undefined`, no member field is selected. The object * must not have any other properties. * - * 4) If "oneofKind" is a `string`, the member field with this name is + * 4) If "kind" is a `string`, the member field with this name is * selected. * * 5) If a member field is selected, the object must have a second property - * with this name. The property must not be `undefined`. + * `value` that contains the field's value. The property must not be `undefined`. * * 6) No extra properties are allowed. The object has either one property * (no selection) or two properties (selection). @@ -32,9 +32,9 @@ export function isOneofGroup(any: any): any is UnknownOneofGroup { if (typeof any != 'object' || any === null || !any.hasOwnProperty('oneofKind')) { return false; } - switch (typeof any.oneofKind) { + switch (typeof any.kind) { case "string": - if (any[any.oneofKind] === undefined) + if (any.value === undefined) return false; return Object.keys(any).length == 2; case "undefined": diff --git a/packages/runtime/src/unknown-types.ts b/packages/runtime/src/unknown-types.ts index 9f68a717..997bd9a3 100644 --- a/packages/runtime/src/unknown-types.ts +++ b/packages/runtime/src/unknown-types.ts @@ -44,10 +44,9 @@ export type UnknownEnum = number; * A unknown oneof group. See `isOneofGroup()` for details. */ export type UnknownOneofGroup = { - oneofKind: undefined | string; - [k: string]: + kind: undefined | string; + value?: | UnknownScalar | UnknownEnum - | UnknownMessage - | undefined; + | UnknownMessage; }; From 6c05ece2691354370bed762d9fe96c99c52609e7 Mon Sep 17 00:00:00 2001 From: James Wyatt Cready-Pyle Date: Thu, 17 Nov 2022 10:24:14 -0500 Subject: [PATCH 2/2] Update oneof representation for v3 (#262) --- MANUAL.md | 10 +- .../browser-client/index.ts | 10 +- .../browser-client/service-chat.ts | 15 +-- packages/example-chat-system/server/server.ts | 12 +-- .../server/service-chat.ts | 39 +++---- .../terminal-client/client.ts | 8 +- .../terminal-client/service-chat.ts | 15 +-- packages/plugin/spec/interpreter.spec.ts | 73 ++++++++++++- .../code-gen/message-interface-generator.ts | 23 ++-- packages/plugin/src/interpreter.ts | 52 ++++++--- .../message-type-extensions/google-types.ts | 10 +- .../internal-binary-read.ts | 48 ++++++--- .../internal-binary-write.ts | 12 +-- .../well-known-types.ts | 28 +++-- packages/plugin/src/our-options.ts | 2 +- .../src/lib/pb-date-pipe.spec.ts | 6 +- .../runtime-angular/src/lib/pb-date-pipe.ts | 20 ++-- packages/runtime/spec/message-type.spec.ts | 16 +-- packages/runtime/spec/oneof.spec.ts | 100 ++++++++---------- .../spec/reflection-json-reader.spec.ts | 10 +- .../spec/reflection-type-check.spec.ts | 8 +- packages/runtime/src/message-type-contract.ts | 4 +- packages/runtime/src/oneof.ts | 67 +++++------- .../runtime/src/reflection-binary-reader.ts | 10 +- .../runtime/src/reflection-binary-writer.ts | 4 +- packages/runtime/src/reflection-create.ts | 2 +- packages/runtime/src/reflection-equals.ts | 13 ++- .../runtime/src/reflection-json-reader.ts | 9 +- .../runtime/src/reflection-json-writer.ts | 4 +- .../runtime/src/reflection-merge-partial.ts | 12 ++- packages/runtime/src/reflection-type-check.ts | 6 +- packages/runtime/src/unknown-types.ts | 7 +- packages/test-fixtures/msg-oneofs.fixtures.ts | 10 +- packages/test-fixtures/msg-oneofs.proto | 2 +- packages/test-fixtures/name-clash.proto | 48 ++++++++- packages/test-generated/conformance_ts.ts | 38 +++---- .../spec/google.protobuf.struct.spec.ts | 32 +++--- .../spec/google.type.datetime.spec.ts | 24 ++--- .../spec/service-annotated.spec.ts | 12 +-- 39 files changed, 484 insertions(+), 337 deletions(-) diff --git a/MANUAL.md b/MANUAL.md index 855c8287..effe33a1 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -636,7 +636,7 @@ idiosyncrasies. message OneofExample { // Only one of (or none of) the fields can be set oneof result { - int32 value = 1; + int32 int = 1; string error = 2; } } @@ -645,13 +645,13 @@ Compiles the `oneof` group to a union type that ensures that only one member field is set: ```ts interface OneofExample { - result: { oneofKind: "value"; value: number; } - | { oneofKind: "error"; error: string; } - | { oneofKind: undefined; }; + result: { kind: "int"; value: number; } + | { kind: "error"; value: string; } + | { kind: undefined; }; } let message: OneofExample; -if (message.result.oneofKind === "value") { +if (message.result.kind === "int") { message.result.value // the union has been narrowed down } ``` diff --git a/packages/example-chat-system/browser-client/index.ts b/packages/example-chat-system/browser-client/index.ts index c34554c9..69164214 100644 --- a/packages/example-chat-system/browser-client/index.ts +++ b/packages/example-chat-system/browser-client/index.ts @@ -43,16 +43,14 @@ joinPanel.startCallback = async (username) => { // print all chat events call.responses.onMessage(message => { - switch (message.event.oneofKind) { + switch (message.event.kind) { case "joined": - chatPanel.addOther(message.event.joined); - break; case "left": - chatPanel.addOther(message.event.left); + chatPanel.addOther(message.event.value); break; case "message": - chatPanel.addMessage(message.username, message.event.message); - console.log(`${message.username}: ${message.event.message}`); + chatPanel.addMessage(message.username, message.event.value); + console.log(`${message.username}: ${message.event.value}`); break; } }); diff --git a/packages/example-chat-system/browser-client/service-chat.ts b/packages/example-chat-system/browser-client/service-chat.ts index 6a4d903f..d951b1c9 100644 --- a/packages/example-chat-system/browser-client/service-chat.ts +++ b/packages/example-chat-system/browser-client/service-chat.ts @@ -24,25 +24,26 @@ export interface ChatEvent { * @generated from protobuf oneof: event */ event: { - oneofKind: "joined"; + kind: "joined"; /** * @generated from protobuf field: string joined = 2; */ - joined: string; + value: string; } | { - oneofKind: "message"; + kind: "message"; /** * @generated from protobuf field: string message = 3; */ - message: string; + value: string; } | { - oneofKind: "left"; + kind: "left"; /** * @generated from protobuf field: string left = 4; */ - left: string; + value: string; } | { - oneofKind: undefined; + kind: undefined; + value?: never; }; } /** diff --git a/packages/example-chat-system/server/server.ts b/packages/example-chat-system/server/server.ts index c96f629c..aadc5600 100644 --- a/packages/example-chat-system/server/server.ts +++ b/packages/example-chat-system/server/server.ts @@ -26,8 +26,8 @@ class ChatService implements IChatService { await this.broadcast({ username: request.username, event: { - oneofKind: "joined", - joined: `${request.username} joined the chat` + kind: "joined", + value: `${request.username} joined the chat` } }); @@ -37,8 +37,8 @@ class ChatService implements IChatService { await this.broadcast({ username: request.username, event: { - oneofKind: "left", - left: `${request.username} left the chat` + kind: "left", + value: `${request.username} left the chat` } }); @@ -54,8 +54,8 @@ class ChatService implements IChatService { await this.broadcast({ username: user.username, event: { - oneofKind: 'message', - message: request.message + kind: 'message', + value: request.message } }); diff --git a/packages/example-chat-system/server/service-chat.ts b/packages/example-chat-system/server/service-chat.ts index 5abe043a..1a82bca8 100644 --- a/packages/example-chat-system/server/service-chat.ts +++ b/packages/example-chat-system/server/service-chat.ts @@ -30,25 +30,26 @@ export interface ChatEvent { * @generated from protobuf oneof: event */ event: { - oneofKind: "joined"; + kind: "joined"; /** * @generated from protobuf field: string joined = 2; */ - joined: string; + value: string; } | { - oneofKind: "message"; + kind: "message"; /** * @generated from protobuf field: string message = 3; */ - message: string; + value: string; } | { - oneofKind: "left"; + kind: "left"; /** * @generated from protobuf field: string left = 4; */ - left: string; + value: string; } | { - oneofKind: undefined; + kind: undefined; + value?: never; }; } /** @@ -125,20 +126,20 @@ class ChatEvent$Type extends MessageType { break; case /* string joined */ 2: message.event = { - oneofKind: "joined", - joined: reader.string() + kind: "joined", + value: reader.string() }; break; case /* string message */ 3: message.event = { - oneofKind: "message", - message: reader.string() + kind: "message", + value: reader.string() }; break; case /* string left */ 4: message.event = { - oneofKind: "left", - left: reader.string() + kind: "left", + value: reader.string() }; break; default: @@ -157,14 +158,14 @@ class ChatEvent$Type extends MessageType { if (message.username !== "") writer.tag(1, WireType.LengthDelimited).string(message.username); /* string joined = 2; */ - if (message.event.oneofKind === "joined") - writer.tag(2, WireType.LengthDelimited).string(message.event.joined); + if (message.event.kind === "joined") + writer.tag(2, WireType.LengthDelimited).string(message.event.value); /* string message = 3; */ - if (message.event.oneofKind === "message") - writer.tag(3, WireType.LengthDelimited).string(message.event.message); + if (message.event.kind === "message") + writer.tag(3, WireType.LengthDelimited).string(message.event.value); /* string left = 4; */ - if (message.event.oneofKind === "left") - writer.tag(4, WireType.LengthDelimited).string(message.event.left); + if (message.event.kind === "left") + writer.tag(4, WireType.LengthDelimited).string(message.event.value); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); diff --git a/packages/example-chat-system/terminal-client/client.ts b/packages/example-chat-system/terminal-client/client.ts index 5fdba8b8..c38caefe 100644 --- a/packages/example-chat-system/terminal-client/client.ts +++ b/packages/example-chat-system/terminal-client/client.ts @@ -32,15 +32,13 @@ async function main(term: TerminalIO, client: IChatServiceClient) { // print all chat events call.responses.onMessage(message => { - switch (message.event.oneofKind) { + switch (message.event.kind) { case "joined": - term.print(`* ${message.event.joined}`); - break; case "left": - term.print(`* ${message.event.left}`); + term.print(`* ${message.event.value}`); break; case "message": - term.print(`${message.username}: ${message.event.message}`); + term.print(`${message.username}: ${message.event.value}`); break; } }); diff --git a/packages/example-chat-system/terminal-client/service-chat.ts b/packages/example-chat-system/terminal-client/service-chat.ts index 6a4d903f..d951b1c9 100644 --- a/packages/example-chat-system/terminal-client/service-chat.ts +++ b/packages/example-chat-system/terminal-client/service-chat.ts @@ -24,25 +24,26 @@ export interface ChatEvent { * @generated from protobuf oneof: event */ event: { - oneofKind: "joined"; + kind: "joined"; /** * @generated from protobuf field: string joined = 2; */ - joined: string; + value: string; } | { - oneofKind: "message"; + kind: "message"; /** * @generated from protobuf field: string message = 3; */ - message: string; + value: string; } | { - oneofKind: "left"; + kind: "left"; /** * @generated from protobuf field: string left = 4; */ - left: string; + value: string; } | { - oneofKind: undefined; + kind: undefined; + value?: never; }; } /** diff --git a/packages/plugin/spec/interpreter.spec.ts b/packages/plugin/spec/interpreter.spec.ts index dcb9434c..8a306eb0 100644 --- a/packages/plugin/spec/interpreter.spec.ts +++ b/packages/plugin/spec/interpreter.spec.ts @@ -1,6 +1,6 @@ import {getFixtureFileDescriptor} from "./support/helpers"; import {DescriptorRegistry} from "@protobuf-ts/plugin-framework"; -import {Interpreter} from "../src/interpreter"; +import {Interpreter, reservedObjectProperties, reservedClassProperties} from "../src/interpreter"; import * as rt from "@protobuf-ts/runtime"; @@ -12,7 +12,7 @@ describe('interpreter', function () { const interpreter = new Interpreter(registry, { normalLongType, synthesizeEnumZeroValue: 'UNSPECIFIED$', - oneofKindDiscriminator: 'oneofKind', + oneofKindDiscriminator: 'kind', forceExcludeAllOptions: false, keepEnumPrefix: false, useProtoFieldName: false, @@ -25,8 +25,77 @@ describe('interpreter', function () { expectFieldType(messageType, 'fixed64_field_min', rt.LongType.BIGINT); }); }); + + describe('name clashes', function () { + const registry = DescriptorRegistry.createFrom(getFixtureFileDescriptor("name-clash.proto")); + const interpreter = new Interpreter(registry, { + normalLongType: rt.LongType.NUMBER, + synthesizeEnumZeroValue: 'UNSPECIFIED$', + oneofKindDiscriminator: 'kind', + forceExcludeAllOptions: false, + keepEnumPrefix: false, + useProtoFieldName: false, + }); + + it('ReservedFieldNames is escaped', function () { + const messageType = interpreter.getMessageType('spec.ReservedFieldNames'); + messageType.fields.forEach((field) => { + expect(reservedObjectProperties.has(field.localName)).toBeFalse(); + }); + expect(getLocalName(messageType, 'kind')).toBe('kind'); + }); + + it('ReservedFieldNamesInOneof is escaped', function () { + const messageType = interpreter.getMessageType('spec.ReservedFieldNamesInOneof'); + messageType.fields.forEach((field) => { + expect(reservedObjectProperties.has(field.localName)).toBeFalse(); + }); + expect(getLocalName(messageType, 'kind')).toBe('kind'); + }); + + it('NameClashService is escaped', function () { + const serviceType = interpreter.getServiceType('spec.NameClashService'); + serviceType.methods.forEach((field) => { + expect(reservedClassProperties.has(field.localName)).toBeFalse(); + }); + }) + + describe('oneof descriminator kind', function () { + it('is escaped in OneofDiscriminatorClash', function () { + const messageType = interpreter.getMessageType('spec.OneofDiscriminatorClash'); + expect(getLocalName(messageType, 'kind')).toBe('kind$'); + }); + + it('is escaped in OneofDiscriminatorClashInOneof', function () { + const messageType = interpreter.getMessageType('spec.OneofDiscriminatorClashInOneof'); + expect(getLocalName(messageType, 'kind')).toBe('kind$'); + }); + + it('is escaped in OneofDiscriminatorClashNumber', function () { + const messageType = interpreter.getMessageType('spec.OneofDiscriminatorClashNumber'); + expect(getLocalName(messageType, 'kind')).toBe('kind$'); + }); + + it('is NOT escaped in NoOneofDiscriminatorClashNumber', function () { + const messageType = interpreter.getMessageType('spec.NoOneofDiscriminatorClashNumber'); + expect(getLocalName(messageType, 'kind')).toBe('kind'); + }); + + it('is NOT escaped in NoOneofDiscriminatorClashRepeated', function () { + const messageType = interpreter.getMessageType('spec.NoOneofDiscriminatorClashRepeated'); + expect(getLocalName(messageType, 'kind')).toBe('kind'); + }); + }) + }); }); +// Expect to find a scalar field `name` of type `type`. +function getLocalName(messageType: rt.IMessageType, name: string) { + const field = messageType.fields.find(f => f.name === name); + expect(field).toBeDefined(); + return field!.localName; +} + // Expect to find a scalar field `name` of type `type`. function expectFieldType(messageType: rt.IMessageType, name: string, type: rt.LongType) { const field = messageType.fields.find(f => f.name === name); diff --git a/packages/plugin/src/code-gen/message-interface-generator.ts b/packages/plugin/src/code-gen/message-interface-generator.ts index f66864a9..7ee0cafe 100644 --- a/packages/plugin/src/code-gen/message-interface-generator.ts +++ b/packages/plugin/src/code-gen/message-interface-generator.ts @@ -166,7 +166,7 @@ export class MessageInterfaceGenerator extends GeneratorBase { // create property const property = ts.createPropertySignature( undefined, - ts.createIdentifier(fieldInfo.localName), + ts.createIdentifier(fieldInfo.oneof ? "value" : fieldInfo.localName), questionToken, type, undefined @@ -181,15 +181,15 @@ export class MessageInterfaceGenerator extends GeneratorBase { * For the following .proto: * * oneof result { - * int32 value = 1; + * int32 int = 1; * string error = 2; * } * * We generate the following property signature: * - * result: { oneofKind: "value"; value: number; } - * | { oneofKind: "error"; error: string; } - * | { oneofKind: undefined; }; + * result: { kind: "int"; value: number; } + * | { kind: "error"; value: string; } + * | { kind: undefined; value?: never }; */ protected createOneofADTPropertySignature(source:TypescriptFile, oneofDescriptor: OneofDescriptorProto): ts.PropertySignature { const @@ -202,7 +202,7 @@ export class MessageInterfaceGenerator extends GeneratorBase { // create a type for each selection case for (let fieldInfo of memberFieldInfos) { - // { oneofKind: 'fieldName' ... } part + // { kind: 'fieldName' ... } part const kindProperty = ts.createPropertySignature( undefined, ts.createIdentifier(this.options.oneofKindDiscriminator), @@ -211,7 +211,7 @@ export class MessageInterfaceGenerator extends GeneratorBase { undefined ); - // { ..., fieldName: type } part + // { ..., value: type } part let fieldDescriptor = messageDescriptor.field.find(fd => fd.number === fieldInfo.no); assert(fieldDescriptor !== undefined); let valueProperty = this.createFieldPropertySignature(source, fieldDescriptor, fieldInfo); @@ -223,7 +223,7 @@ export class MessageInterfaceGenerator extends GeneratorBase { } - // case for no selection: { oneofKind: undefined; } + // case for no selection: { kind: undefined; value?: never } oneofCases.push( ts.createTypeLiteralNode([ ts.createPropertySignature( @@ -232,6 +232,13 @@ export class MessageInterfaceGenerator extends GeneratorBase { undefined, ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), undefined + ), + ts.createPropertySignature( + undefined, + ts.createIdentifier("value"), + ts.createToken(ts.SyntaxKind.QuestionToken), + ts.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), + undefined ) ]) ); diff --git a/packages/plugin/src/interpreter.ts b/packages/plugin/src/interpreter.ts index 0a51d0f9..aa2bd026 100644 --- a/packages/plugin/src/interpreter.ts +++ b/packages/plugin/src/interpreter.ts @@ -17,6 +17,18 @@ import { FieldInfoGenerator } from "./code-gen/field-info-generator"; import {OurFileOptions, OurServiceOptions, readOurFileOptions, readOurServiceOptions} from "./our-options"; +const escapeCharacter = "$"; +export const reservedObjectProperties = new Set(["__proto__", "toString"]); +export const reservedClassProperties = new Set([ + // js built in + "__proto__", "toString", "name", "constructor", + // generic clients + "methods", "typeName", "options", "_transport", + // @grpc/grpc-js clients + "close", "getChannel", "waitForReady", "makeUnaryRequest", "makeClientStreamRequest", "makeServerStreamRequest", "makeBidiStreamRequest" +]); + + type JsonOptionsMap = { [extensionName: string]: rt.JsonValue } @@ -270,19 +282,10 @@ export class Interpreter { private static createTypescriptNameForMethod(descriptor: MethodDescriptorProto): string { - let escapeCharacter = '$'; - let reservedClassProperties = [ - // js built in - "__proto__", "toString", "name", "constructor", - // generic clients - "methods", "typeName", "options", "_transport", - // @grpc/grpc-js clients - "close", "getChannel", "waitForReady", "makeUnaryRequest", "makeClientStreamRequest", "makeServerStreamRequest", "makeBidiStreamRequest" - ]; let name = descriptor.name; assert(name !== undefined); name = rt.lowerCamelCase(name); - if (reservedClassProperties.includes(name)) { + if (reservedClassProperties.has(name)) { name = name + escapeCharacter; } return name; @@ -353,15 +356,11 @@ export class Interpreter { * adding '$' at the end * - don't have to escape reserved keywords */ - private createTypescriptNameForField(descriptor: FieldDescriptorProto | OneofDescriptorProto, escapeCharacter = '$'): string { - const reservedObjectProperties = '__proto__,toString'.split(','); + private createTypescriptNameForField(descriptor: FieldDescriptorProto | OneofDescriptorProto): string { let name = descriptor.name; assert(name !== undefined); name = FieldInfoGenerator.createTypescriptLocalName(name, this.options); - if (reservedObjectProperties.includes(name)) { - name = name + escapeCharacter; - } - if (this.options.oneofKindDiscriminator.split(',').includes(name)) { + if (reservedObjectProperties.has(name)) { name = name + escapeCharacter; } return name; @@ -370,6 +369,8 @@ export class Interpreter { // skips GROUP field type private buildFieldInfos(fieldDescriptors: readonly FieldDescriptorProto[]): rt.PartialFieldInfo[] { + const kind = this.options.oneofKindDiscriminator; + const has: Record = {}; const result: rt.PartialFieldInfo[] = []; for (const fd of fieldDescriptors) { if (this.registry.isGroupField(fd)) { @@ -379,9 +380,28 @@ export class Interpreter { } const fi = this.buildFieldInfo(fd); if (fi) { + const name = fi.localName ?? fi.name; + if (name === kind || name === "value") { + has[name] = fi; + } result.push(fi); } } + // Escape a field in this partial field info array if all of the following are true: + // 1. One of them has a name of "value" + // 2. Another one has a name matching the `oneofKindDiscriminator` + // 3. That other field is also a non-repeating string (or Long type represented with string) field + const fi = has[kind]; + if ( + fi && has.value && fi.kind === "scalar" && + (fi.repeat === undefined || fi.repeat === rt.RepeatType.NO) && + (fi.T === rt.ScalarType.STRING || ( + Interpreter.isLongValueType(fi.T) && + (fi.L !== rt.LongType.BIGINT && fi.L !== rt.LongType.NUMBER) + )) + ) { + fi.localName = kind + escapeCharacter; + } return result; } diff --git a/packages/plugin/src/message-type-extensions/google-types.ts b/packages/plugin/src/message-type-extensions/google-types.ts index 3538c71e..1454425c 100644 --- a/packages/plugin/src/message-type-extensions/google-types.ts +++ b/packages/plugin/src/message-type-extensions/google-types.ts @@ -202,10 +202,10 @@ export class GoogleTypes implements CustomMethodGenerator { ), to = message.${timeOffsetField}; if (to) { - if (to.oneofKind === "${timeZoneField}") + if (to.kind === "${timeZoneField}") throw new globalThis.Error("IANA time zone not supported"); - if (to.oneofKind === "${utcOffsetField}") { - let s = ${PbLong}.from(to.${utcOffsetField}.seconds).toNumber(); + if (to.kind === "${utcOffsetField}") { + let s = ${PbLong}.from(to.value.seconds).toNumber(); dt = new globalThis.Date(dt.getTime() - (s * 1000)); } } @@ -227,8 +227,8 @@ export class GoogleTypes implements CustomMethodGenerator { seconds: date.getSeconds(), nanos: date.getMilliseconds() * 1000, ${timeOffsetField}: { - oneofKind: "${utcOffsetField}", - ${utcOffsetField}: { + kind: "${utcOffsetField}", + value: { seconds: ${PbLong}.from(date.getTimezoneOffset() * 60).${longConvertMethod}(), nanos: 0, } diff --git a/packages/plugin/src/message-type-extensions/internal-binary-read.ts b/packages/plugin/src/message-type-extensions/internal-binary-read.ts index 0b827e90..e0e1cdc2 100644 --- a/packages/plugin/src/message-type-extensions/internal-binary-read.ts +++ b/packages/plugin/src/message-type-extensions/internal-binary-read.ts @@ -418,12 +418,16 @@ export class InternalBinaryRead implements CustomMethodGenerator { // message.result = { - // oneofKind: "msg", - // msg: OtherMessage.internalBinaryRead(reader, reader.uint32(), options, (message.result as any).msg) + // kind: "msg", + // value: OtherMessage.internalBinaryRead(reader, reader.uint32(), options, message.result.kind === "msg" ? message.result.value : undefined) // }; messageOneof(source:TypescriptFile,field: rt.FieldInfo & { kind: "message"; repeat: undefined | rt.RepeatType.NO; oneof: string; }): ts.Statement[] { let messageDescriptor = this.registry.resolveTypeName(field.T().typeName); assert(DescriptorProto.is(messageDescriptor)); + let groupPropertyAccess = ts.createPropertyAccess( + ts.createIdentifier("message"), + ts.createIdentifier(field.oneof) + ); let handlerMergeCall = ts.createCall( ts.createPropertyAccess( ts.createIdentifier(this.imports.type(source, messageDescriptor)), @@ -434,24 +438,34 @@ export class InternalBinaryRead implements CustomMethodGenerator { ts.createIdentifier("reader"), this.makeReaderCall("reader", rt.ScalarType.UINT32), ts.createIdentifier("options"), - ts.createPropertyAccess( - ts.createParen(ts.createAsExpression( - ts.createPropertyAccess(ts.createIdentifier("message"), field.oneof), - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - )), - ts.createIdentifier(field.localName) + ts.createConditional( + ts.createBinary( + ts.createPropertyAccess( + groupPropertyAccess, + ts.createIdentifier(this.options.oneofKindDiscriminator) + ), + ts.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), + ts.createStringLiteral(field.localName) + ), + ts.createToken(ts.SyntaxKind.QuestionToken), + ts.createPropertyAccess( + groupPropertyAccess, + ts.createIdentifier("value") + ), + ts.createToken(ts.SyntaxKind.ColonToken), + ts.createIdentifier("undefined") ) ] ); return [ts.createExpressionStatement(ts.createBinary( - ts.createPropertyAccess(ts.createIdentifier("message"), field.oneof), + groupPropertyAccess, ts.createToken(ts.SyntaxKind.EqualsToken), ts.createObjectLiteral( [ - // oneofKind: "msg", + // kind: "msg", ts.createPropertyAssignment(ts.createIdentifier(this.options.oneofKindDiscriminator), ts.createStringLiteral(field.localName)), - // msg: OtherMessage.internalBinaryRead(reader, reader.uint32(), options, (message.result as any).msg) - ts.createPropertyAssignment(field.localName, handlerMergeCall) + // value: OtherMessage.internalBinaryRead(reader, reader.uint32(), options, message.result.kind === "msg" ? message.result.value : undefined) + ts.createPropertyAssignment(ts.createIdentifier("value"), handlerMergeCall) ], true ) @@ -500,8 +514,8 @@ export class InternalBinaryRead implements CustomMethodGenerator { // message.result = { - // oneofKind: "err", - // err: reader.string() + // kind: "err", + // value: reader.string() // }; scalarOneof(field: rt.FieldInfo & { kind: "scalar" | "enum"; oneof: string; repeat: undefined | rt.RepeatType.NO }): ts.Statement[] { let type = field.kind == "enum" ? rt.ScalarType.INT32 : field.T; @@ -511,10 +525,10 @@ export class InternalBinaryRead implements CustomMethodGenerator { ts.createToken(ts.SyntaxKind.EqualsToken), ts.createObjectLiteral( [ - // oneofKind: "err", + // kind: "err", ts.createPropertyAssignment(ts.createIdentifier(this.options.oneofKindDiscriminator), ts.createStringLiteral(field.localName)), - // err: reader.string() - ts.createPropertyAssignment(field.localName, this.makeReaderCall("reader", type, longType)) + // value: reader.string() + ts.createPropertyAssignment("value", this.makeReaderCall("reader", type, longType)) ], true ) diff --git a/packages/plugin/src/message-type-extensions/internal-binary-write.ts b/packages/plugin/src/message-type-extensions/internal-binary-write.ts index 17764dbd..e99ed4f0 100644 --- a/packages/plugin/src/message-type-extensions/internal-binary-write.ts +++ b/packages/plugin/src/message-type-extensions/internal-binary-write.ts @@ -293,7 +293,7 @@ export class InternalBinaryWrite implements CustomMethodGenerator { ); let statement = ts.createIf( - // if (message.result.oneofKind === 'value') + // if (message.result.kind === 'value') ts.createBinary( ts.createPropertyAccess( groupPropertyAccess, @@ -302,14 +302,14 @@ export class InternalBinaryWrite implements CustomMethodGenerator { ts.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.createStringLiteral(field.localName) ), - // writer.tag( , ).string(message.stringField) + // writer.tag( , ).string(message.result.value) ts.createExpressionStatement( this.makeWriterCall( this.makeWriterTagCall(source, 'writer', field.no, this.wireTypeForSingleScalar(type)), type, ts.createPropertyAccess( groupPropertyAccess, - field.localName + ts.createIdentifier("value") ) ) ), @@ -422,7 +422,7 @@ export class InternalBinaryWrite implements CustomMethodGenerator { 'fork' ); - // MessageFieldMessage_TestMessage.internalBinaryWrite(message.., , options); + // MessageFieldMessage_TestMessage.internalBinaryWrite(message..value, , options); let binaryWrite = ts.createCall( ts.createPropertyAccess( ts.createIdentifier(this.imports.type(source, messageDescriptor)), @@ -432,7 +432,7 @@ export class InternalBinaryWrite implements CustomMethodGenerator { [ ts.createPropertyAccess( groupPropertyAccess, - field.localName + "value" ), writeTagAndFork, ts.createIdentifier("options") @@ -442,7 +442,7 @@ export class InternalBinaryWrite implements CustomMethodGenerator { // <...>.join() let binaryWriteAndJoin = this.makeWriterCall(binaryWrite, 'join'); - // if (message.objects.oneofKind === 'a') { + // if (message.objects.kind === 'a') { let statement = ts.createIf( ts.createBinary( ts.createPropertyAccess( diff --git a/packages/plugin/src/message-type-extensions/well-known-types.ts b/packages/plugin/src/message-type-extensions/well-known-types.ts index 49c7bcb1..dfaf4ffd 100644 --- a/packages/plugin/src/message-type-extensions/well-known-types.ts +++ b/packages/plugin/src/message-type-extensions/well-known-types.ts @@ -428,26 +428,24 @@ export class WellKnownTypes implements CustomMethodGenerator { * Encode \`${descriptor.name}\` to JSON value. */ function internalJsonWrite(message: ${Value}, options: ${JsonWriteOptions}): ${JsonValue} { - if (message.kind.oneofKind === undefined) throw new globalThis.Error(); - switch (message.kind.oneofKind) { + if (message.kind.kind === undefined) throw new globalThis.Error(); + switch (message.kind.kind) { case undefined: throw new globalThis.Error(); case "${boolValueField}": - return message.kind.${boolValueField}; - case "${nullValueField}": - return null; case "${numberValueField}": - return message.kind.${numberValueField}; case "${stringValueField}": - return message.kind.${stringValueField}; + return message.kind.value; + case "${nullValueField}": + return null; case "${listValueField}": let listValueField = this.fields.find(f => f.no === 6); if (listValueField?.kind !== 'message') throw new globalThis.Error(); - return listValueField.T().toJson(message.kind.${listValueField}); + return listValueField.T().toJson(message.kind.value); case "${structValueField}": let structValueField = this.fields.find(f => f.no === 5); if (structValueField?.kind !== 'message') throw new globalThis.Error(); - return structValueField.T().toJson(message.kind.${structValueField}); + return structValueField.T().toJson(message.kind.value); } } `, ` @@ -459,22 +457,22 @@ export class WellKnownTypes implements CustomMethodGenerator { target = this.create(); switch (typeof json) { case "number": - target.kind = {oneofKind: "${numberValueField}", ${numberValueField}: json}; + target.kind = {kind: "${numberValueField}", value: json}; break; case "string": - target.kind = {oneofKind: "${stringValueField}", ${stringValueField}: json}; + target.kind = {kind: "${stringValueField}", value: json}; break; case "boolean": - target.kind = {oneofKind: "${boolValueField}", ${boolValueField}: json}; + target.kind = {kind: "${boolValueField}", value: json}; break; case "object": if (json === null) { - target.kind = {oneofKind: "${nullValueField}", ${nullValueField}: NullValue.NULL_VALUE}; + target.kind = {kind: "${nullValueField}", value: NullValue.NULL_VALUE}; } else if (globalThis.Array.isArray(json)) { - target.kind = {oneofKind: "${listValueField}", ${listValueField}: ListValue.fromJson(json)}; + target.kind = {kind: "${listValueField}", value: ListValue.fromJson(json)}; } else { let val = Struct.fromJson(json); - target.kind = {oneofKind: "${structValueField}", ${structValueField}: Struct.fromJson(json)}; + target.kind = {kind: "${structValueField}", value: Struct.fromJson(json)}; } break; default: diff --git a/packages/plugin/src/our-options.ts b/packages/plugin/src/our-options.ts index 6c3893b3..37e0bb3a 100644 --- a/packages/plugin/src/our-options.ts +++ b/packages/plugin/src/our-options.ts @@ -253,7 +253,7 @@ export function makeInternalOptions( forcedServerStyle: undefined, emitAngularAnnotations: false, synthesizeEnumZeroValue: 'UNSPECIFIED$', - oneofKindDiscriminator: 'oneofKind', + oneofKindDiscriminator: 'kind', runtimeAngularImportPath: '@protobuf-ts/runtime-angular', runtimeRpcImportPath: '@protobuf-ts/runtime-rpc', angularCoreImportPath: '@angular/core', diff --git a/packages/runtime-angular/src/lib/pb-date-pipe.spec.ts b/packages/runtime-angular/src/lib/pb-date-pipe.spec.ts index 93b7ac58..fdedd9a2 100644 --- a/packages/runtime-angular/src/lib/pb-date-pipe.spec.ts +++ b/packages/runtime-angular/src/lib/pb-date-pipe.spec.ts @@ -13,7 +13,7 @@ describe('PbDatePipe', () => { minutes: 45, seconds: 59, nanos: 0, - timeOffset: {oneofKind: undefined} + timeOffset: {kind: undefined} }; let dateTimeOffset = { year: 2020, @@ -24,8 +24,8 @@ describe('PbDatePipe', () => { seconds: 59, nanos: 0, timeOffset: { - oneofKind: "utcOffset", - utcOffset: { + kind: "utcOffset", + value: { seconds: -3600, nanos: 0 } diff --git a/packages/runtime-angular/src/lib/pb-date-pipe.ts b/packages/runtime-angular/src/lib/pb-date-pipe.ts index b775800e..409363d5 100644 --- a/packages/runtime-angular/src/lib/pb-date-pipe.ts +++ b/packages/runtime-angular/src/lib/pb-date-pipe.ts @@ -22,10 +22,10 @@ export class PbDatePipe implements PipeTransform { if (isPbDateTime(value)) { let dt = new Date(value.year, value.month - 1, value.day, value.hours, value.minutes, value.seconds, value.nanos / 1000); if (value.timeOffset) { - if (value.timeOffset.oneofKind === "timeZone") { + if (value.timeOffset.kind === "timeZone") { throw new Error("Do not understand IANA time zone. Cannot convert to javascript Date."); - } else if (value.timeOffset.oneofKind === "utcOffset") { - let pbOffset = PbLong.from(value.timeOffset.utcOffset.seconds).toNumber() / 60; + } else if (value.timeOffset.kind === "utcOffset") { + let pbOffset = PbLong.from(value.timeOffset.value.seconds).toNumber() / 60; let jsOffset = dt.getTimezoneOffset(); dt.setMinutes(dt.getMinutes() + (pbOffset - jsOffset)) } @@ -96,10 +96,10 @@ function isPbDateTime(arg: any): arg is PbDateTime { return false; if (!isOneofGroup(arg.timeOffset)) return false; - let k = arg.timeOffset.oneofKind; + let k = arg.timeOffset.kind; if (k !== undefined && k != "timeZone" && k != "utcOffset") return false; - return arg.timeOffset.utcOffset === undefined || isPbDuration(arg.timeOffset.utcOffset); + return arg.timeOffset.value === undefined || isPbDuration(arg.timeOffset.value); } @@ -159,7 +159,7 @@ interface PbDateTime { * @generated from protobuf oneof: time_offset */ timeOffset: { - oneofKind: "utcOffset"; + kind: "utcOffset"; /** * UTC offset. Must be whole seconds, between -18 hours and +18 hours. * For example, a UTC offset of -4:00 would be represented as @@ -167,17 +167,17 @@ interface PbDateTime { * * @generated from protobuf field: google.protobuf.Duration utc_offset = 8; */ - utcOffset: PbDuration; + value: PbDuration; } | { - oneofKind: "timeZone"; + kind: "timeZone"; /** * Time zone. * * @generated from protobuf field: google.type.TimeZone time_zone = 9; */ - timeZone: any; + value: any; } | { - oneofKind: undefined; + kind: undefined; }; } diff --git a/packages/runtime/spec/message-type.spec.ts b/packages/runtime/spec/message-type.spec.ts index 327df0bd..5868d8b2 100644 --- a/packages/runtime/spec/message-type.spec.ts +++ b/packages/runtime/spec/message-type.spec.ts @@ -8,9 +8,9 @@ describe('MessageType', () => { boolField: boolean; repeatedInt32Field: number[], msg?: MyMessage; - result: { oneofKind: 'error'; readonly error: string; } - | { oneofKind: 'value'; readonly value: number; } - | { oneofKind: undefined; }; + result: { kind: 'error'; readonly value: string; } + | { kind: 'int'; readonly value: number; } + | { kind: undefined; }; messageMap: { [key: string]: MyMessage }; } @@ -20,7 +20,7 @@ describe('MessageType', () => { {no: 3, name: 'repeated_int32_field', kind: "scalar", T: ScalarType.INT32, repeat: RepeatType.PACKED}, {no: 4, name: 'msg', kind: "message", T: () => MyMessage}, {no: 5, name: 'error', kind: "scalar", T: ScalarType.STRING, oneof: 'result'}, - {no: 6, name: 'value', kind: "scalar", T: ScalarType.INT32, oneof: 'result'}, + {no: 6, name: 'int', kind: "scalar", T: ScalarType.INT32, oneof: 'result'}, { no: 7, name: 'message_map', kind: "map", K: ScalarType.STRING, V: {kind: "message", T: () => MyMessage} } @@ -30,7 +30,7 @@ describe('MessageType', () => { const msg = MyMessage.create({ stringField: "hello world", result: { - oneofKind: 'value', + kind: 'int', value: 123 }, messageMap: { @@ -45,7 +45,7 @@ describe('MessageType', () => { repeatedInt32Field: [], // msg: undefined, result: { - oneofKind: 'value', + kind: 'int', value: 123 }, messageMap: { @@ -53,7 +53,7 @@ describe('MessageType', () => { stringField: "", boolField: false, // msg: undefined, - result: {oneofKind: undefined}, + result: {kind: undefined}, messageMap: {}, repeatedInt32Field: [], } @@ -93,7 +93,7 @@ describe('MessageType', () => { stringField: "hello world", repeatedInt32Field: [], result: { - oneofKind: undefined + kind: undefined }, } expect(msg).toEqual(exp); diff --git a/packages/runtime/spec/oneof.spec.ts b/packages/runtime/spec/oneof.spec.ts index fbe174c4..d676a10b 100644 --- a/packages/runtime/spec/oneof.spec.ts +++ b/packages/runtime/spec/oneof.spec.ts @@ -13,14 +13,14 @@ describe('isOneofGroup()', function () { it('returns true for case "none"', () => { expect(isOneofGroup({ - oneofKind: undefined + kind: undefined })).toBeTrue(); }); it('returns true for a valid case', () => { expect(isOneofGroup({ - oneofKind: "error", - error: "error message" + kind: "error", + value: "error message" })).toBeTrue(); }); @@ -32,29 +32,22 @@ describe('isOneofGroup()', function () { it('returns false if discriminator is not a string', () => { expect(isOneofGroup({ - oneofKind: true, - true: 123 - })).toBeFalse(); - }); - - it('returns false if discriminator points to a missing property', () => { - expect(isOneofGroup({ - oneofKind: "error", - foo: 123 + kind: true, + value: 123 })).toBeFalse(); }); it('returns false if case "none" has extra properties', () => { expect(isOneofGroup({ - oneofKind: undefined, + kind: undefined, foo: 123 })).toBeFalse(); }); it('returns false if valid case has extra properties', () => { expect(isOneofGroup({ - oneofKind: "error", - error: "error message", + kind: "error", + value: "error message", foo: 123 })).toBeFalse(); }); @@ -63,14 +56,13 @@ describe('isOneofGroup()', function () { type ExampleOneof = - | { oneofKind: "a"; a: string; } - | { oneofKind: "b"; b: number; } - | { oneofKind: "c"; c: boolean; } - | { oneofKind: undefined; }; + | { kind: "a"; value: string; } + | { kind: "b"; value: number; } + | { kind: "c"; value: boolean; } + | { kind: undefined; value?: never }; type EmptyOneof = - | { oneofKind: undefined; }; - + | { kind: undefined; value?: never }; describe('clearOneofValue()', function () { let exampleOneof: ExampleOneof; @@ -78,33 +70,33 @@ describe('clearOneofValue()', function () { let unknownOneof: UnknownOneofGroup; beforeEach(() => { exampleOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; emptyOneof = { - oneofKind: undefined, + kind: undefined, }; unknownOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; }); it('clears empty oneof', () => { clearOneofValue(emptyOneof); - expect(emptyOneof.oneofKind).toBe(undefined); + expect(emptyOneof.kind).toBe(undefined); expect(isOneofGroup(emptyOneof)).toBeTrue(); }); it('clears example oneof', () => { clearOneofValue(exampleOneof); - expect(exampleOneof.oneofKind).toBe(undefined); + expect(exampleOneof.kind).toBe(undefined); expect(isOneofGroup(exampleOneof)).toBeTrue(); }); it('clears unknown oneof', () => { clearOneofValue(unknownOneof); - expect(unknownOneof.oneofKind).toBe(undefined); + expect(unknownOneof.kind).toBe(undefined); expect(isOneofGroup(unknownOneof)).toBeTrue(); }); @@ -116,39 +108,39 @@ describe('setOneofValue()', function () { let unknownOneof: UnknownOneofGroup; beforeEach(() => { exampleOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; emptyOneof = { - oneofKind: undefined, + kind: undefined, }; unknownOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; }); it('sets example oneof value', () => { setOneofValue(exampleOneof, "b", 1); - expect(exampleOneof.oneofKind).toBe("b"); - if (exampleOneof.oneofKind === "b") { - expect(exampleOneof.b).toBe(1); + expect(exampleOneof.kind).toBe("b"); + if (exampleOneof.kind === "b") { + expect(exampleOneof.value).toBe(1); } expect(isOneofGroup(exampleOneof)).toBeTrue(); }); it('sets example oneof other value', () => { setOneofValue(exampleOneof, "c", true); - expect(exampleOneof.oneofKind).toBe("c"); - if (exampleOneof.oneofKind === "c") { - expect(exampleOneof.c).toBeTrue() + expect(exampleOneof.kind).toBe("c"); + if (exampleOneof.kind === "c") { + expect(exampleOneof.value).toBeTrue() } expect(isOneofGroup(exampleOneof)).toBeTrue(); }); it('sets empty oneof value', () => { setOneofValue(emptyOneof, undefined); - expect(emptyOneof.oneofKind).toBe(undefined); + expect(emptyOneof.kind).toBe(undefined); expect(isOneofGroup(emptyOneof)).toBeTrue(); }); @@ -158,21 +150,21 @@ describe('setUnknownOneofValue()', function () { let unknownOneof: UnknownOneofGroup; beforeEach(() => { unknownOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; }); it('sets undefined', () => { setOneofValue(unknownOneof, undefined); - expect(unknownOneof.oneofKind).toBe(undefined); + expect(unknownOneof.kind).toBe(undefined); expect(isOneofGroup(unknownOneof)).toBeTrue(); }); it('sets defined', () => { setUnknownOneofValue(unknownOneof, "a", "x"); - expect(unknownOneof.oneofKind).toBe("a"); - expect(unknownOneof["a"]).toBe("x"); + expect(unknownOneof.kind).toBe("a"); + expect(unknownOneof["value"]).toBe("x"); expect(isOneofGroup(unknownOneof)).toBeTrue(); }); @@ -185,15 +177,15 @@ describe('getSelectedOneofValue()', function () { let unknownOneof: UnknownOneofGroup; beforeEach(() => { exampleOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; emptyOneof = { - oneofKind: undefined, + kind: undefined, }; unknownOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; }); @@ -210,7 +202,7 @@ describe('getSelectedOneofValue()', function () { }); it('returns unknown oneof value', () => { - const val: UnknownOneofGroup[string] = getSelectedOneofValue(unknownOneof); + const val: UnknownOneofGroup["value"] = getSelectedOneofValue(unknownOneof); expect(val).toBe("x"); expect(isOneofGroup(emptyOneof)).toBeTrue(); }); @@ -222,8 +214,8 @@ describe('getOneofValue()', function () { let exampleOneof: ExampleOneof; beforeEach(() => { exampleOneof = { - oneofKind: "a", - a: "x" + kind: "a", + value: "x" }; }); diff --git a/packages/runtime/spec/reflection-json-reader.spec.ts b/packages/runtime/spec/reflection-json-reader.spec.ts index 6be9beef..cc65bd8e 100644 --- a/packages/runtime/spec/reflection-json-reader.spec.ts +++ b/packages/runtime/spec/reflection-json-reader.spec.ts @@ -127,7 +127,7 @@ describe('ReflectionJsonReader', function () { it('throws on invalid input', () => { const output = fixtures.getMessage(typeName, 'default'); const input: JsonObject = { - value: 123, + int: 123, error: 'hello' }; expect(() => reader.read(input, output, jsonReadOptions())) @@ -137,18 +137,18 @@ describe('ReflectionJsonReader', function () { it('null selects kind with scalar default value', () => { const output = fixtures.getMessage(typeName, 'default'); const input: JsonObject = { - value: null, + int: null, }; reader.read(input, output, jsonReadOptions()); expect(output).toEqual({ - result: {oneofKind: 'value', value: 0} + result: {kind: 'int', value: 0} }); }); it('deletes existing oneof member', () => { const output: object = { result: { - oneofKind: 'value', + kind: 'int', value: 123 } }; @@ -157,7 +157,7 @@ describe('ReflectionJsonReader', function () { }; reader.read(input, output, jsonReadOptions()); expect(output).toEqual({ - result: {oneofKind: 'error', error: 'message'} + result: {kind: 'error', value: 'message'} }); }); diff --git a/packages/runtime/spec/reflection-type-check.spec.ts b/packages/runtime/spec/reflection-type-check.spec.ts index 90998369..0cb08d1b 100644 --- a/packages/runtime/spec/reflection-type-check.spec.ts +++ b/packages/runtime/spec/reflection-type-check.spec.ts @@ -81,7 +81,7 @@ describe('ReflectionTypeCheck.is()', function () { let check = new ReflectionTypeCheck(fixtures.makeMessageInfo('spec.OneofScalarMemberMessage')); let m = fixtures.getMessage('spec.OneofScalarMemberMessage', 'err'); assert(isOneofGroup(m.result)); - m.result.oneofKind = 'xxx'; + m.result.kind = 'xxx'; let is = check.is(m, depth, false); expect(is).toBe(false); }); @@ -90,7 +90,7 @@ describe('ReflectionTypeCheck.is()', function () { let check = new ReflectionTypeCheck(fixtures.makeMessageInfo('spec.OneofScalarMemberMessage')); let m = fixtures.getMessage('spec.OneofScalarMemberMessage', 'err'); assert(isOneofGroup(m.result)); - m.result.error = 123; + m.result.value = 123; let is = check.is(m, depth, false); expect(is).toBe(false); }); @@ -100,8 +100,8 @@ describe('ReflectionTypeCheck.is()', function () { let m = fixtures.getMessage('spec.OneofScalarMemberMessage', 'err'); assert(isOneofGroup(m.result)); m.result = { - oneofKind: 'wrong', - wrong: 123 + kind: 'wrong', + value: 123 }; let is = check.is(m, depth, false); expect(is).toBe(false); diff --git a/packages/runtime/src/message-type-contract.ts b/packages/runtime/src/message-type-contract.ts index 8a7b5d06..b3e4a347 100644 --- a/packages/runtime/src/message-type-contract.ts +++ b/packages/runtime/src/message-type-contract.ts @@ -22,8 +22,8 @@ export type PartialMessage = { T extends (Date | Uint8Array | bigint | boolean | string | number) ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> - : T extends { oneofKind: string } ? T - : T extends { oneofKind: undefined } ? T + : T extends { kind: string; value: unknown } ? T + : T extends { kind: undefined; value?: never } ? T : T extends object ? PartialMessage : T ; // @formatter:on diff --git a/packages/runtime/src/oneof.ts b/packages/runtime/src/oneof.ts index 882e32f1..ad9c8a45 100644 --- a/packages/runtime/src/oneof.ts +++ b/packages/runtime/src/oneof.ts @@ -1,6 +1,5 @@ import type {UnknownEnum, UnknownMessage, UnknownOneofGroup, UnknownScalar} from "./unknown-types"; - /** * Is the given value a valid oneof group? * @@ -29,12 +28,12 @@ import type {UnknownEnum, UnknownMessage, UnknownOneofGroup, UnknownScalar} from * */ export function isOneofGroup(any: any): any is UnknownOneofGroup { - if (typeof any != 'object' || any === null || !any.hasOwnProperty('oneofKind')) { + if (typeof any != 'object' || any === null || !any.hasOwnProperty('kind')) { return false; } switch (typeof any.kind) { case "string": - if (any.value === undefined) + if (any.value === undefined || !any.hasOwnProperty('value')) return false; return Object.keys(any).length == 2; case "undefined": @@ -49,16 +48,16 @@ export function isOneofGroup(any: any): any is UnknownOneofGroup { * Returns the value of the given field in a oneof group. */ export function getOneofValue( oneof: T, kind: K ): V | undefined { - return oneof[kind] as any; + if (oneof.kind === kind) return oneof.value as V; } @@ -70,27 +69,25 @@ export function getOneofValue(oneof: T, kind: K, value: V): void; export function setOneofValue(oneof: T, kind: undefined, value?: undefined): void; export function setOneofValue(oneof: any, kind: any, value?: any): void { - if (oneof.oneofKind !== undefined) { - delete oneof[oneof.oneofKind]; + if (oneof.kind !== undefined) { + delete oneof.value; } - oneof.oneofKind = kind; + oneof.kind = kind; if (value !== undefined) { - oneof[kind] = value; + oneof.value = value; } } @@ -102,12 +99,12 @@ export function setOneofValue(oneof: any, kind: any, value?: any): void { export function setUnknownOneofValue(oneof: UnknownOneofGroup, kind: string, value: UnknownScalar | UnknownEnum | UnknownMessage): void; export function setUnknownOneofValue(oneof: UnknownOneofGroup, kind: undefined, value?: undefined): void; export function setUnknownOneofValue(oneof: UnknownOneofGroup, kind?: string, value?: any): void { - if (oneof.oneofKind !== undefined) { - delete oneof[oneof.oneofKind]; + if (oneof.kind !== undefined) { + delete (oneof as any).value; } - oneof.oneofKind = kind; + oneof.kind = kind; if (value !== undefined && kind !== undefined) { - oneof[kind] = value; + oneof.value = value; } } @@ -119,14 +116,14 @@ export function setUnknownOneofValue(oneof: UnknownOneofGroup, kind?: string, va * a new object: * * ```ts - * message.result = { oneofKind: undefined }; + * message.result = { kind: undefined }; * ``` */ export function clearOneofValue(oneof: T) { - if (oneof.oneofKind !== undefined) { - delete oneof[oneof.oneofKind]; + if (oneof.kind !== undefined) { + delete (oneof as any).value; } - oneof.oneofKind = undefined; + oneof.kind = undefined; } @@ -134,12 +131,12 @@ export function clearOneofValue(oneof: T) { * Returns the selected value of the given oneof group. * * Not that the recommended way to access a oneof group is to check - * the "oneofKind" property and let TypeScript narrow down the union + * the "kind" property and let TypeScript narrow down the union * type for you: * * ```ts - * if (message.result.oneofKind === "error") { - * message.result.error; // string + * if (message.result.kind === "error") { + * message.result.value; // string * } * ``` * @@ -147,14 +144,8 @@ export function clearOneofValue(oneof: T) { * which protobuf field is selected, you can use this function * for convenience. */ -export function getSelectedOneofValue(oneof: T): V | undefined { - if (oneof.oneofKind === undefined) { - return undefined; - } - return oneof[oneof.oneofKind] as any; +export function getSelectedOneofValue(oneof: T): V { + return oneof.value as V; } diff --git a/packages/runtime/src/reflection-binary-reader.ts b/packages/runtime/src/reflection-binary-reader.ts index 69112393..e0c6b7a7 100644 --- a/packages/runtime/src/reflection-binary-reader.ts +++ b/packages/runtime/src/reflection-binary-reader.ts @@ -61,16 +61,18 @@ export class ReflectionBinaryReader { // target object for the field we are reading let target: UnknownMessage | UnknownOneofGroup = message as UnknownMessage, repeated = field.repeat, - localName = field.localName; + /** This can actually be any `string`, but we cast it to the literal `"value"` so we can index into oneof groups */ + localName = field.localName as "value"; // if field is member of oneof ADT, use ADT as target if (field.oneof) { target = target[field.oneof] as UnknownOneofGroup; // if other oneof member selected, set new ADT - if (target.oneofKind !== localName) + if (target.kind !== localName) target = (message as UnknownMessage)[field.oneof] = { - oneofKind: localName - }; + kind: localName + } as UnknownOneofGroup; + localName = "value"; } // we have handled oneof above, we just have read the value into `target[localName]` diff --git a/packages/runtime/src/reflection-binary-writer.ts b/packages/runtime/src/reflection-binary-writer.ts index c8aff619..2e2fe022 100644 --- a/packages/runtime/src/reflection-binary-writer.ts +++ b/packages/runtime/src/reflection-binary-writer.ts @@ -46,9 +46,9 @@ export class ReflectionBinaryWriter { // handle oneof ADT if (field.oneof) { const group = (message as UnknownMessage)[field.oneof] as UnknownOneofGroup; - if (group.oneofKind !== localName) + if (group.kind !== localName) continue; // if field is not selected, skip - value = group[localName]; + value = group.value; emitDefault = true; } else { value = (message as UnknownMessage)[localName]; diff --git a/packages/runtime/src/reflection-create.ts b/packages/runtime/src/reflection-create.ts index e316410a..1e65023b 100644 --- a/packages/runtime/src/reflection-create.ts +++ b/packages/runtime/src/reflection-create.ts @@ -15,7 +15,7 @@ export function reflectionCreate(type: IMessageType): T { if (field.opt) continue; if (field.oneof) - msg[field.oneof] = {oneofKind: undefined} as UnknownOneofGroup; + msg[field.oneof] = {kind: undefined} as UnknownOneofGroup; else if (field.repeat) msg[name] = []; else diff --git a/packages/runtime/src/reflection-equals.ts b/packages/runtime/src/reflection-equals.ts index 8eec1447..0f163d1d 100644 --- a/packages/runtime/src/reflection-equals.ts +++ b/packages/runtime/src/reflection-equals.ts @@ -16,9 +16,16 @@ export function reflectionEquals(info: MessageInfo, a: UnknownMessage | undefine if (!a || !b) return false; for (let field of info.fields) { - let localName = field.localName; - let val_a = field.oneof ? (a[field.oneof] as UnknownOneofGroup)[localName] : a[localName]; - let val_b = field.oneof ? (b[field.oneof] as UnknownOneofGroup)[localName] : b[localName]; + let val_a = a[field.localName]; + let val_b = b[field.localName]; + if (field.oneof) { + const group_a = (a[field.oneof] as UnknownOneofGroup); + const group_b = (b[field.oneof] as UnknownOneofGroup); + if (group_a.kind !== group_b.kind) + return false; + val_a = group_a.value; + val_b = group_b.value; + } switch (field.kind) { case "enum": case "scalar": diff --git a/packages/runtime/src/reflection-json-reader.ts b/packages/runtime/src/reflection-json-reader.ts index 0641148f..9a88ae01 100644 --- a/packages/runtime/src/reflection-json-reader.ts +++ b/packages/runtime/src/reflection-json-reader.ts @@ -73,7 +73,9 @@ export class ReflectionJsonReader { throw new Error(`Found unknown field while reading ${this.info.typeName} from JSON format. JSON key: ${jsonKey}`); continue; } - const localName = field.localName; + + /** This can actually be any `string`, but we cast it to the literal `"value"` so we can index into oneof groups */ + let localName = field.localName as "value"; // handle oneof ADT let target: UnknownMessage | UnknownOneofGroup; // this will be the target for the field value, whether it is member of a oneof or not @@ -82,8 +84,9 @@ export class ReflectionJsonReader { if (oneofsHandled.includes(field.oneof)) throw new Error(`Multiple members of the oneof group "${field.oneof}" of ${this.info.typeName} are present in JSON.`); oneofsHandled.push(field.oneof); target = (message as UnknownMessage)[field.oneof] = { - oneofKind: localName - } + kind: localName + } as UnknownOneofGroup; + localName = "value"; } else { target = message as UnknownMessage; } diff --git a/packages/runtime/src/reflection-json-writer.ts b/packages/runtime/src/reflection-json-writer.ts index 66a989ed..47e0862b 100644 --- a/packages/runtime/src/reflection-json-writer.ts +++ b/packages/runtime/src/reflection-json-writer.ts @@ -40,12 +40,12 @@ export class ReflectionJsonWriter { } // field is part of a oneof const group = source[field.oneof] as UnknownOneofGroup; - if (group.oneofKind !== field.localName) + if (group.kind !== field.localName) continue; // not selected, skip const opt = field.kind == 'scalar' || field.kind == 'enum' ? {...options, emitDefaultValues: true} // make sure to emit default values too : options; - let jsonValue = this.field(field, group[field.localName], opt); + let jsonValue = this.field(field, group.value, opt); assert(jsonValue !== undefined); json[options.useProtoFieldName ? field.name : field.jsonName] = jsonValue; } diff --git a/packages/runtime/src/reflection-merge-partial.ts b/packages/runtime/src/reflection-merge-partial.ts index b49125b7..b7344adf 100644 --- a/packages/runtime/src/reflection-merge-partial.ts +++ b/packages/runtime/src/reflection-merge-partial.ts @@ -25,18 +25,20 @@ export function reflectionMergePartial(info: MessageInfo, targ output: UnknownMessage | UnknownOneofGroup; // where we want our field value to go for (let field of info.fields) { - let name = field.localName; + /** This can actually be any `string`, but we cast it to the literal `"value"` so we can index into oneof groups */ + let name = field.localName as "value"; if (field.oneof) { const group = input[field.oneof] as UnknownOneofGroup | undefined; // this is the oneof`s group in the source - if (group === undefined) { // the user is free to omit + if (group?.kind !== name) { // the user is free to omit continue; // we skip this field, and all other members too } - fieldValue = group[name]; // our value comes from the the oneof group of the source + fieldValue = group.value; // our value comes from the the oneof group of the source output = (target as UnknownMessage)[field.oneof] as UnknownOneofGroup; // and our output is the oneof group of the target - output.oneofKind = group.oneofKind; // always update discriminator + output.kind = group.kind; // always update discriminator + name = "value"; // the field name for a oneof's value is always 'value' if (fieldValue === undefined) { - delete output[name]; // remove any existing value + delete output.value; // remove any existing value continue; // skip further work on field } } else { diff --git a/packages/runtime/src/reflection-type-check.ts b/packages/runtime/src/reflection-type-check.ts index 97fa5394..b166c0dd 100644 --- a/packages/runtime/src/reflection-type-check.ts +++ b/packages/runtime/src/reflection-type-check.ts @@ -105,12 +105,12 @@ export class ReflectionTypeCheck { const group = message[name]; if (!isOneofGroup(group)) return false; - if (group.oneofKind === undefined) + if (group.kind === undefined) continue; - const field = this.fields.find(f => f.localName === group.oneofKind); + const field = this.fields.find(f => f.localName === group.kind); if (!field) return false; // we found no field, but have a kind, something is wrong - if (!this.field(group[group.oneofKind], field, allowExcessProperties, depth)) + if (!this.field(group.value, field, allowExcessProperties, depth)) return false; } diff --git a/packages/runtime/src/unknown-types.ts b/packages/runtime/src/unknown-types.ts index 997bd9a3..64418644 100644 --- a/packages/runtime/src/unknown-types.ts +++ b/packages/runtime/src/unknown-types.ts @@ -44,8 +44,11 @@ export type UnknownEnum = number; * A unknown oneof group. See `isOneofGroup()` for details. */ export type UnknownOneofGroup = { - kind: undefined | string; - value?: + kind: undefined; + value?: never; +} | { + kind: string; + value: | UnknownScalar | UnknownEnum | UnknownMessage; diff --git a/packages/test-fixtures/msg-oneofs.fixtures.ts b/packages/test-fixtures/msg-oneofs.fixtures.ts index 12671c6b..908a7e05 100644 --- a/packages/test-fixtures/msg-oneofs.fixtures.ts +++ b/packages/test-fixtures/msg-oneofs.fixtures.ts @@ -8,15 +8,15 @@ export default f; f.push({ typeName: 'spec.OneofScalarMemberMessage', fields: [ - {no: 1, name: "value", oneof: "result", kind: "scalar", T: 5 /*int32*/}, + {no: 1, name: "int", oneof: "result", kind: "scalar", T: 5 /*int32*/}, {no: 2, name: "error", oneof: "result", kind: "scalar", T: 9 /*string*/} ], messages: { 'default': { - result: {oneofKind: undefined} + result: {kind: undefined} }, 'err': { - result: {oneofKind: 'error', error: 'hello'} + result: {kind: 'error', value: 'hello'} }, }, json: { @@ -34,10 +34,10 @@ f.push({ ], messages: { 'default': { - objects: {oneofKind: undefined} + objects: {kind: undefined} }, 'a': { - objects: {oneofKind: 'a', a: {name: 'A'}} + objects: {kind: 'a', value: {name: 'A'}} }, }, json: { diff --git a/packages/test-fixtures/msg-oneofs.proto b/packages/test-fixtures/msg-oneofs.proto index 5ed98f13..182a09f9 100644 --- a/packages/test-fixtures/msg-oneofs.proto +++ b/packages/test-fixtures/msg-oneofs.proto @@ -8,7 +8,7 @@ message OneofScalarMemberMessage { oneof result { // Contains the value if result available - int32 value = 1; + int32 int = 1; // Contains error message if result not available string error = 2; diff --git a/packages/test-fixtures/name-clash.proto b/packages/test-fixtures/name-clash.proto index f27dbc05..4dfcc490 100644 --- a/packages/test-fixtures/name-clash.proto +++ b/packages/test-fixtures/name-clash.proto @@ -8,14 +8,18 @@ message ReservedFieldNames { // not allowed as object property, should be escaped string to_string = 2; - // reserved for our ADT discriminator, should be escaped - string oneofKind = 3; + // not allowed as object property ONLY if a "value" field is also present + // see OneofDiscriminatorClash below for example escaping case + // should NOT be escaped + string kind = 3; } message ReservedFieldNamesInOneof { oneof oneof_group { - // reserved for our ADT discriminator, should be escaped - string oneofKind = 1; + // reserved for our ADT discriminator ONLY if a "value" field is also present + // see OneofDiscriminatorClashInOneof below for example escaping case + // should NOT be escaped + string kind = 1; // not allowed as object property, should be escaped string __proto__ = 2; @@ -25,6 +29,42 @@ message ReservedFieldNamesInOneof { } } +// reserved oneof group fields, only escaped when both "value" and "kind fields are present +message OneofDiscriminatorClash { + // should be escaped + string kind = 1; + string value = 2; + string other = 3; +} + +// reserved oneof group fields, only escaped when both "value" and "kind fields are present +message OneofDiscriminatorClashInOneof { + oneof oneof_group { + // should be escaped + string kind = 1; + string value = 2; + string other = 3; + } +} + +message OneofDiscriminatorClashNumber { + // should be escaped + int64 kind = 1 [jstype = JS_STRING]; + string value = 2; +} + +message NoOneofDiscriminatorClashNumber { + // should NOT be escaped + int64 kind = 1; + string value = 2; +} + +message NoOneofDiscriminatorClashRepeated { + // should NOT be escaped + repeated string kind = 1; + string value = 2; +} + // reserved word, should be escaped message interface { } diff --git a/packages/test-generated/conformance_ts.ts b/packages/test-generated/conformance_ts.ts index 0f2af45e..f43659f0 100644 --- a/packages/test-generated/conformance_ts.ts +++ b/packages/test-generated/conformance_ts.ts @@ -42,20 +42,20 @@ function doTest(request: ConformanceRequest): ConformanceResponse { default: return ConformanceResponse.create({ result: { - oneofKind: "runtimeError", - runtimeError: `unknown request message type ${request.messageType}` + kind: "runtimeError", + value: `unknown request message type ${request.messageType}` } }); } try { - switch (request.payload.oneofKind) { + switch (request.payload.kind) { case "protobufPayload": - testMessage = testMessageType.fromBinary(request.payload.protobufPayload); + testMessage = testMessageType.fromBinary(request.payload.value); break; case "jsonPayload": - testMessage = testMessageType.fromJsonString(request.payload.jsonPayload, { + testMessage = testMessageType.fromJsonString(request.payload.value, { ignoreUnknownFields: request.testCategory === TestCategory.JSON_IGNORE_UNKNOWN_PARSING_TEST, typeRegistry, }); @@ -64,8 +64,8 @@ function doTest(request: ConformanceRequest): ConformanceResponse { default: return ConformanceResponse.create({ result: { - oneofKind: "skipped", - skipped: `${request.payload.oneofKind} not supported.`, + kind: "skipped", + value: `${request.payload.kind} not supported.`, } }); @@ -73,8 +73,8 @@ function doTest(request: ConformanceRequest): ConformanceResponse { } catch (err) { return ConformanceResponse.create({ result: { - oneofKind: "parseError", - parseError: err.toString() + "\n" + err.stack, + kind: "parseError", + value: err.toString() + "\n" + err.stack, } }); } @@ -83,41 +83,41 @@ function doTest(request: ConformanceRequest): ConformanceResponse { switch (request.requestedOutputFormat) { case WireFormat.PROTOBUF: response.result = { - oneofKind: "protobufPayload", - protobufPayload: testMessageType.toBinary(testMessage), + kind: "protobufPayload", + value: testMessageType.toBinary(testMessage), }; break; case WireFormat.JSON: response.result = { - oneofKind: "jsonPayload", - jsonPayload: testMessageType.toJsonString(testMessage, { + kind: "jsonPayload", + value: testMessageType.toJsonString(testMessage, { typeRegistry }) }; break; case WireFormat.JSPB: - response.result = {oneofKind: "skipped", skipped: "JSPB not supported."}; + response.result = {kind: "skipped", value: "JSPB not supported."}; return response; case WireFormat.TEXT_FORMAT: - response.result = {oneofKind: "skipped", skipped: "Text format not supported."}; + response.result = {kind: "skipped", value: "Text format not supported."}; return response; default: return ConformanceResponse.create({ result: { - oneofKind: "runtimeError", - runtimeError: `unknown requested output format ${request.requestedOutputFormat}` + kind: "runtimeError", + value: `unknown requested output format ${request.requestedOutputFormat}` } }); } } catch (err) { return ConformanceResponse.create({ result: { - oneofKind: "serializeError", - serializeError: err.toString() + "\n" + err.stack + kind: "serializeError", + value: err.toString() + "\n" + err.stack } }); } diff --git a/packages/test-generated/spec/google.protobuf.struct.spec.ts b/packages/test-generated/spec/google.protobuf.struct.spec.ts index b2a308e9..7d75ae8f 100644 --- a/packages/test-generated/spec/google.protobuf.struct.spec.ts +++ b/packages/test-generated/spec/google.protobuf.struct.spec.ts @@ -6,32 +6,32 @@ describe('google.protobuf.Struct', function () { let fixMessage: Struct = { fields: { - "bool": {kind: {oneofKind: 'boolValue', boolValue: true}}, - "null": {kind: {oneofKind: 'nullValue', nullValue: NullValue.NULL_VALUE}}, - "string": {kind: {oneofKind: 'stringValue', stringValue: "a string"}}, - "number": {kind: {oneofKind: 'numberValue', numberValue: 123}}, + "bool": {kind: {kind: 'boolValue', value: true}}, + "null": {kind: {kind: 'nullValue', value: NullValue.NULL_VALUE}}, + "string": {kind: {kind: 'stringValue', value: "a string"}}, + "number": {kind: {kind: 'numberValue', value: 123}}, "list": { kind: { - oneofKind: 'listValue', - listValue: { + kind: 'listValue', + value: { values: [ - {kind: {oneofKind: 'boolValue', boolValue: true}}, - {kind: {oneofKind: 'nullValue', nullValue: NullValue.NULL_VALUE}}, - {kind: {oneofKind: 'stringValue', stringValue: "a string"}}, - {kind: {oneofKind: 'numberValue', numberValue: 123}}, + {kind: {kind: 'boolValue', value: true}}, + {kind: {kind: 'nullValue', value: NullValue.NULL_VALUE}}, + {kind: {kind: 'stringValue', value: "a string"}}, + {kind: {kind: 'numberValue', value: 123}}, ] } } }, "struct": { kind: { - oneofKind: 'structValue', - structValue: { + kind: 'structValue', + value: { fields: { - "bool": {kind: {oneofKind: 'boolValue', boolValue: true}}, - "null": {kind: {oneofKind: 'nullValue', nullValue: NullValue.NULL_VALUE}}, - "string": {kind: {oneofKind: 'stringValue', stringValue: "a string"}}, - "number": {kind: {oneofKind: 'numberValue', numberValue: 123}}, + "bool": {kind: {kind: 'boolValue', value: true}}, + "null": {kind: {kind: 'nullValue', value: NullValue.NULL_VALUE}}, + "string": {kind: {kind: 'stringValue', value: "a string"}}, + "number": {kind: {kind: 'numberValue', value: 123}}, } } } diff --git a/packages/test-generated/spec/google.type.datetime.spec.ts b/packages/test-generated/spec/google.type.datetime.spec.ts index 8fc909d2..8dd85c81 100644 --- a/packages/test-generated/spec/google.type.datetime.spec.ts +++ b/packages/test-generated/spec/google.type.datetime.spec.ts @@ -16,9 +16,9 @@ describe('google.type.DateTime', function () { expect(nowPb.hours).toBe(nowDate.getHours()); expect(nowPb.minutes).toBe(nowDate.getMinutes()); expect(nowPb.seconds).toBe(nowDate.getSeconds()); - expect(nowPb.timeOffset.oneofKind).toBe("utcOffset"); - if (nowPb.timeOffset.oneofKind === "utcOffset") { - let offsetSeconds = PbLong.from(nowPb.timeOffset.utcOffset.seconds).toNumber(); + expect(nowPb.timeOffset.kind).toBe("utcOffset"); + if (nowPb.timeOffset.kind === "utcOffset") { + let offsetSeconds = PbLong.from(nowPb.timeOffset.value.seconds).toNumber(); expect(offsetSeconds / 60).toBe(nowDate.getTimezoneOffset()); } }); @@ -36,7 +36,7 @@ describe('google.type.DateTime', function () { seconds: 59, nanos: 500 * 1000, timeOffset: { - oneofKind: undefined + kind: undefined } }); expect(dt).toBeInstanceOf(globalThis.Date); @@ -51,7 +51,7 @@ describe('google.type.DateTime', function () { seconds: 59, nanos: 500 * 1000, timeOffset: { - oneofKind: undefined + kind: undefined } }); expect(dt.getFullYear()).toBe(2020); @@ -72,8 +72,8 @@ describe('google.type.DateTime', function () { seconds: 59, nanos: 500 * 1000, timeOffset: { - oneofKind: "timeZone", - timeZone: { + kind: "timeZone", + value: { id: "foo", version: "bar" } @@ -94,8 +94,8 @@ describe('google.type.DateTime', function () { seconds: now.getUTCSeconds(), nanos: now.getUTCMilliseconds() * 1000, timeOffset: { - oneofKind: "utcOffset", - utcOffset: utcOffset + kind: "utcOffset", + value: utcOffset } }); expect(dt.getFullYear()).toBe(now.getFullYear()); @@ -119,9 +119,9 @@ describe('google.type.DateTime', function () { expect(dt.minutes).toBe(now.getMinutes()); expect(dt.seconds).toBe(now.getSeconds()); expect(dt.nanos).toBe(now.getMilliseconds() * 1000); - expect(dt.timeOffset.oneofKind).toBe("utcOffset"); - if (dt.timeOffset.oneofKind === "utcOffset") { - let act = dt.timeOffset.utcOffset; + expect(dt.timeOffset.kind).toBe("utcOffset"); + if (dt.timeOffset.kind === "utcOffset") { + let act = dt.timeOffset.value; let exp = makeDuration(now.getTimezoneOffset() * 60); expect(act).toEqual(exp); } diff --git a/packages/test-generated/spec/service-annotated.spec.ts b/packages/test-generated/spec/service-annotated.spec.ts index da5d6184..c8c74d20 100644 --- a/packages/test-generated/spec/service-annotated.spec.ts +++ b/packages/test-generated/spec/service-annotated.spec.ts @@ -62,18 +62,18 @@ describe('spec.AnnotatedService', function () { const rule = HttpRule.fromJson(mi.options["google.api.http"]); expect(rule).toEqual(HttpRule.create({ pattern: { - oneofKind: "get", - get: "/v1/{name=messages/*}" + kind: "get", + value: "/v1/{name=messages/*}" }, additionalBindings: [{ pattern: { - oneofKind: "get", - get: "xxx" + kind: "get", + value: "xxx" } }, { pattern: { - oneofKind: "get", - get: "yyy" + kind: "get", + value: "yyy" } }] }));