diff --git a/README.markdown b/README.markdown index 1a9f0b58d..9bf3c0b2e 100644 --- a/README.markdown +++ b/README.markdown @@ -22,10 +22,13 @@ - [Assumptions](#assumptions) - [Todo](#todo) - [OneOf Handling](#oneof-handling) -- [Primitive Types](#primitive-types) +- [Default Values and Unset Fields](#default-values-and-unset-fields) +- [Well-Known Types](#well-known-types) + - [Wrapper Types](#wrapper-types) + - [JSON Types (Struct Types)](#json-types-struct-types) + - [Timestamps](#timestamp) - [Wrapper Types](#wrapper-types) - [Number Types](#number-types) -- [Timestamps](#timestamps) - [Current Status of Optional Values](#current-status-of-optional-values) # Overview @@ -399,29 +402,201 @@ As this will automatically enforce only one of `field_a` or `field_b` "being set In ts-proto's currently-unscheduled 2.x release, `oneof=unions` will become the default behavior. -# Primitive Types +# Default values and unset fields -Protobuf has the somewhat annoying behavior that primitives types cannot differentiate between set-to-defalut-value and unset. +In core Protobuf, values that are _unset_ or equal to the default value are not sent over the wire. +The default value of a message is `undefined`. Primitive types take their natural default value, i.e. `string` is `''`, `number` is `0`, etc. +This behavior enables forward compatibility, as primitive fields will always have a value, even when omitted by outdated agents, but it also means _default_ and _unset_ values cannot be distinguished. -I.e. if you have a `string name = 1`, and set `object.name = ''`, Protobuf will skip sending the tagged `name` field over the wire, because its understood that readers on the other end will, when they see `name` is not included in the payload, return empty string. +If you need primitive fields where you can detect set/unset, see [Wrapper Types](#wrapper-types). -`ts-proto` models this behavior, of "unset" values being the primitive's default. (Technically by setting up an object prototype that knows the default values of the message's primitive fields.) +**Encode / Decode** -If you want fields where you can model set/unset, see Wrapper Types. +`ts-proto` follows the Protobuf rules, and always returns default values for unsets fields when decoding, while omitting them from the output when serialized in binary format. -# Wrapper Types +```protobuf +syntax = "proto3"; +message Foo { + string bar = 1; +} +``` + +```typescript +protobufBytes; // assume this is an empty Foo object, in protobuf binary format +Foo.decode(protobufBytes); // => { bar: '' } +``` + +```typescript +Foo.encode({ bar: '' }); // => { }, writes an empty Foo object, in protobuf binary format +``` + +**fromJSON / toJSON** + +Reading JSON will also initialize the default values. Since senders may either omit unset fields, or set them to the default value, use `fromJSON` to normalize the input. + +```typescript +Foo.fromJSON({ }); // => { bar: '' } +Foo.fromJSON({ bar: '' }); // => { bar: '' } +Foo.fromJSON({ bar: 'baz' }); // => { bar: 'baz' } +``` + +When writing JSON, `ts-proto` currently does **not** normalize message when converting to JSON, other than omitting unset fields, but it may do so in the future. + +```typescript +// Current ts-proto behavior +Foo.toJSON({ }); // => { } +Foo.toJSON({ bar: undefined }); // => { } +Foo.toJSON({ bar: '' }); // => { bar: '' } - note: this is the default value, but it's not omitted +Foo.toJSON({ bar: 'baz' }); // => { bar: 'baz' } +``` + +```typescript +// Possible future behavior, where ts-proto would normalize message +Foo.toJSON({ }); // => { } +Foo.toJSON({ bar: undefined }); // => { } +Foo.toJSON({ bar: '' }); // => { } - note: omitting the default value, as expected +Foo.toJSON({ bar: 'baz' }); // => { bar: 'baz' } +``` -In core Protobuf, unset primitive fields become their respective default values (so you loose ability to distinguish "unset" from "default"). +- Please open an issue if you need this behavior. -However, unset message fields stay `null`. +# Well-Known Types -This allows a cute hack where you can model a logical `string | unset` by creating a field that is technically a message (i.e. so it can stay `null` for the unset case), but the message only has a single string field (i.e for storing the value in the set case). +Protobuf comes with several predefined message definitions, called "[Well-Known Types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf)". +Their interpretation is defined by the Protobuf specification, and libraries are expected to convert these messages to corresponding native types in the target language. -Protobuf has already "blessed" this pattern with several built-in types, i.e. `google.protobuf.StringValue`, `google.protobuf.Int32Value`, etc. +`ts-proto` currently automatically converts these messages to their corresponding native types. -`ts-proto` understands these wrapper types and "re-idiomizes" them by generating a `google.protobuf.StringValue name = 1` field as a `name: string | undefined`, and hides the `StringValue` implementation detail from your code (i.e. during `encode`/`decode` of the `name` field on the wire to external consumers, it's still read/written as a `StringValue` message field). +- Wrapper Types: + + * [google.protobuf.DoubleValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#DoubleValue) ⇆ `number | undefined` + * [google.protobuf.FloatValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#FloatValue) ⇆ `number | undefined` + * [google.protobuf.Int64Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Int64Value) ⇆ `number | undefined` + * [google.protobuf.UInt64Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#UInt64Value) ⇆ `number | undefined` + * [google.protobuf.Int32Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Int32Value) ⇆ `number | undefined` + * [google.protobuf.UInt32Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#UInt32Value) ⇆ `number | undefined` + * [google.protobuf.BoolValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#BoolValue) ⇆ `boolean | undefined` + * [google.protobuf.StringValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#StringValue) ⇆ `string | undefined` + * [google.protobuf.BytesValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.BytesValue) ⇆ `Uint8Array | undefined` + +- JSON Types (Struct Types): + + * [google.protobuf.Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Value) ⇆ `any | undefined` (i.e. `number | string | boolean | null | array | object`) + * [google.protobuf.ListValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#ListValue) ⇆ `any[]` + * [google.protobuf.Struct](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Struct) ⇆ `{ [key: string]: any } | undefined` + +## Wrapper Types + +Wrapper Types are messages containing a single primitive field, and can be imported in `.proto` files with `import "google/protobuf/wrappers.proto"`. + + +Since these are _messages_, their default value is `undefined`, allowing you to distinguish unset primitives from their default values, when using Wrapper Types. +`ts-proto` generates these fields as ` | undefined`. + +For example: + +```protobuf +// Protobuf +syntax = "proto3"; + +import "google/protobuf/wrappers.proto"; + +message ExampleMessage { + google.protobuf.StringValue name = 1; +} +``` + +```typescript +// TypeScript +interface ExampleMessage { + name: string | undefined; +} +``` + +When encoding a message the primitive value is converted back to its corresponding wrapper type: + +```typescript +ExampleMessage.encode({ name: 'foo' }) // => { name: { value: 'foo' } }, in binary +``` + +When calling toJSON, the value is not converted, because wrapper types are idiomatic in JSON. + +```typescript +ExampleMessage.toJSON({ name: 'foo' }) // => { name: 'foo' } +``` + +## JSON Types (Struct Types) + +Protobuf's language and types are not sufficient to represent all possible JSON values, since JSON may contain values whose type is unknown in advance. +For this reason, Protobuf offers several additional types to represent arbitrary JSON values. + +These are called Struct Types, and can be imported in `.proto` files with `import "google/protobuf/struct.proto"`. + +- [google.protobuf.Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Value) ⇆ `any` + - This is the most general type, and can represent any JSON value (i.e. `number | string | boolean | null | array | object`). +- [google.protobuf.ListValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#ListValue) ⇆ `any[]` + - To represent a JSON array +- [google.protobuf.Struct](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Struct) ⇆ `{ [key: string]: any }` + - To represent a JSON object + +`ts-proto` automatically converts back and forth between these Struct Types and their corresponding JSON types. + +Example: + +```protobuf +// Protobuf +syntax = "proto3"; + +import "google/protobuf/struct.proto"; + +message ExampleMessage { + google.protobuf.Value anything = 1; +} +``` + +```typescript +// TypeScript +interface ExampleMessage { + anything: any | undefined; +} +``` + +Encoding a JSON value embedded in a message, converts it to a Struct Type: +```typescript +ExampleMessage.encode({ anything: { "name": "hello" } }) +/* Outputs the following structure, encoded in protobuf binary format: +{ + anything: Value { + structValue = Struct { + fields = [ + MapEntry { + key = "name", + value = Value { + stringValue = "hello" + } + ] + } + } + } +}*/ + +ExampleMessage.encode({ anything: true }) +/* Outputs the following structure encoded in protobuf binary format: +{ + anything: Value { + boolValue = true + } +}*/ +``` + +## Timestamp + +The representation of `google.protobuf.Timestamp` is configurable by the `useDate` flag. + +| Protobuf well-known type | Default/`useDate=true` | `useDate=false` | `useDate=string` | +| --------------------------- | ---------------------- | ------------------------------------ | ---------------- | +| `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` | -This makes dealing with `string | unset` in your code much nicer, albeit it's unfortunate that, in Protobuf core, this is not as simple as marking a `string name = 1` field as `optional`, i.e. you have to "dirty" your proto files a bit by knowing to use the `StringValue` convention. # Number Types @@ -452,14 +627,6 @@ The protobuf number types map to JavaScript types based on the `forceLong` confi Where (\*) indicates they might throw an error at runtime. -# Timestamps - -The representation of `google.protobuf.Timestamp` is configurable by the `useDate` flag. - -| Protobuf well-known type | Default/`useDate=true` | `useDate=false` | `useDate=string` | -| --------------------------- | ---------------------- | ------------------------------------ | ---------------- | -| `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` | - # Current Status of Optional Values - Required primitives: use as-is, i.e. `string name = 1`. diff --git a/integration/pbjs.sh b/integration/pbjs.sh index 8ed0a2036..5f93eb0dd 100755 --- a/integration/pbjs.sh +++ b/integration/pbjs.sh @@ -52,3 +52,11 @@ yarn pbts --no-comments -o integration/oneof-properties/pbjs.d.ts integration/on # oneof-unions/ yarn pbjs --force-message --force-number -t static-module -o integration/oneof-unions/pbjs.js integration/oneof-unions/oneof.proto yarn pbts --no-comments -o integration/oneof-unions/pbjs.d.ts integration/oneof-unions/pbjs.js + +# struct/ +yarn pbjs --force-message --force-number -t static-module -o integration/struct/pbjs.js integration/struct/struct.proto +yarn pbts --no-comments -o integration/struct/pbjs.d.ts integration/struct/pbjs.js + +# value/ +yarn pbjs --force-message --force-number -t static-module -o integration/value/pbjs.js integration/value/value.proto +yarn pbts --no-comments -o integration/value/pbjs.d.ts integration/value/pbjs.js diff --git a/integration/struct/google/protobuf/struct.ts b/integration/struct/google/protobuf/struct.ts new file mode 100644 index 000000000..d54b6ca9b --- /dev/null +++ b/integration/struct/google/protobuf/struct.ts @@ -0,0 +1,436 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; + +export const protobufPackage = 'google.protobuf'; + +/** + * `NullValue` is a singleton enumeration to represent the null value for the + * `Value` type union. + * + * The JSON representation for `NullValue` is JSON `null`. + */ +export enum NullValue { + /** NULL_VALUE - Null value. */ + NULL_VALUE = 0, + UNRECOGNIZED = -1, +} + +export function nullValueFromJSON(object: any): NullValue { + switch (object) { + case 0: + case 'NULL_VALUE': + return NullValue.NULL_VALUE; + case -1: + case 'UNRECOGNIZED': + default: + return NullValue.UNRECOGNIZED; + } +} + +export function nullValueToJSON(object: NullValue): string { + switch (object) { + case NullValue.NULL_VALUE: + return 'NULL_VALUE'; + default: + return 'UNKNOWN'; + } +} + +/** + * `Struct` represents a structured data value, consisting of fields + * which map to dynamically typed values. In some languages, `Struct` + * might be supported by a native representation. For example, in + * scripting languages like JS a struct is represented as an + * object. The details of that representation are described together + * with the proto support for the language. + * + * The JSON representation for `Struct` is JSON object. + */ +export interface Struct { + /** Unordered map of dynamically typed values. */ + fields: { [key: string]: any | undefined }; +} + +export interface Struct_FieldsEntry { + key: string; + value: any | undefined; +} + +/** + * `Value` represents a dynamically typed value which can be either + * null, a number, a string, a boolean, a recursive struct value, or a + * list of values. A producer of value is expected to set one of that + * variants, absence of any variant indicates an error. + * + * The JSON representation for `Value` is JSON value. + */ +export interface Value { + /** Represents a null value. */ + nullValue: NullValue | undefined; + /** Represents a double value. */ + numberValue: number | undefined; + /** Represents a string value. */ + stringValue: string | undefined; + /** Represents a boolean value. */ + boolValue: boolean | undefined; + /** Represents a structured value. */ + structValue: { [key: string]: any } | undefined; + /** Represents a repeated `Value`. */ + listValue: Array | undefined; +} + +/** + * `ListValue` is a wrapper around a repeated field of values. + * + * The JSON representation for `ListValue` is JSON array. + */ +export interface ListValue { + /** Repeated field of dynamically typed values. */ + values: any[]; +} + +const baseStruct: object = {}; + +export const Struct = { + encode(message: Struct, writer: Writer = Writer.create()): Writer { + Object.entries(message.fields).forEach(([key, value]) => { + if (value !== undefined) { + Struct_FieldsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).ldelim(); + } + }); + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Struct { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseStruct } as Struct; + message.fields = {}; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + const entry1 = Struct_FieldsEntry.decode(reader, reader.uint32()); + if (entry1.value !== undefined) { + message.fields[entry1.key] = entry1.value; + } + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Struct { + const message = { ...baseStruct } as Struct; + message.fields = {}; + if (object.fields !== undefined && object.fields !== null) { + Object.entries(object.fields).forEach(([key, value]) => { + message.fields[key] = value as any | undefined; + }); + } + return message; + }, + + toJSON(message: Struct): unknown { + const obj: any = {}; + obj.fields = {}; + if (message.fields) { + Object.entries(message.fields).forEach(([k, v]) => { + obj.fields[k] = v; + }); + } + return obj; + }, + + fromPartial(object: DeepPartial): Struct { + const message = { ...baseStruct } as Struct; + message.fields = {}; + if (object.fields !== undefined && object.fields !== null) { + Object.entries(object.fields).forEach(([key, value]) => { + if (value !== undefined) { + message.fields[key] = value; + } + }); + } + return message; + }, +}; + +const baseStruct_FieldsEntry: object = { key: '' }; + +export const Struct_FieldsEntry = { + encode(message: Struct_FieldsEntry, writer: Writer = Writer.create()): Writer { + if (message.key !== '') { + writer.uint32(10).string(message.key); + } + if (message.value !== undefined) { + Value.encode(wrapAnyValue(message.value), writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Struct_FieldsEntry { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseStruct_FieldsEntry } as Struct_FieldsEntry; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.key = reader.string(); + break; + case 2: + message.value = unwrapAnyValue(Value.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Struct_FieldsEntry { + const message = { ...baseStruct_FieldsEntry } as Struct_FieldsEntry; + message.key = object.key !== undefined && object.key !== null ? String(object.key) : ''; + message.value = object.value; + return message; + }, + + toJSON(message: Struct_FieldsEntry): unknown { + const obj: any = {}; + message.key !== undefined && (obj.key = message.key); + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): Struct_FieldsEntry { + const message = { ...baseStruct_FieldsEntry } as Struct_FieldsEntry; + message.key = object.key ?? ''; + message.value = object.value ?? undefined; + return message; + }, +}; + +const baseValue: object = {}; + +export const Value = { + encode(message: Value, writer: Writer = Writer.create()): Writer { + if (message.nullValue !== undefined) { + writer.uint32(8).int32(message.nullValue); + } + if (message.numberValue !== undefined) { + writer.uint32(17).double(message.numberValue); + } + if (message.stringValue !== undefined) { + writer.uint32(26).string(message.stringValue); + } + if (message.boolValue !== undefined) { + writer.uint32(32).bool(message.boolValue); + } + if (message.structValue !== undefined) { + Struct.encode(wrapStruct(message.structValue), writer.uint32(42).fork()).ldelim(); + } + if (message.listValue !== undefined) { + ListValue.encode({ values: message.listValue }, writer.uint32(50).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Value { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseValue } as Value; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.nullValue = reader.int32() as any; + break; + case 2: + message.numberValue = reader.double(); + break; + case 3: + message.stringValue = reader.string(); + break; + case 4: + message.boolValue = reader.bool(); + break; + case 5: + message.structValue = unwrapStruct(Struct.decode(reader, reader.uint32())); + break; + case 6: + message.listValue = ListValue.decode(reader, reader.uint32()).values; + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Value { + const message = { ...baseValue } as Value; + message.nullValue = + object.nullValue !== undefined && object.nullValue !== null ? nullValueFromJSON(object.nullValue) : undefined; + message.numberValue = + object.numberValue !== undefined && object.numberValue !== null ? Number(object.numberValue) : undefined; + message.stringValue = + object.stringValue !== undefined && object.stringValue !== null ? String(object.stringValue) : undefined; + message.boolValue = + object.boolValue !== undefined && object.boolValue !== null ? Boolean(object.boolValue) : undefined; + message.structValue = typeof object.structValue === 'object' ? object.structValue : undefined; + message.listValue = Array.isArray(object?.listValue) ? [...object.listValue] : undefined; + return message; + }, + + toJSON(message: Value): unknown { + const obj: any = {}; + message.nullValue !== undefined && + (obj.nullValue = message.nullValue !== undefined ? nullValueToJSON(message.nullValue) : undefined); + message.numberValue !== undefined && (obj.numberValue = message.numberValue); + message.stringValue !== undefined && (obj.stringValue = message.stringValue); + message.boolValue !== undefined && (obj.boolValue = message.boolValue); + message.structValue !== undefined && (obj.structValue = message.structValue); + message.listValue !== undefined && (obj.listValue = message.listValue); + return obj; + }, + + fromPartial(object: DeepPartial): Value { + const message = { ...baseValue } as Value; + message.nullValue = object.nullValue ?? undefined; + message.numberValue = object.numberValue ?? undefined; + message.stringValue = object.stringValue ?? undefined; + message.boolValue = object.boolValue ?? undefined; + message.structValue = object.structValue ?? undefined; + message.listValue = object.listValue ?? undefined; + return message; + }, +}; + +const baseListValue: object = {}; + +export const ListValue = { + encode(message: ListValue, writer: Writer = Writer.create()): Writer { + for (const v of message.values) { + Value.encode(wrapAnyValue(v!), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): ListValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseListValue } as ListValue; + message.values = []; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.values.push(unwrapAnyValue(Value.decode(reader, reader.uint32()))); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): ListValue { + const message = { ...baseListValue } as ListValue; + message.values = Array.isArray(object?.values) ? [...object.values] : []; + return message; + }, + + toJSON(message: ListValue): unknown { + const obj: any = {}; + if (message.values) { + obj.values = message.values.map((e) => e); + } else { + obj.values = []; + } + return obj; + }, + + fromPartial(object: DeepPartial): ListValue { + const message = { ...baseListValue } as ListValue; + message.values = (object.values ?? []).map((e) => e); + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} + +function wrapAnyValue(value: any): Value { + if (value === null) { + return { nullValue: 0 } as Value; + } else if (typeof value === 'boolean') { + return { boolValue: value } as Value; + } else if (typeof value === 'number') { + return { numberValue: value } as Value; + } else if (typeof value === 'string') { + return { stringValue: value } as Value; + } else if (Array.isArray(value)) { + return { listValue: value } as Value; + } else if (typeof value === 'object') { + return { structValue: value } as Value; + } else if (typeof value === 'undefined') { + return {} as Value; + } else { + throw new Error('Unsupported any value type: ' + typeof value); + } +} + +function unwrapAnyValue(value: Value): string | number | boolean | Object | null | Array | undefined { + if (value.stringValue !== undefined) { + return value.stringValue; + } else if (value.numberValue !== undefined) { + return value.numberValue; + } else if (value.boolValue !== undefined) { + return value.boolValue; + } else if (value.structValue !== undefined) { + return value.structValue; + } else if (value.listValue !== undefined) { + return value.listValue; + } else if (value.nullValue !== undefined) { + return null; + } +} + +function wrapStruct(object: { [key: string]: any }): Struct { + const struct = Struct.fromPartial({}); + Object.keys(object).forEach((key) => { + struct.fields[key] = object[key]; + }); + return struct; +} + +function unwrapStruct(struct: Struct): { [key: string]: any } { + const object: { [key: string]: any } = {}; + Object.keys(struct.fields).forEach((key) => { + object[key] = struct.fields[key]; + }); + return object; +} diff --git a/integration/struct/struct-test.ts b/integration/struct/struct-test.ts new file mode 100644 index 000000000..27b701754 --- /dev/null +++ b/integration/struct/struct-test.ts @@ -0,0 +1,63 @@ +import { Reader } from 'protobufjs'; +import { StructMessage } from './struct'; + +import { StructMessage as PbStructMessage } from './pbjs'; + +let data = { + value: { + name: 'john', + pet: null, + posts: [{ id: 1, title: 'hello world', public: true }], + }, +}; + +describe('struct', () => { + it('can encode objects', () => { + const s1 = StructMessage.fromJSON(data); + const s2 = PbStructMessage.decode(Reader.create(StructMessage.encode(s1).finish())); + + expect(s2).toMatchInlineSnapshot(` + Object { + "value": Object { + "fields": Object { + "name": Object { + "stringValue": "john", + }, + "pet": Object { + "nullValue": "NULL_VALUE", + }, + "posts": Object { + "listValue": Object { + "values": Array [ + Object { + "structValue": Object { + "fields": Object { + "id": Object { + "numberValue": 1, + }, + "public": Object { + "boolValue": true, + }, + "title": Object { + "stringValue": "hello world", + }, + }, + }, + }, + ], + }, + }, + }, + }, + } + `); + }); + + it('can decode objects', () => { + let message = StructMessage.fromJSON(data); + let encodedValue = StructMessage.encode(message).finish(); + const decodedValue = StructMessage.decode(Reader.create(encodedValue)); + + expect(StructMessage.toJSON(decodedValue)).toEqual(data); + }); +}); diff --git a/integration/struct/struct.bin b/integration/struct/struct.bin new file mode 100644 index 000000000..af5a5b0a3 Binary files /dev/null and b/integration/struct/struct.bin differ diff --git a/integration/struct/struct.proto b/integration/struct/struct.proto new file mode 100644 index 000000000..6aa650d40 --- /dev/null +++ b/integration/struct/struct.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +import "google/protobuf/struct.proto"; + +message StructMessage { + google.protobuf.Struct value = 1; +} diff --git a/integration/struct/struct.ts b/integration/struct/struct.ts new file mode 100644 index 000000000..5edbeb70a --- /dev/null +++ b/integration/struct/struct.ts @@ -0,0 +1,91 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; +import { Struct } from './google/protobuf/struct'; + +export const protobufPackage = ''; + +export interface StructMessage { + value: { [key: string]: any } | undefined; +} + +const baseStructMessage: object = {}; + +export const StructMessage = { + encode(message: StructMessage, writer: Writer = Writer.create()): Writer { + if (message.value !== undefined) { + Struct.encode(wrapStruct(message.value), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): StructMessage { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseStructMessage } as StructMessage; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = unwrapStruct(Struct.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): StructMessage { + const message = { ...baseStructMessage } as StructMessage; + message.value = typeof object.value === 'object' ? object.value : undefined; + return message; + }, + + toJSON(message: StructMessage): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): StructMessage { + const message = { ...baseStructMessage } as StructMessage; + message.value = object.value ?? undefined; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} + +function wrapStruct(object: { [key: string]: any }): Struct { + const struct = Struct.fromPartial({}); + Object.keys(object).forEach((key) => { + struct.fields[key] = object[key]; + }); + return struct; +} + +function unwrapStruct(struct: Struct): { [key: string]: any } { + const object: { [key: string]: any } = {}; + Object.keys(struct.fields).forEach((key) => { + object[key] = struct.fields[key]; + }); + return object; +} diff --git a/integration/value/google/protobuf/struct.ts b/integration/value/google/protobuf/struct.ts new file mode 100644 index 000000000..d54b6ca9b --- /dev/null +++ b/integration/value/google/protobuf/struct.ts @@ -0,0 +1,436 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; + +export const protobufPackage = 'google.protobuf'; + +/** + * `NullValue` is a singleton enumeration to represent the null value for the + * `Value` type union. + * + * The JSON representation for `NullValue` is JSON `null`. + */ +export enum NullValue { + /** NULL_VALUE - Null value. */ + NULL_VALUE = 0, + UNRECOGNIZED = -1, +} + +export function nullValueFromJSON(object: any): NullValue { + switch (object) { + case 0: + case 'NULL_VALUE': + return NullValue.NULL_VALUE; + case -1: + case 'UNRECOGNIZED': + default: + return NullValue.UNRECOGNIZED; + } +} + +export function nullValueToJSON(object: NullValue): string { + switch (object) { + case NullValue.NULL_VALUE: + return 'NULL_VALUE'; + default: + return 'UNKNOWN'; + } +} + +/** + * `Struct` represents a structured data value, consisting of fields + * which map to dynamically typed values. In some languages, `Struct` + * might be supported by a native representation. For example, in + * scripting languages like JS a struct is represented as an + * object. The details of that representation are described together + * with the proto support for the language. + * + * The JSON representation for `Struct` is JSON object. + */ +export interface Struct { + /** Unordered map of dynamically typed values. */ + fields: { [key: string]: any | undefined }; +} + +export interface Struct_FieldsEntry { + key: string; + value: any | undefined; +} + +/** + * `Value` represents a dynamically typed value which can be either + * null, a number, a string, a boolean, a recursive struct value, or a + * list of values. A producer of value is expected to set one of that + * variants, absence of any variant indicates an error. + * + * The JSON representation for `Value` is JSON value. + */ +export interface Value { + /** Represents a null value. */ + nullValue: NullValue | undefined; + /** Represents a double value. */ + numberValue: number | undefined; + /** Represents a string value. */ + stringValue: string | undefined; + /** Represents a boolean value. */ + boolValue: boolean | undefined; + /** Represents a structured value. */ + structValue: { [key: string]: any } | undefined; + /** Represents a repeated `Value`. */ + listValue: Array | undefined; +} + +/** + * `ListValue` is a wrapper around a repeated field of values. + * + * The JSON representation for `ListValue` is JSON array. + */ +export interface ListValue { + /** Repeated field of dynamically typed values. */ + values: any[]; +} + +const baseStruct: object = {}; + +export const Struct = { + encode(message: Struct, writer: Writer = Writer.create()): Writer { + Object.entries(message.fields).forEach(([key, value]) => { + if (value !== undefined) { + Struct_FieldsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).ldelim(); + } + }); + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Struct { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseStruct } as Struct; + message.fields = {}; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + const entry1 = Struct_FieldsEntry.decode(reader, reader.uint32()); + if (entry1.value !== undefined) { + message.fields[entry1.key] = entry1.value; + } + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Struct { + const message = { ...baseStruct } as Struct; + message.fields = {}; + if (object.fields !== undefined && object.fields !== null) { + Object.entries(object.fields).forEach(([key, value]) => { + message.fields[key] = value as any | undefined; + }); + } + return message; + }, + + toJSON(message: Struct): unknown { + const obj: any = {}; + obj.fields = {}; + if (message.fields) { + Object.entries(message.fields).forEach(([k, v]) => { + obj.fields[k] = v; + }); + } + return obj; + }, + + fromPartial(object: DeepPartial): Struct { + const message = { ...baseStruct } as Struct; + message.fields = {}; + if (object.fields !== undefined && object.fields !== null) { + Object.entries(object.fields).forEach(([key, value]) => { + if (value !== undefined) { + message.fields[key] = value; + } + }); + } + return message; + }, +}; + +const baseStruct_FieldsEntry: object = { key: '' }; + +export const Struct_FieldsEntry = { + encode(message: Struct_FieldsEntry, writer: Writer = Writer.create()): Writer { + if (message.key !== '') { + writer.uint32(10).string(message.key); + } + if (message.value !== undefined) { + Value.encode(wrapAnyValue(message.value), writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Struct_FieldsEntry { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseStruct_FieldsEntry } as Struct_FieldsEntry; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.key = reader.string(); + break; + case 2: + message.value = unwrapAnyValue(Value.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Struct_FieldsEntry { + const message = { ...baseStruct_FieldsEntry } as Struct_FieldsEntry; + message.key = object.key !== undefined && object.key !== null ? String(object.key) : ''; + message.value = object.value; + return message; + }, + + toJSON(message: Struct_FieldsEntry): unknown { + const obj: any = {}; + message.key !== undefined && (obj.key = message.key); + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): Struct_FieldsEntry { + const message = { ...baseStruct_FieldsEntry } as Struct_FieldsEntry; + message.key = object.key ?? ''; + message.value = object.value ?? undefined; + return message; + }, +}; + +const baseValue: object = {}; + +export const Value = { + encode(message: Value, writer: Writer = Writer.create()): Writer { + if (message.nullValue !== undefined) { + writer.uint32(8).int32(message.nullValue); + } + if (message.numberValue !== undefined) { + writer.uint32(17).double(message.numberValue); + } + if (message.stringValue !== undefined) { + writer.uint32(26).string(message.stringValue); + } + if (message.boolValue !== undefined) { + writer.uint32(32).bool(message.boolValue); + } + if (message.structValue !== undefined) { + Struct.encode(wrapStruct(message.structValue), writer.uint32(42).fork()).ldelim(); + } + if (message.listValue !== undefined) { + ListValue.encode({ values: message.listValue }, writer.uint32(50).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Value { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseValue } as Value; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.nullValue = reader.int32() as any; + break; + case 2: + message.numberValue = reader.double(); + break; + case 3: + message.stringValue = reader.string(); + break; + case 4: + message.boolValue = reader.bool(); + break; + case 5: + message.structValue = unwrapStruct(Struct.decode(reader, reader.uint32())); + break; + case 6: + message.listValue = ListValue.decode(reader, reader.uint32()).values; + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Value { + const message = { ...baseValue } as Value; + message.nullValue = + object.nullValue !== undefined && object.nullValue !== null ? nullValueFromJSON(object.nullValue) : undefined; + message.numberValue = + object.numberValue !== undefined && object.numberValue !== null ? Number(object.numberValue) : undefined; + message.stringValue = + object.stringValue !== undefined && object.stringValue !== null ? String(object.stringValue) : undefined; + message.boolValue = + object.boolValue !== undefined && object.boolValue !== null ? Boolean(object.boolValue) : undefined; + message.structValue = typeof object.structValue === 'object' ? object.structValue : undefined; + message.listValue = Array.isArray(object?.listValue) ? [...object.listValue] : undefined; + return message; + }, + + toJSON(message: Value): unknown { + const obj: any = {}; + message.nullValue !== undefined && + (obj.nullValue = message.nullValue !== undefined ? nullValueToJSON(message.nullValue) : undefined); + message.numberValue !== undefined && (obj.numberValue = message.numberValue); + message.stringValue !== undefined && (obj.stringValue = message.stringValue); + message.boolValue !== undefined && (obj.boolValue = message.boolValue); + message.structValue !== undefined && (obj.structValue = message.structValue); + message.listValue !== undefined && (obj.listValue = message.listValue); + return obj; + }, + + fromPartial(object: DeepPartial): Value { + const message = { ...baseValue } as Value; + message.nullValue = object.nullValue ?? undefined; + message.numberValue = object.numberValue ?? undefined; + message.stringValue = object.stringValue ?? undefined; + message.boolValue = object.boolValue ?? undefined; + message.structValue = object.structValue ?? undefined; + message.listValue = object.listValue ?? undefined; + return message; + }, +}; + +const baseListValue: object = {}; + +export const ListValue = { + encode(message: ListValue, writer: Writer = Writer.create()): Writer { + for (const v of message.values) { + Value.encode(wrapAnyValue(v!), writer.uint32(10).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): ListValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseListValue } as ListValue; + message.values = []; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.values.push(unwrapAnyValue(Value.decode(reader, reader.uint32()))); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): ListValue { + const message = { ...baseListValue } as ListValue; + message.values = Array.isArray(object?.values) ? [...object.values] : []; + return message; + }, + + toJSON(message: ListValue): unknown { + const obj: any = {}; + if (message.values) { + obj.values = message.values.map((e) => e); + } else { + obj.values = []; + } + return obj; + }, + + fromPartial(object: DeepPartial): ListValue { + const message = { ...baseListValue } as ListValue; + message.values = (object.values ?? []).map((e) => e); + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} + +function wrapAnyValue(value: any): Value { + if (value === null) { + return { nullValue: 0 } as Value; + } else if (typeof value === 'boolean') { + return { boolValue: value } as Value; + } else if (typeof value === 'number') { + return { numberValue: value } as Value; + } else if (typeof value === 'string') { + return { stringValue: value } as Value; + } else if (Array.isArray(value)) { + return { listValue: value } as Value; + } else if (typeof value === 'object') { + return { structValue: value } as Value; + } else if (typeof value === 'undefined') { + return {} as Value; + } else { + throw new Error('Unsupported any value type: ' + typeof value); + } +} + +function unwrapAnyValue(value: Value): string | number | boolean | Object | null | Array | undefined { + if (value.stringValue !== undefined) { + return value.stringValue; + } else if (value.numberValue !== undefined) { + return value.numberValue; + } else if (value.boolValue !== undefined) { + return value.boolValue; + } else if (value.structValue !== undefined) { + return value.structValue; + } else if (value.listValue !== undefined) { + return value.listValue; + } else if (value.nullValue !== undefined) { + return null; + } +} + +function wrapStruct(object: { [key: string]: any }): Struct { + const struct = Struct.fromPartial({}); + Object.keys(object).forEach((key) => { + struct.fields[key] = object[key]; + }); + return struct; +} + +function unwrapStruct(struct: Struct): { [key: string]: any } { + const object: { [key: string]: any } = {}; + Object.keys(struct.fields).forEach((key) => { + object[key] = struct.fields[key]; + }); + return object; +} diff --git a/integration/value/google/protobuf/wrappers.ts b/integration/value/google/protobuf/wrappers.ts new file mode 100644 index 000000000..216285744 --- /dev/null +++ b/integration/value/google/protobuf/wrappers.ts @@ -0,0 +1,578 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; + +export const protobufPackage = 'google.protobuf'; + +/** + * Wrapper message for `double`. + * + * The JSON representation for `DoubleValue` is JSON number. + */ +export interface DoubleValue { + /** The double value. */ + value: number; +} + +/** + * Wrapper message for `float`. + * + * The JSON representation for `FloatValue` is JSON number. + */ +export interface FloatValue { + /** The float value. */ + value: number; +} + +/** + * Wrapper message for `int64`. + * + * The JSON representation for `Int64Value` is JSON string. + */ +export interface Int64Value { + /** The int64 value. */ + value: number; +} + +/** + * Wrapper message for `uint64`. + * + * The JSON representation for `UInt64Value` is JSON string. + */ +export interface UInt64Value { + /** The uint64 value. */ + value: number; +} + +/** + * Wrapper message for `int32`. + * + * The JSON representation for `Int32Value` is JSON number. + */ +export interface Int32Value { + /** The int32 value. */ + value: number; +} + +/** + * Wrapper message for `uint32`. + * + * The JSON representation for `UInt32Value` is JSON number. + */ +export interface UInt32Value { + /** The uint32 value. */ + value: number; +} + +/** + * Wrapper message for `bool`. + * + * The JSON representation for `BoolValue` is JSON `true` and `false`. + */ +export interface BoolValue { + /** The bool value. */ + value: boolean; +} + +/** + * Wrapper message for `string`. + * + * The JSON representation for `StringValue` is JSON string. + */ +export interface StringValue { + /** The string value. */ + value: string; +} + +/** + * Wrapper message for `bytes`. + * + * The JSON representation for `BytesValue` is JSON string. + */ +export interface BytesValue { + /** The bytes value. */ + value: Uint8Array; +} + +const baseDoubleValue: object = { value: 0 }; + +export const DoubleValue = { + encode(message: DoubleValue, writer: Writer = Writer.create()): Writer { + if (message.value !== 0) { + writer.uint32(9).double(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): DoubleValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseDoubleValue } as DoubleValue; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.double(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): DoubleValue { + const message = { ...baseDoubleValue } as DoubleValue; + message.value = object.value !== undefined && object.value !== null ? Number(object.value) : 0; + return message; + }, + + toJSON(message: DoubleValue): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): DoubleValue { + const message = { ...baseDoubleValue } as DoubleValue; + message.value = object.value ?? 0; + return message; + }, +}; + +const baseFloatValue: object = { value: 0 }; + +export const FloatValue = { + encode(message: FloatValue, writer: Writer = Writer.create()): Writer { + if (message.value !== 0) { + writer.uint32(13).float(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): FloatValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseFloatValue } as FloatValue; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.float(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): FloatValue { + const message = { ...baseFloatValue } as FloatValue; + message.value = object.value !== undefined && object.value !== null ? Number(object.value) : 0; + return message; + }, + + toJSON(message: FloatValue): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): FloatValue { + const message = { ...baseFloatValue } as FloatValue; + message.value = object.value ?? 0; + return message; + }, +}; + +const baseInt64Value: object = { value: 0 }; + +export const Int64Value = { + encode(message: Int64Value, writer: Writer = Writer.create()): Writer { + if (message.value !== 0) { + writer.uint32(8).int64(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Int64Value { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseInt64Value } as Int64Value; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = longToNumber(reader.int64() as Long); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Int64Value { + const message = { ...baseInt64Value } as Int64Value; + message.value = object.value !== undefined && object.value !== null ? Number(object.value) : 0; + return message; + }, + + toJSON(message: Int64Value): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): Int64Value { + const message = { ...baseInt64Value } as Int64Value; + message.value = object.value ?? 0; + return message; + }, +}; + +const baseUInt64Value: object = { value: 0 }; + +export const UInt64Value = { + encode(message: UInt64Value, writer: Writer = Writer.create()): Writer { + if (message.value !== 0) { + writer.uint32(8).uint64(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): UInt64Value { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseUInt64Value } as UInt64Value; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = longToNumber(reader.uint64() as Long); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): UInt64Value { + const message = { ...baseUInt64Value } as UInt64Value; + message.value = object.value !== undefined && object.value !== null ? Number(object.value) : 0; + return message; + }, + + toJSON(message: UInt64Value): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): UInt64Value { + const message = { ...baseUInt64Value } as UInt64Value; + message.value = object.value ?? 0; + return message; + }, +}; + +const baseInt32Value: object = { value: 0 }; + +export const Int32Value = { + encode(message: Int32Value, writer: Writer = Writer.create()): Writer { + if (message.value !== 0) { + writer.uint32(8).int32(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): Int32Value { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseInt32Value } as Int32Value; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.int32(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Int32Value { + const message = { ...baseInt32Value } as Int32Value; + message.value = object.value !== undefined && object.value !== null ? Number(object.value) : 0; + return message; + }, + + toJSON(message: Int32Value): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): Int32Value { + const message = { ...baseInt32Value } as Int32Value; + message.value = object.value ?? 0; + return message; + }, +}; + +const baseUInt32Value: object = { value: 0 }; + +export const UInt32Value = { + encode(message: UInt32Value, writer: Writer = Writer.create()): Writer { + if (message.value !== 0) { + writer.uint32(8).uint32(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): UInt32Value { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseUInt32Value } as UInt32Value; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.uint32(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): UInt32Value { + const message = { ...baseUInt32Value } as UInt32Value; + message.value = object.value !== undefined && object.value !== null ? Number(object.value) : 0; + return message; + }, + + toJSON(message: UInt32Value): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): UInt32Value { + const message = { ...baseUInt32Value } as UInt32Value; + message.value = object.value ?? 0; + return message; + }, +}; + +const baseBoolValue: object = { value: false }; + +export const BoolValue = { + encode(message: BoolValue, writer: Writer = Writer.create()): Writer { + if (message.value === true) { + writer.uint32(8).bool(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): BoolValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseBoolValue } as BoolValue; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.bool(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): BoolValue { + const message = { ...baseBoolValue } as BoolValue; + message.value = object.value !== undefined && object.value !== null ? Boolean(object.value) : false; + return message; + }, + + toJSON(message: BoolValue): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): BoolValue { + const message = { ...baseBoolValue } as BoolValue; + message.value = object.value ?? false; + return message; + }, +}; + +const baseStringValue: object = { value: '' }; + +export const StringValue = { + encode(message: StringValue, writer: Writer = Writer.create()): Writer { + if (message.value !== '') { + writer.uint32(10).string(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): StringValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseStringValue } as StringValue; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): StringValue { + const message = { ...baseStringValue } as StringValue; + message.value = object.value !== undefined && object.value !== null ? String(object.value) : ''; + return message; + }, + + toJSON(message: StringValue): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + return obj; + }, + + fromPartial(object: DeepPartial): StringValue { + const message = { ...baseStringValue } as StringValue; + message.value = object.value ?? ''; + return message; + }, +}; + +const baseBytesValue: object = {}; + +export const BytesValue = { + encode(message: BytesValue, writer: Writer = Writer.create()): Writer { + if (message.value.length !== 0) { + writer.uint32(10).bytes(message.value); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): BytesValue { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseBytesValue } as BytesValue; + message.value = new Uint8Array(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = reader.bytes(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): BytesValue { + const message = { ...baseBytesValue } as BytesValue; + message.value = + object.value !== undefined && object.value !== null ? bytesFromBase64(object.value) : new Uint8Array(); + return message; + }, + + toJSON(message: BytesValue): unknown { + const obj: any = {}; + message.value !== undefined && + (obj.value = base64FromBytes(message.value !== undefined ? message.value : new Uint8Array())); + return obj; + }, + + fromPartial(object: DeepPartial): BytesValue { + const message = { ...baseBytesValue } as BytesValue; + message.value = object.value ?? new Uint8Array(); + return message; + }, +}; + +declare var self: any | undefined; +declare var window: any | undefined; +declare var global: any | undefined; +var globalThis: any = (() => { + if (typeof globalThis !== 'undefined') return globalThis; + if (typeof self !== 'undefined') return self; + if (typeof window !== 'undefined') return window; + if (typeof global !== 'undefined') return global; + throw 'Unable to locate global object'; +})(); + +const atob: (b64: string) => string = + globalThis.atob || ((b64) => globalThis.Buffer.from(b64, 'base64').toString('binary')); +function bytesFromBase64(b64: string): Uint8Array { + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; +} + +const btoa: (bin: string) => string = + globalThis.btoa || ((bin) => globalThis.Buffer.from(bin, 'binary').toString('base64')); +function base64FromBytes(arr: Uint8Array): string { + const bin: string[] = []; + for (const byte of arr) { + bin.push(String.fromCharCode(byte)); + } + return btoa(bin.join('')); +} + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +function longToNumber(long: Long): number { + if (long.gt(Number.MAX_SAFE_INTEGER)) { + throw new globalThis.Error('Value is larger than Number.MAX_SAFE_INTEGER'); + } + return long.toNumber(); +} + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/integration/value/value-test.ts b/integration/value/value-test.ts new file mode 100644 index 000000000..deb4a73f3 --- /dev/null +++ b/integration/value/value-test.ts @@ -0,0 +1,64 @@ +import { Reader } from 'protobufjs'; +import { ValueMessage } from './value'; + +import { ValueMessage as PbValueMessage } from './pbjs'; + +describe('values', () => { + it('json value', () => { + const s1 = ValueMessage.fromJSON({ + value: 'Hello', + anyList: [1, 'foo', true], + repeatedAny: [2, 'bar', false], + }); + + const s2 = PbValueMessage.decode(Reader.create(ValueMessage.encode(s1).finish())); + + expect(s2).toMatchInlineSnapshot(` + Object { + "anyList": Object { + "values": Array [ + Object { + "numberValue": 1, + }, + Object { + "stringValue": "foo", + }, + Object { + "boolValue": true, + }, + ], + }, + "repeatedAny": Array [ + Object { + "numberValue": 2, + }, + Object { + "stringValue": "bar", + }, + Object { + "boolValue": false, + }, + ], + "value": Object { + "stringValue": "Hello", + }, + } + `); + }); + + it('decodes lists of any types correctly', () => { + const s1 = ValueMessage.fromJSON({ + anyList: [1, 'foo', true], + repeatedAny: [2, 'bar', false], + }); + expect(s1).toEqual({ anyList: [1, 'foo', true], repeatedAny: [2, 'bar', false] }); + }); + + it('toJson', () => { + const s1 = ValueMessage.fromPartial({ + anyList: [1], + repeatedAny: [2], + }); + expect(s1).toEqual({ anyList: [1], repeatedAny: [2] }); + }); +}); diff --git a/integration/value/value.bin b/integration/value/value.bin new file mode 100644 index 000000000..4f4868c81 Binary files /dev/null and b/integration/value/value.bin differ diff --git a/integration/value/value.proto b/integration/value/value.proto new file mode 100644 index 000000000..0b4710624 --- /dev/null +++ b/integration/value/value.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +message ValueMessage { + google.protobuf.Value value = 1; + google.protobuf.ListValue anyList = 2; + repeated google.protobuf.Value repeatedAny = 3; +} diff --git a/integration/value/value.ts b/integration/value/value.ts new file mode 100644 index 000000000..7e15efe5b --- /dev/null +++ b/integration/value/value.ts @@ -0,0 +1,136 @@ +/* eslint-disable */ +import { util, configure, Writer, Reader } from 'protobufjs/minimal'; +import * as Long from 'long'; +import { Value, ListValue } from './google/protobuf/struct'; + +export const protobufPackage = ''; + +export interface ValueMessage { + value: any | undefined; + anyList: Array | undefined; + repeatedAny: any[]; +} + +const baseValueMessage: object = {}; + +export const ValueMessage = { + encode(message: ValueMessage, writer: Writer = Writer.create()): Writer { + if (message.value !== undefined) { + Value.encode(wrapAnyValue(message.value), writer.uint32(10).fork()).ldelim(); + } + if (message.anyList !== undefined) { + ListValue.encode({ values: message.anyList }, writer.uint32(18).fork()).ldelim(); + } + for (const v of message.repeatedAny) { + Value.encode(wrapAnyValue(v!), writer.uint32(26).fork()).ldelim(); + } + return writer; + }, + + decode(input: Reader | Uint8Array, length?: number): ValueMessage { + const reader = input instanceof Reader ? input : new Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseValueMessage } as ValueMessage; + message.repeatedAny = []; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.value = unwrapAnyValue(Value.decode(reader, reader.uint32())); + break; + case 2: + message.anyList = ListValue.decode(reader, reader.uint32()).values; + break; + case 3: + message.repeatedAny.push(unwrapAnyValue(Value.decode(reader, reader.uint32()))); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): ValueMessage { + const message = { ...baseValueMessage } as ValueMessage; + message.value = object.value; + message.anyList = Array.isArray(object?.anyList) ? [...object.anyList] : undefined; + message.repeatedAny = Array.isArray(object?.repeatedAny) ? [...object.repeatedAny] : []; + return message; + }, + + toJSON(message: ValueMessage): unknown { + const obj: any = {}; + message.value !== undefined && (obj.value = message.value); + message.anyList !== undefined && (obj.anyList = message.anyList); + if (message.repeatedAny) { + obj.repeatedAny = message.repeatedAny.map((e) => e); + } else { + obj.repeatedAny = []; + } + return obj; + }, + + fromPartial(object: DeepPartial): ValueMessage { + const message = { ...baseValueMessage } as ValueMessage; + message.value = object.value ?? undefined; + message.anyList = object.anyList ?? undefined; + message.repeatedAny = (object.repeatedAny ?? []).map((e) => e); + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} + +function wrapAnyValue(value: any): Value { + if (value === null) { + return { nullValue: 0 } as Value; + } else if (typeof value === 'boolean') { + return { boolValue: value } as Value; + } else if (typeof value === 'number') { + return { numberValue: value } as Value; + } else if (typeof value === 'string') { + return { stringValue: value } as Value; + } else if (Array.isArray(value)) { + return { listValue: value } as Value; + } else if (typeof value === 'object') { + return { structValue: value } as Value; + } else if (typeof value === 'undefined') { + return {} as Value; + } else { + throw new Error('Unsupported any value type: ' + typeof value); + } +} + +function unwrapAnyValue(value: Value): string | number | boolean | Object | null | Array | undefined { + if (value.stringValue !== undefined) { + return value.stringValue; + } else if (value.numberValue !== undefined) { + return value.numberValue; + } else if (value.boolValue !== undefined) { + return value.boolValue; + } else if (value.structValue !== undefined) { + return value.structValue; + } else if (value.listValue !== undefined) { + return value.listValue; + } else if (value.nullValue !== undefined) { + return null; + } +} diff --git a/src/encode.ts b/src/encode.ts index d2c412058..022033d01 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -36,6 +36,8 @@ export function generateEncoder(ctx: Context, typeName: string): Code { return code`${TypeValue}.encode({value: value ?? false}).finish()`; case 'BytesValue': return code`${TypeValue}.encode({value: value ?? new Uint8Array()}).finish()`; + case 'ListValue': + return code`${TypeValue}.encode({value: value ?? []]}).finish()`; } throw new Error(`unknown wrapper type: ${name}`); diff --git a/src/main.ts b/src/main.ts index ea3744a18..bc8251458 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,9 +14,11 @@ import { defaultValue, detectMapType, getEnumMethod, + isAnyValueType, isBytes, isBytesValueType, isEnum, + isListValueType, isLong, isLongValueType, isMapType, @@ -32,6 +34,8 @@ import { toReaderCall, toTypeName, valueTypeName, + messageToTypeName, + isStructType, } from './types'; import SourceInfo, { Fields } from './sourceInfo'; import { assertInstanceOf, FormattedMethodDescriptor, maybeAddComment, maybePrefixPackage } from './utils'; @@ -258,7 +262,8 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri export type Utils = ReturnType & ReturnType & ReturnType & - ReturnType; + ReturnType & + ReturnType; /** These are runtime utility methods used by the generated code. */ export function makeUtils(options: Options): Utils { @@ -269,6 +274,7 @@ export function makeUtils(options: Options): Utils { ...makeDeepPartial(options, longs), ...makeTimestampMethods(options, longs), ...longs, + ...makeWrappingUtils(), }; } @@ -342,6 +348,73 @@ function makeLongUtils(options: Options, bytes: ReturnType return { numberToLong, longToNumber, longToString, longInit, Long }; } +function makeWrappingUtils() { + const wrapAnyValue = conditionalOutput( + 'wrapAnyValue', + code`function wrapAnyValue(value: any): Value { + if (value === null) { + return {nullValue: 0} as Value; + } else if (typeof value === 'boolean') { + return {boolValue: value} as Value; + } else if (typeof value === 'number') { + return {numberValue: value} as Value; + } else if (typeof value === 'string') { + return {stringValue: value} as Value; + } else if (Array.isArray(value)) { + return {listValue: value} as Value; + } else if (typeof value === 'object') { + return {structValue: value} as Value; + } else if (typeof value === 'undefined') { + return {} as Value; + } else { + throw new Error('Unsupported any value type: ' + typeof value); + } + }` + ); + + const unwrapAnyValue = conditionalOutput( + 'unwrapAnyValue', + code`function unwrapAnyValue(value: Value): string | number | boolean | Object | null | Array | undefined { + if (value.stringValue !== undefined) { + return value.stringValue; + } else if (value.numberValue !== undefined) { + return value.numberValue; + } else if (value.boolValue !== undefined) { + return value.boolValue; + } else if (value.structValue !== undefined) { + return value.structValue; + } else if (value.listValue !== undefined) { + return value.listValue; + } else if (value.nullValue !== undefined) { + return null; + } + }` + ); + + const wrapStruct = conditionalOutput( + 'wrapStruct', + code`function wrapStruct(object: {[key: string]: any}): Struct { + const struct = Struct.fromPartial({}); + Object.keys(object).forEach(key => { + struct.fields[key] = object[key]; + }); + return struct; + }` + ); + + const unwrapStruct = conditionalOutput( + 'unwrapStruct', + code`function unwrapStruct(struct: Struct): {[key: string]: any} { + const object: { [key: string]: any } = {}; + Object.keys(struct.fields).forEach(key => { + object[key] = struct.fields[key]; + }); + return object; + }` + ); + return { wrapAnyValue, unwrapAnyValue, wrapStruct, unwrapStruct }; +} + function makeByteUtils() { const globalThis = conditionalOutput( 'globalThis', @@ -693,8 +766,15 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP } } } else if (isValueType(ctx, field)) { + const unwrap = (decodedValue: any): Code => { + if (isAnyValueType(field)) return code`${ctx.utils.unwrapAnyValue}(${decodedValue})`; + if (isStructType(field)) return code`${ctx.utils.unwrapStruct}(${decodedValue})`; + if (isListValueType(field)) return code`${decodedValue}.values`; + return code`${decodedValue}.value`; + }; const type = basicTypeName(ctx, field, { keepValueType: true }); - readSnippet = code`${type}.decode(reader, reader.uint32()).value`; + const decoder = code`${type}.decode(reader, reader.uint32())`; + readSnippet = code`${unwrap(decoder)}`; } else if (isTimestamp(field) && (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING)) { const type = basicTypeName(ctx, field, { keepValueType: true }); readSnippet = code`${utils.fromTimestamp}(${type}.decode(reader, reader.uint32()))`; @@ -789,11 +869,18 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP writeSnippet = (place) => code`${type}.encode(${utils.toTimestamp}(${place}), writer.uint32(${tag}).fork()).ldelim()`; } else if (isValueType(ctx, field)) { + const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : ''; + + const wrappedValue = (place: string): Code => { + if (isAnyValueType(field)) return code`${ctx.utils.wrapAnyValue}(${place})`; + if (isListValueType(field)) return code`{values: ${place}}`; + if (isStructType(field)) return code`${ctx.utils.wrapStruct}(${place})`; + return code`{${maybeTypeField} value: ${place}!}`; + }; + const tag = ((field.number << 3) | 2) >>> 0; const type = basicTypeName(ctx, field, { keepValueType: true }); - const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : ''; - writeSnippet = (place) => - code`${type}.encode({ ${maybeTypeField} value: ${place}! }, writer.uint32(${tag}).fork()).ldelim()`; + writeSnippet = (place) => code`${type}.encode(${wrappedValue(place)}, writer.uint32(${tag}).fork()).ldelim()`; } else if (isMessage(field)) { const tag = ((field.number << 3) | 2) >>> 0; const type = basicTypeName(ctx, field); @@ -932,12 +1019,16 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto (options.useDate === DateOption.DATE || options.useDate === DateOption.TIMESTAMP) ) { return code`${utils.fromJsonTimestamp}(${from})`; + } else if (isAnyValueType(field) || isStructType(field)) { + return code`${from}`; } else if (isValueType(ctx, field)) { const valueType = valueTypeName(ctx, field.typeName)!; if (isLongValueType(field) && options.forceLong === LongOption.LONG) { return code`${capitalize(valueType.toCodeString())}.fromValue(${from})`; } else if (isBytesValueType(field)) { return code`new ${capitalize(valueType.toCodeString())}(${from})`; + } else if (isListValueType(field)) { + return code`[...${from}]`; } else { return code`${capitalize(valueType.toCodeString())}(${from})`; } @@ -968,6 +1059,8 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto } else if (isValueType(ctx, valueType)) { const type = basicTypeName(ctx, valueType); return code`${from} as ${type}`; + } else if (isAnyValueType(valueType)) { + return code`${from}`; } else { const type = basicTypeName(ctx, valueType); return code`${type}.fromJSON(${from})`; @@ -993,6 +1086,10 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto }); `); chunks.push(code`}`); + } else if (isAnyValueType(field)) { + chunks.push(code` + message.${fieldName} = Array.isArray(object?.${fieldName}) ? [...object.${fieldName}] : []; + `); } else { // Explicit `any` type required to make TS with noImplicitAny happy. `object` is also `any` here. chunks.push(code` @@ -1006,6 +1103,18 @@ function generateFromJson(ctx: Context, fullName: string, messageDesc: Descripto message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${fieldName}`)} } `); chunks.push(code`}`); + } else if (isAnyValueType(field)) { + chunks.push(code`message.${fieldName} = object.${fieldName};`); + } else if (isStructType(field)) { + chunks.push( + code`message.${fieldName} = typeof(object.${fieldName}) === 'object' ? object.${fieldName} : undefined;` + ); + } else if (isListValueType(field)) { + chunks.push(code` + message.${fieldName} = Array.isArray(object?.${fieldName}) + ? ${readSnippet(`object.${fieldName}`)} + : ${'undefined'}; + `); } else { const fallback = isWithinOneOf(field) ? 'undefined' : defaultValue(ctx, field); chunks.push(code` @@ -1063,10 +1172,14 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP return code`${utils.fromTimestamp}(${from}).toISOString()`; } else if (isScalar(valueType) || isValueType(ctx, valueType)) { return code`${from}`; + } else if (isAnyValueType(valueType)) { + return code`${from}`; } else { const type = basicTypeName(ctx, valueType); return code`${type}.toJSON(${from})`; } + } else if (isAnyValueType(field)) { + return code`${from}`; } else if (isMessage(field) && !isValueType(ctx, field) && !isMapType(ctx, messageDesc, field)) { const type = basicTypeName(ctx, field, { keepValueType: true }); return code`${from} ? ${type}.toJSON(${from}) : ${defaultValue(ctx, field)}`; @@ -1152,6 +1265,8 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const cstr = capitalize(basicTypeName(ctx, valueType).toCodeString()); return code`${cstr}(${from})`; } + } else if (isAnyValueType(valueType)) { + return code`${from}`; } else if ( isTimestamp(valueType) && (options.useDate === DateOption.DATE || options.useDate === DateOption.STRING) @@ -1163,6 +1278,8 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri const type = basicTypeName(ctx, valueType); return code`${type}.fromPartial(${from})`; } + } else if (isAnyValueType(field)) { + return code`${from}`; } else { const type = basicTypeName(ctx, field); return code`${type}.fromPartial(${from})`; diff --git a/src/types.ts b/src/types.ts index 6e162b074..450c0f804 100644 --- a/src/types.ts +++ b/src/types.ts @@ -370,10 +370,26 @@ export function isValueType(ctx: Context, field: FieldDescriptorProto): boolean return valueTypeName(ctx, field.typeName) !== undefined; } +export function isAnyValueType(field: FieldDescriptorProto): boolean { + return field.typeName === '.google.protobuf.Value'; +} + export function isBytesValueType(field: FieldDescriptorProto): boolean { return field.typeName === '.google.protobuf.BytesValue'; } +export function isListValueType(field: FieldDescriptorProto): boolean { + return isListValueTypeName(field.typeName); +} + +export function isListValueTypeName(typeName: string): boolean { + return typeName === 'google.protobuf.ListValue' || typeName === '.google.protobuf.ListValue'; +} + +export function isStructType(field: FieldDescriptorProto): boolean { + return field.typeName === '.google.protobuf.Struct'; +} + export function isLongValueType(field: FieldDescriptorProto): boolean { return field.typeName === '.google.protobuf.Int64Value' || field.typeName === '.google.protobuf.UInt64Value'; } @@ -399,6 +415,12 @@ export function valueTypeName(ctx: Context, typeName: string): Code | undefined return code`boolean`; case '.google.protobuf.BytesValue': return code`Uint8Array`; + case '.google.protobuf.ListValue': + return code`Array`; + case '.google.protobuf.Value': + return code`any`; + case '.google.protobuf.Struct': + return code`{[key: string]: any}`; default: return undefined; } @@ -415,6 +437,7 @@ export function wrapperTypeName(typeName: string): string | undefined { case '.google.protobuf.UInt64Value': case '.google.protobuf.BoolValue': case '.google.protobuf.BytesValue': + case '.google.protobuf.ListValue': case '.google.protobuf.Timestamp': return typeName.split('.')[3]; default: