ts-proto 1.87.1 → 1.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [1.90.1](https://github.com/stephenh/ts-proto/compare/v1.90.0...v1.90.1) (2021-11-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * code-generation for Services with Struct response types ([#407](https://github.com/stephenh/ts-proto/issues/407)) ([f041fa1](https://github.com/stephenh/ts-proto/commit/f041fa1047816748c366bcb81895b6b917eb328d))
7
+
8
+ # [1.90.0](https://github.com/stephenh/ts-proto/compare/v1.89.0...v1.90.0) (2021-11-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add support for 'json_name' annotation ([#408](https://github.com/stephenh/ts-proto/issues/408)) ([b519717](https://github.com/stephenh/ts-proto/commit/b5197174bcaacb8f163cd197d52ab9c645d21d4c))
14
+
15
+ # [1.89.0](https://github.com/stephenh/ts-proto/compare/v1.88.0...v1.89.0) (2021-11-24)
16
+
17
+
18
+ ### Features
19
+
20
+ * Improve map reading (fromJSON/fromPartial) ([#410](https://github.com/stephenh/ts-proto/issues/410)) ([057d438](https://github.com/stephenh/ts-proto/commit/057d438548d95c354331f7d2d767ccff952ad5c6))
21
+
22
+ # [1.88.0](https://github.com/stephenh/ts-proto/compare/v1.87.1...v1.88.0) (2021-11-22)
23
+
24
+
25
+ ### Features
26
+
27
+ * Support for Google.Protobuf.Value, ListValue and Struct ([#396](https://github.com/stephenh/ts-proto/issues/396)) ([7dd9c16](https://github.com/stephenh/ts-proto/commit/7dd9c16ffdec4d9ea296fbdc30d390fe44192c42))
28
+
1
29
  ## [1.87.1](https://github.com/stephenh/ts-proto/compare/v1.87.0...v1.87.1) (2021-11-21)
2
30
 
3
31
 
package/README.markdown CHANGED
@@ -22,10 +22,13 @@
22
22
  - [Assumptions](#assumptions)
23
23
  - [Todo](#todo)
24
24
  - [OneOf Handling](#oneof-handling)
25
- - [Primitive Types](#primitive-types)
25
+ - [Default Values and Unset Fields](#default-values-and-unset-fields)
26
+ - [Well-Known Types](#well-known-types)
27
+ - [Wrapper Types](#wrapper-types)
28
+ - [JSON Types (Struct Types)](#json-types-struct-types)
29
+ - [Timestamps](#timestamp)
26
30
  - [Wrapper Types](#wrapper-types)
27
31
  - [Number Types](#number-types)
28
- - [Timestamps](#timestamps)
29
32
  - [Current Status of Optional Values](#current-status-of-optional-values)
30
33
 
31
34
  # Overview
@@ -376,7 +379,6 @@ The test suite's proto files (i.e. `simple.proto`, `batching.proto`, etc.) curre
376
379
  # Todo
377
380
 
378
381
  - Support the string-based encoding of duration in `fromJSON`/`toJSON`
379
- - Support the `json_name` annotation
380
382
  - Make `oneof=unions` the default behavior in 2.0
381
383
  - Probably change `forceLong` default in 2.0, should default to `forceLong=long`
382
384
  - Make `esModuleInterop=true` the default in 2.0
@@ -399,29 +401,201 @@ As this will automatically enforce only one of `field_a` or `field_b` "being set
399
401
 
400
402
  In ts-proto's currently-unscheduled 2.x release, `oneof=unions` will become the default behavior.
401
403
 
402
- # Primitive Types
404
+ # Default values and unset fields
403
405
 
404
- Protobuf has the somewhat annoying behavior that primitives types cannot differentiate between set-to-defalut-value and unset.
406
+ In core Protobuf, values that are _unset_ or equal to the default value are not sent over the wire.
407
+ The default value of a message is `undefined`. Primitive types take their natural default value, i.e. `string` is `''`, `number` is `0`, etc.
408
+ 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.
405
409
 
406
- 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.
410
+ If you need primitive fields where you can detect set/unset, see [Wrapper Types](#wrapper-types).
407
411
 
408
- `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.)
412
+ **Encode / Decode**
409
413
 
410
- If you want fields where you can model set/unset, see Wrapper Types.
414
+ `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.
411
415
 
412
- # Wrapper Types
416
+ ```protobuf
417
+ syntax = "proto3";
418
+ message Foo {
419
+ string bar = 1;
420
+ }
421
+ ```
422
+
423
+ ```typescript
424
+ protobufBytes; // assume this is an empty Foo object, in protobuf binary format
425
+ Foo.decode(protobufBytes); // => { bar: '' }
426
+ ```
427
+
428
+ ```typescript
429
+ Foo.encode({ bar: '' }); // => { }, writes an empty Foo object, in protobuf binary format
430
+ ```
431
+
432
+ **fromJSON / toJSON**
433
+
434
+ 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.
435
+
436
+ ```typescript
437
+ Foo.fromJSON({ }); // => { bar: '' }
438
+ Foo.fromJSON({ bar: '' }); // => { bar: '' }
439
+ Foo.fromJSON({ bar: 'baz' }); // => { bar: 'baz' }
440
+ ```
441
+
442
+ 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.
443
+
444
+ ```typescript
445
+ // Current ts-proto behavior
446
+ Foo.toJSON({ }); // => { }
447
+ Foo.toJSON({ bar: undefined }); // => { }
448
+ Foo.toJSON({ bar: '' }); // => { bar: '' } - note: this is the default value, but it's not omitted
449
+ Foo.toJSON({ bar: 'baz' }); // => { bar: 'baz' }
450
+ ```
451
+
452
+ ```typescript
453
+ // Possible future behavior, where ts-proto would normalize message
454
+ Foo.toJSON({ }); // => { }
455
+ Foo.toJSON({ bar: undefined }); // => { }
456
+ Foo.toJSON({ bar: '' }); // => { } - note: omitting the default value, as expected
457
+ Foo.toJSON({ bar: 'baz' }); // => { bar: 'baz' }
458
+ ```
413
459
 
414
- In core Protobuf, unset primitive fields become their respective default values (so you loose ability to distinguish "unset" from "default").
460
+ - Please open an issue if you need this behavior.
415
461
 
416
- However, unset message fields stay `null`.
462
+ # Well-Known Types
417
463
 
418
- 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).
464
+ Protobuf comes with several predefined message definitions, called "[Well-Known Types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf)".
465
+ Their interpretation is defined by the Protobuf specification, and libraries are expected to convert these messages to corresponding native types in the target language.
419
466
 
420
- Protobuf has already "blessed" this pattern with several built-in types, i.e. `google.protobuf.StringValue`, `google.protobuf.Int32Value`, etc.
467
+ `ts-proto` currently automatically converts these messages to their corresponding native types.
421
468
 
422
- `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).
469
+ - Wrapper Types:
470
+
471
+ * [google.protobuf.DoubleValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#DoubleValue) ⇆ `number | undefined`
472
+ * [google.protobuf.FloatValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#FloatValue) ⇆ `number | undefined`
473
+ * [google.protobuf.Int64Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Int64Value) ⇆ `number | undefined`
474
+ * [google.protobuf.UInt64Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#UInt64Value) ⇆ `number | undefined`
475
+ * [google.protobuf.Int32Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Int32Value) ⇆ `number | undefined`
476
+ * [google.protobuf.UInt32Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#UInt32Value) ⇆ `number | undefined`
477
+ * [google.protobuf.BoolValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#BoolValue) ⇆ `boolean | undefined`
478
+ * [google.protobuf.StringValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#StringValue) ⇆ `string | undefined`
479
+ * [google.protobuf.BytesValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.BytesValue) ⇆ `Uint8Array | undefined`
480
+
481
+ - JSON Types (Struct Types):
482
+
483
+ * [google.protobuf.Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Value) ⇆ `any | undefined` (i.e. `number | string | boolean | null | array | object`)
484
+ * [google.protobuf.ListValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#ListValue) ⇆ `any[]`
485
+ * [google.protobuf.Struct](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Struct) ⇆ `{ [key: string]: any } | undefined`
486
+
487
+ ## Wrapper Types
488
+
489
+ Wrapper Types are messages containing a single primitive field, and can be imported in `.proto` files with `import "google/protobuf/wrappers.proto"`.
490
+
491
+
492
+ Since these are _messages_, their default value is `undefined`, allowing you to distinguish unset primitives from their default values, when using Wrapper Types.
493
+ `ts-proto` generates these fields as `<primitive> | undefined`.
494
+
495
+ For example:
496
+
497
+ ```protobuf
498
+ // Protobuf
499
+ syntax = "proto3";
500
+
501
+ import "google/protobuf/wrappers.proto";
502
+
503
+ message ExampleMessage {
504
+ google.protobuf.StringValue name = 1;
505
+ }
506
+ ```
507
+
508
+ ```typescript
509
+ // TypeScript
510
+ interface ExampleMessage {
511
+ name: string | undefined;
512
+ }
513
+ ```
514
+
515
+ When encoding a message the primitive value is converted back to its corresponding wrapper type:
516
+
517
+ ```typescript
518
+ ExampleMessage.encode({ name: 'foo' }) // => { name: { value: 'foo' } }, in binary
519
+ ```
520
+
521
+ When calling toJSON, the value is not converted, because wrapper types are idiomatic in JSON.
522
+
523
+ ```typescript
524
+ ExampleMessage.toJSON({ name: 'foo' }) // => { name: 'foo' }
525
+ ```
526
+
527
+ ## JSON Types (Struct Types)
528
+
529
+ 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.
530
+ For this reason, Protobuf offers several additional types to represent arbitrary JSON values.
531
+
532
+ These are called Struct Types, and can be imported in `.proto` files with `import "google/protobuf/struct.proto"`.
533
+
534
+ - [google.protobuf.Value](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Value) &lrarr; `any`
535
+ - This is the most general type, and can represent any JSON value (i.e. `number | string | boolean | null | array | object`).
536
+ - [google.protobuf.ListValue](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#ListValue) &lrarr; `any[]`
537
+ - To represent a JSON array
538
+ - [google.protobuf.Struct](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#Struct) &lrarr; `{ [key: string]: any }`
539
+ - To represent a JSON object
540
+
541
+ `ts-proto` automatically converts back and forth between these Struct Types and their corresponding JSON types.
542
+
543
+ Example:
544
+
545
+ ```protobuf
546
+ // Protobuf
547
+ syntax = "proto3";
548
+
549
+ import "google/protobuf/struct.proto";
550
+
551
+ message ExampleMessage {
552
+ google.protobuf.Value anything = 1;
553
+ }
554
+ ```
555
+
556
+ ```typescript
557
+ // TypeScript
558
+ interface ExampleMessage {
559
+ anything: any | undefined;
560
+ }
561
+ ```
562
+
563
+ Encoding a JSON value embedded in a message, converts it to a Struct Type:
564
+ ```typescript
565
+ ExampleMessage.encode({ anything: { "name": "hello" } })
566
+ /* Outputs the following structure, encoded in protobuf binary format:
567
+ {
568
+ anything: Value {
569
+ structValue = Struct {
570
+ fields = [
571
+ MapEntry {
572
+ key = "name",
573
+ value = Value {
574
+ stringValue = "hello"
575
+ }
576
+ ]
577
+ }
578
+ }
579
+ }
580
+ }*/
581
+
582
+ ExampleMessage.encode({ anything: true })
583
+ /* Outputs the following structure encoded in protobuf binary format:
584
+ {
585
+ anything: Value {
586
+ boolValue = true
587
+ }
588
+ }*/
589
+ ```
590
+
591
+ ## Timestamp
592
+
593
+ The representation of `google.protobuf.Timestamp` is configurable by the `useDate` flag.
594
+
595
+ | Protobuf well-known type | Default/`useDate=true` | `useDate=false` | `useDate=string` |
596
+ | --------------------------- | ---------------------- | ------------------------------------ | ---------------- |
597
+ | `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` |
423
598
 
424
- 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.
425
599
 
426
600
  # Number Types
427
601
 
@@ -452,14 +626,6 @@ The protobuf number types map to JavaScript types based on the `forceLong` confi
452
626
 
453
627
  Where (\*) indicates they might throw an error at runtime.
454
628
 
455
- # Timestamps
456
-
457
- The representation of `google.protobuf.Timestamp` is configurable by the `useDate` flag.
458
-
459
- | Protobuf well-known type | Default/`useDate=true` | `useDate=false` | `useDate=string` |
460
- | --------------------------- | ---------------------- | ------------------------------------ | ---------------- |
461
- | `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` |
462
-
463
629
  # Current Status of Optional Values
464
630
 
465
631
  - Required primitives: use as-is, i.e. `string name = 1`.
package/build/encode.js CHANGED
@@ -7,12 +7,20 @@ const options_1 = require("./options");
7
7
  function generateEncoder(ctx, typeName) {
8
8
  const name = types_1.wrapperTypeName(typeName);
9
9
  if (!name) {
10
- return ts_poet_1.code `${types_1.messageToTypeName(ctx, typeName)}.encode(value).finish()`;
10
+ return ts_poet_1.code `${types_1.messageToTypeName(ctx, typeName, { keepValueType: true })}.encode(value).finish()`;
11
11
  }
12
12
  if (name == 'Timestamp') {
13
13
  const TimestampValue = ts_poet_1.imp(`${name}@./google/protobuf/timestamp`);
14
14
  return ts_poet_1.code `${TimestampValue}.encode(${ctx.utils.toTimestamp}(value)).finish()`;
15
15
  }
16
+ if (name == 'Struct') {
17
+ const StructType = ts_poet_1.imp(`${name}@./google/protobuf/struct`);
18
+ return ts_poet_1.code `${StructType}.encode(${StructType}.wrap(value)).finish()`;
19
+ }
20
+ if (name == 'ListValue') {
21
+ const ListValueType = ts_poet_1.imp(`${name}@./google/protobuf/struct`);
22
+ return ts_poet_1.code `${ListValueType}.encode({values: value ?? []}).finish()`;
23
+ }
16
24
  const TypeValue = ts_poet_1.imp(`${name}@./google/protobuf/wrappers`);
17
25
  switch (name) {
18
26
  case 'StringValue':
@@ -39,13 +47,17 @@ exports.generateEncoder = generateEncoder;
39
47
  function generateDecoder(ctx, typeName) {
40
48
  let name = types_1.wrapperTypeName(typeName);
41
49
  if (!name) {
42
- return ts_poet_1.code `${types_1.messageToTypeName(ctx, typeName)}.decode(value)`;
50
+ return ts_poet_1.code `${types_1.messageToTypeName(ctx, typeName, { keepValueType: true })}.decode(value)`;
43
51
  }
44
52
  let TypeValue;
45
53
  if (name == 'Timestamp') {
46
54
  TypeValue = ts_poet_1.imp(`${name}@./google/protobuf/timestamp`);
47
55
  return ts_poet_1.code `${TypeValue}.decode(value)`;
48
56
  }
57
+ if (name == 'Struct' || name == 'ListValue') {
58
+ TypeValue = ts_poet_1.imp(`${name}@./google/protobuf/struct`);
59
+ return ts_poet_1.code `${TypeValue}.unwrap(${TypeValue}.decode(value))`;
60
+ }
49
61
  TypeValue = ts_poet_1.imp(`${name}@./google/protobuf/wrappers`);
50
62
  return ts_poet_1.code `${TypeValue}.decode(value).value`;
51
63
  }
package/build/main.js CHANGED
@@ -77,6 +77,8 @@ function generateFile(ctx, fileDesc) {
77
77
  if (options.outputPartialMethods) {
78
78
  staticMembers.push(generateFromPartial(ctx, fullName, message));
79
79
  }
80
+ staticMembers.push(...generateWrap(fullTypeName));
81
+ staticMembers.push(...generateUnwrap(fullTypeName));
80
82
  chunks.push(ts_poet_1.code `
81
83
  export const ${ts_poet_1.def(fullName)} = {
82
84
  ${ts_poet_1.joinCode(staticMembers, { on: ',\n\n' })}
@@ -529,7 +531,14 @@ function generateDecode(ctx, fullName, messageDesc) {
529
531
  }
530
532
  else if (types_1.isValueType(ctx, field)) {
531
533
  const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
532
- readSnippet = ts_poet_1.code `${type}.decode(reader, reader.uint32()).value`;
534
+ const unwrap = (decodedValue) => {
535
+ if (types_1.isListValueType(field) || types_1.isStructType(field) || types_1.isAnyValueType(field)) {
536
+ return ts_poet_1.code `${type}.unwrap(${decodedValue})`;
537
+ }
538
+ return ts_poet_1.code `${decodedValue}.value`;
539
+ };
540
+ const decoder = ts_poet_1.code `${type}.decode(reader, reader.uint32())`;
541
+ readSnippet = ts_poet_1.code `${unwrap(decoder)}`;
533
542
  }
534
543
  else if (types_1.isTimestamp(field) && (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.STRING)) {
535
544
  const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
@@ -624,10 +633,16 @@ function generateEncode(ctx, fullName, messageDesc) {
624
633
  writeSnippet = (place) => ts_poet_1.code `${type}.encode(${utils.toTimestamp}(${place}), writer.uint32(${tag}).fork()).ldelim()`;
625
634
  }
626
635
  else if (types_1.isValueType(ctx, field)) {
627
- const tag = ((field.number << 3) | 2) >>> 0;
628
- const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
629
636
  const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : '';
630
- writeSnippet = (place) => ts_poet_1.code `${type}.encode({ ${maybeTypeField} value: ${place}! }, writer.uint32(${tag}).fork()).ldelim()`;
637
+ const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
638
+ const wrappedValue = (place) => {
639
+ if (types_1.isAnyValueType(field) || types_1.isListValueType(field) || types_1.isStructType(field)) {
640
+ return ts_poet_1.code `${type}.wrap(${place})`;
641
+ }
642
+ return ts_poet_1.code `{${maybeTypeField} value: ${place}!}`;
643
+ };
644
+ const tag = ((field.number << 3) | 2) >>> 0;
645
+ writeSnippet = (place) => ts_poet_1.code `${type}.encode(${wrappedValue(place)}, writer.uint32(${tag}).fork()).ldelim()`;
631
646
  }
632
647
  else if (types_1.isMessage(field)) {
633
648
  const tag = ((field.number << 3) | 2) >>> 0;
@@ -743,6 +758,7 @@ function generateFromJson(ctx, fullName, messageDesc) {
743
758
  // add a check for each incoming field
744
759
  messageDesc.field.forEach((field) => {
745
760
  const fieldName = case_1.maybeSnakeToCamel(field.name, options);
761
+ const jsonName = utils_1.determineFieldJsonName(field, options);
746
762
  // get a generic 'reader.doSomething' bit that is specific to the basic type
747
763
  const readSnippet = (from) => {
748
764
  if (types_1.isEnum(field)) {
@@ -775,6 +791,9 @@ function generateFromJson(ctx, fullName, messageDesc) {
775
791
  (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.TIMESTAMP)) {
776
792
  return ts_poet_1.code `${utils.fromJsonTimestamp}(${from})`;
777
793
  }
794
+ else if (types_1.isAnyValueType(field) || types_1.isStructType(field)) {
795
+ return ts_poet_1.code `${from}`;
796
+ }
778
797
  else if (types_1.isValueType(ctx, field)) {
779
798
  const valueType = types_1.valueTypeName(ctx, field.typeName);
780
799
  if (types_1.isLongValueType(field) && options.forceLong === options_1.LongOption.LONG) {
@@ -783,6 +802,9 @@ function generateFromJson(ctx, fullName, messageDesc) {
783
802
  else if (types_1.isBytesValueType(field)) {
784
803
  return ts_poet_1.code `new ${case_1.capitalize(valueType.toCodeString())}(${from})`;
785
804
  }
805
+ else if (types_1.isListValueType(field)) {
806
+ return ts_poet_1.code `[...${from}]`;
807
+ }
786
808
  else {
787
809
  return ts_poet_1.code `${case_1.capitalize(valueType.toCodeString())}(${from})`;
788
810
  }
@@ -822,6 +844,9 @@ function generateFromJson(ctx, fullName, messageDesc) {
822
844
  const type = types_1.basicTypeName(ctx, valueType);
823
845
  return ts_poet_1.code `${from} as ${type}`;
824
846
  }
847
+ else if (types_1.isAnyValueType(valueType)) {
848
+ return ts_poet_1.code `${from}`;
849
+ }
825
850
  else {
826
851
  const type = types_1.basicTypeName(ctx, valueType);
827
852
  return ts_poet_1.code `${type}.fromJSON(${from})`;
@@ -839,36 +864,53 @@ function generateFromJson(ctx, fullName, messageDesc) {
839
864
  // and then use the snippet to handle repeated fields if necessary
840
865
  if (types_1.isRepeated(field)) {
841
866
  if (types_1.isMapType(ctx, messageDesc, field)) {
842
- chunks.push(ts_poet_1.code `message.${fieldName} = {};`);
843
- chunks.push(ts_poet_1.code `if (object.${fieldName} !== undefined && object.${fieldName} !== null) {`);
867
+ const fieldType = types_1.toTypeName(ctx, messageDesc, field);
844
868
  const i = maybeCastToNumber(ctx, messageDesc, field, 'key');
845
869
  chunks.push(ts_poet_1.code `
846
- Object.entries(object.${fieldName}).forEach(([key, value]) => {
847
- message.${fieldName}[${i}] = ${readSnippet('value')};
848
- });
870
+ message.${fieldName} = Object.entries(object.${jsonName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => {
871
+ acc[${i}] = ${readSnippet('value')};
872
+ return acc;
873
+ }, {});
874
+ `);
875
+ }
876
+ else if (types_1.isAnyValueType(field)) {
877
+ chunks.push(ts_poet_1.code `
878
+ message.${fieldName} = Array.isArray(object?.${jsonName}) ? [...object.${jsonName}] : [];
849
879
  `);
850
- chunks.push(ts_poet_1.code `}`);
851
880
  }
852
881
  else {
853
882
  // Explicit `any` type required to make TS with noImplicitAny happy. `object` is also `any` here.
854
883
  chunks.push(ts_poet_1.code `
855
- message.${fieldName} = (object.${fieldName} ?? []).map((e: any) => ${readSnippet('e')});
884
+ message.${fieldName} = (object.${jsonName} ?? []).map((e: any) => ${readSnippet('e')});
856
885
  `);
857
886
  }
858
887
  }
859
888
  else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
860
- chunks.push(ts_poet_1.code `if (object.${fieldName} !== undefined && object.${fieldName} !== null) {`);
889
+ chunks.push(ts_poet_1.code `if (object.${jsonName} !== undefined && object.${jsonName} !== null) {`);
861
890
  const oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
862
891
  chunks.push(ts_poet_1.code `
863
- message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${fieldName}`)} }
892
+ message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${jsonName}`)} }
864
893
  `);
865
894
  chunks.push(ts_poet_1.code `}`);
866
895
  }
896
+ else if (types_1.isAnyValueType(field)) {
897
+ chunks.push(ts_poet_1.code `message.${fieldName} = object.${fieldName};`);
898
+ }
899
+ else if (types_1.isStructType(field)) {
900
+ chunks.push(ts_poet_1.code `message.${fieldName} = typeof(object.${fieldName}) === 'object' ? object.${fieldName} : undefined;`);
901
+ }
902
+ else if (types_1.isListValueType(field)) {
903
+ chunks.push(ts_poet_1.code `
904
+ message.${fieldName} = Array.isArray(object?.${fieldName})
905
+ ? ${readSnippet(`object.${fieldName}`)}
906
+ : ${'undefined'};
907
+ `);
908
+ }
867
909
  else {
868
910
  const fallback = types_1.isWithinOneOf(field) ? 'undefined' : types_1.defaultValue(ctx, field);
869
911
  chunks.push(ts_poet_1.code `
870
- message.${fieldName} = (object.${fieldName} !== undefined && object.${fieldName} !== null)
871
- ? ${readSnippet(`object.${fieldName}`)}
912
+ message.${fieldName} = (object.${jsonName} !== undefined && object.${jsonName} !== null)
913
+ ? ${readSnippet(`object.${jsonName}`)}
872
914
  : ${fallback};
873
915
  `);
874
916
  }
@@ -889,6 +931,7 @@ function generateToJson(ctx, fullName, messageDesc) {
889
931
  // then add a case for each field
890
932
  messageDesc.field.forEach((field) => {
891
933
  const fieldName = case_1.maybeSnakeToCamel(field.name, options);
934
+ const jsonName = utils_1.determineFieldJsonName(field, options);
892
935
  const readSnippet = (from) => {
893
936
  if (types_1.isEnum(field)) {
894
937
  const toJson = types_1.getEnumMethod(typeMap, field.typeName, 'ToJSON');
@@ -930,11 +973,17 @@ function generateToJson(ctx, fullName, messageDesc) {
930
973
  else if (types_1.isScalar(valueType) || types_1.isValueType(ctx, valueType)) {
931
974
  return ts_poet_1.code `${from}`;
932
975
  }
976
+ else if (types_1.isAnyValueType(valueType)) {
977
+ return ts_poet_1.code `${from}`;
978
+ }
933
979
  else {
934
980
  const type = types_1.basicTypeName(ctx, valueType);
935
981
  return ts_poet_1.code `${type}.toJSON(${from})`;
936
982
  }
937
983
  }
984
+ else if (types_1.isAnyValueType(field)) {
985
+ return ts_poet_1.code `${from}`;
986
+ }
938
987
  else if (types_1.isMessage(field) && !types_1.isValueType(ctx, field) && !types_1.isMapType(ctx, messageDesc, field)) {
939
988
  const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
940
989
  return ts_poet_1.code `${from} ? ${type}.toJSON(${from}) : ${types_1.defaultValue(ctx, field)}`;
@@ -958,10 +1007,10 @@ function generateToJson(ctx, fullName, messageDesc) {
958
1007
  if (types_1.isMapType(ctx, messageDesc, field)) {
959
1008
  // Maps might need their values transformed, i.e. bytes --> base64
960
1009
  chunks.push(ts_poet_1.code `
961
- obj.${fieldName} = {};
1010
+ obj.${jsonName} = {};
962
1011
  if (message.${fieldName}) {
963
1012
  Object.entries(message.${fieldName}).forEach(([k, v]) => {
964
- obj.${fieldName}[k] = ${readSnippet('v')};
1013
+ obj.${jsonName}[k] = ${readSnippet('v')};
965
1014
  });
966
1015
  }
967
1016
  `);
@@ -970,9 +1019,9 @@ function generateToJson(ctx, fullName, messageDesc) {
970
1019
  // Arrays might need their elements transformed
971
1020
  chunks.push(ts_poet_1.code `
972
1021
  if (message.${fieldName}) {
973
- obj.${fieldName} = message.${fieldName}.map(e => ${readSnippet('e')});
1022
+ obj.${jsonName} = message.${fieldName}.map(e => ${readSnippet('e')});
974
1023
  } else {
975
- obj.${fieldName} = [];
1024
+ obj.${jsonName} = [];
976
1025
  }
977
1026
  `);
978
1027
  }
@@ -980,11 +1029,11 @@ function generateToJson(ctx, fullName, messageDesc) {
980
1029
  // oneofs in a union are only output as `oneof name = ...`
981
1030
  const oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
982
1031
  const v = readSnippet(`message.${oneofName}?.${fieldName}`);
983
- chunks.push(ts_poet_1.code `message.${oneofName}?.$case === '${fieldName}' && (obj.${fieldName} = ${v});`);
1032
+ chunks.push(ts_poet_1.code `message.${oneofName}?.$case === '${fieldName}' && (obj.${jsonName} = ${v});`);
984
1033
  }
985
1034
  else {
986
1035
  const v = readSnippet(`message.${fieldName}`);
987
- chunks.push(ts_poet_1.code `message.${fieldName} !== undefined && (obj.${fieldName} = ${v});`);
1036
+ chunks.push(ts_poet_1.code `message.${fieldName} !== undefined && (obj.${jsonName} = ${v});`);
988
1037
  }
989
1038
  });
990
1039
  chunks.push(ts_poet_1.code `return obj;`);
@@ -1030,6 +1079,9 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1030
1079
  return ts_poet_1.code `${cstr}(${from})`;
1031
1080
  }
1032
1081
  }
1082
+ else if (types_1.isAnyValueType(valueType)) {
1083
+ return ts_poet_1.code `${from}`;
1084
+ }
1033
1085
  else if (types_1.isTimestamp(valueType) &&
1034
1086
  (options.useDate === options_1.DateOption.DATE || options.useDate === options_1.DateOption.STRING)) {
1035
1087
  return ts_poet_1.code `${from}`;
@@ -1042,6 +1094,9 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1042
1094
  return ts_poet_1.code `${type}.fromPartial(${from})`;
1043
1095
  }
1044
1096
  }
1097
+ else if (types_1.isAnyValueType(field)) {
1098
+ return ts_poet_1.code `${from}`;
1099
+ }
1045
1100
  else {
1046
1101
  const type = types_1.basicTypeName(ctx, field);
1047
1102
  return ts_poet_1.code `${type}.fromPartial(${from})`;
@@ -1054,17 +1109,16 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1054
1109
  // and then use the snippet to handle repeated fields if necessary
1055
1110
  if (types_1.isRepeated(field)) {
1056
1111
  if (types_1.isMapType(ctx, messageDesc, field)) {
1057
- chunks.push(ts_poet_1.code `message.${fieldName} = {};`);
1058
- chunks.push(ts_poet_1.code `if (object.${fieldName} !== undefined && object.${fieldName} !== null) {`);
1112
+ const fieldType = types_1.toTypeName(ctx, messageDesc, field);
1059
1113
  const i = maybeCastToNumber(ctx, messageDesc, field, 'key');
1060
1114
  chunks.push(ts_poet_1.code `
1061
- Object.entries(object.${fieldName}).forEach(([key, value]) => {
1115
+ message.${fieldName} = Object.entries(object.${fieldName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => {
1062
1116
  if (value !== undefined) {
1063
- message.${fieldName}[${i}] = ${readSnippet('value')};
1117
+ acc[${i}] = ${readSnippet('value')};
1064
1118
  }
1065
- });
1119
+ return acc;
1120
+ }, {});
1066
1121
  `);
1067
- chunks.push(ts_poet_1.code `}`);
1068
1122
  }
1069
1123
  else {
1070
1124
  chunks.push(ts_poet_1.code `
@@ -1104,6 +1158,82 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1104
1158
  chunks.push(ts_poet_1.code `}`);
1105
1159
  return ts_poet_1.joinCode(chunks, { on: '\n' });
1106
1160
  }
1161
+ function generateWrap(fullProtoTypeName) {
1162
+ const chunks = [];
1163
+ if (types_1.isStructTypeName(fullProtoTypeName)) {
1164
+ chunks.push(ts_poet_1.code `wrap(object: {[key: string]: any} | undefined): Struct {
1165
+ const struct = Struct.fromPartial({});
1166
+ if (object !== undefined) {
1167
+ Object.keys(object).forEach(key => {
1168
+ struct.fields[key] = object[key];
1169
+ });
1170
+ }
1171
+ return struct;
1172
+ }`);
1173
+ }
1174
+ if (types_1.isAnyValueTypeName(fullProtoTypeName)) {
1175
+ chunks.push(ts_poet_1.code `wrap(value: any): Value {
1176
+ if (value === null) {
1177
+ return {nullValue: 0} as Value;
1178
+ } else if (typeof value === 'boolean') {
1179
+ return {boolValue: value} as Value;
1180
+ } else if (typeof value === 'number') {
1181
+ return {numberValue: value} as Value;
1182
+ } else if (typeof value === 'string') {
1183
+ return {stringValue: value} as Value;
1184
+ } else if (Array.isArray(value)) {
1185
+ return {listValue: value} as Value;
1186
+ } else if (typeof value === 'object') {
1187
+ return {structValue: value} as Value;
1188
+ } else if (typeof value === 'undefined') {
1189
+ return {} as Value;
1190
+ } else {
1191
+ throw new Error('Unsupported any value type: ' + typeof value);
1192
+ }
1193
+ }`);
1194
+ }
1195
+ if (types_1.isListValueTypeName(fullProtoTypeName)) {
1196
+ chunks.push(ts_poet_1.code `wrap(value: Array<any>): ListValue {
1197
+ return {values: value};
1198
+ }`);
1199
+ }
1200
+ return chunks;
1201
+ }
1202
+ function generateUnwrap(fullProtoTypeName) {
1203
+ const chunks = [];
1204
+ if (types_1.isStructTypeName(fullProtoTypeName)) {
1205
+ chunks.push(ts_poet_1.code `unwrap(message: Struct): {[key: string]: any} {
1206
+ const object: { [key: string]: any } = {};
1207
+ Object.keys(message.fields).forEach(key => {
1208
+ object[key] = message.fields[key];
1209
+ });
1210
+ return object;
1211
+ }`);
1212
+ }
1213
+ if (types_1.isAnyValueTypeName(fullProtoTypeName)) {
1214
+ chunks.push(ts_poet_1.code `unwrap(message: Value): string | number | boolean | Object | null | Array<any> | undefined {
1215
+ if (message.stringValue !== undefined) {
1216
+ return message.stringValue;
1217
+ } else if (message.numberValue !== undefined) {
1218
+ return message.numberValue;
1219
+ } else if (message.boolValue !== undefined) {
1220
+ return message.boolValue;
1221
+ } else if (message.structValue !== undefined) {
1222
+ return message.structValue;
1223
+ } else if (message.listValue !== undefined) {
1224
+ return message.listValue;
1225
+ } else if (message.nullValue !== undefined) {
1226
+ return null;
1227
+ }
1228
+ }`);
1229
+ }
1230
+ if (types_1.isListValueTypeName(fullProtoTypeName)) {
1231
+ chunks.push(ts_poet_1.code `unwrap(message: ListValue): Array<any> {
1232
+ return message.values;
1233
+ }`);
1234
+ }
1235
+ return chunks;
1236
+ }
1107
1237
  exports.contextTypeVar = 'Context extends DataLoaders';
1108
1238
  function maybeCastToNumber(ctx, messageDesc, field, variableName) {
1109
1239
  const { keyType } = types_1.detectMapType(ctx, messageDesc, field);
package/build/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectBatchMethod = exports.responsePromiseOrObservable = exports.responseObservable = exports.responsePromise = exports.responseType = exports.requestType = exports.rawRequestType = exports.detectMapType = exports.toTypeName = exports.getEnumMethod = exports.messageToTypeName = exports.wrapperTypeName = exports.valueTypeName = exports.isEmptyType = exports.isLongValueType = exports.isBytesValueType = exports.isValueType = exports.isTimestamp = exports.isMapType = exports.isLong = exports.isRepeated = exports.isWithinOneOfThatShouldBeUnion = exports.isWithinOneOf = exports.isEnum = exports.isMessage = exports.isBytes = exports.isPrimitive = exports.isScalar = exports.createTypeMap = exports.notDefaultCheck = exports.defaultValue = exports.packedType = exports.toReaderCall = exports.basicTypeName = exports.basicLongWireType = exports.basicWireType = void 0;
3
+ exports.detectBatchMethod = exports.responsePromiseOrObservable = exports.responseObservable = exports.responsePromise = exports.responseType = exports.requestType = exports.rawRequestType = exports.detectMapType = exports.toTypeName = exports.getEnumMethod = exports.messageToTypeName = exports.wrapperTypeName = exports.valueTypeName = exports.isEmptyType = exports.isLongValueType = exports.isStructTypeName = exports.isStructType = exports.isListValueTypeName = exports.isListValueType = exports.isBytesValueType = exports.isAnyValueTypeName = exports.isAnyValueType = exports.isValueType = exports.isTimestamp = exports.isMapType = exports.isLong = exports.isRepeated = exports.isWithinOneOfThatShouldBeUnion = exports.isWithinOneOf = exports.isEnum = exports.isMessage = exports.isBytes = exports.isPrimitive = exports.isScalar = exports.createTypeMap = exports.notDefaultCheck = exports.defaultValue = exports.packedType = exports.toReaderCall = exports.basicTypeName = exports.basicLongWireType = exports.basicWireType = void 0;
4
4
  const ts_proto_descriptors_1 = require("ts-proto-descriptors");
5
5
  const ts_poet_1 = require("ts-poet");
6
6
  const options_1 = require("./options");
@@ -359,10 +359,34 @@ function isValueType(ctx, field) {
359
359
  return valueTypeName(ctx, field.typeName) !== undefined;
360
360
  }
361
361
  exports.isValueType = isValueType;
362
+ function isAnyValueType(field) {
363
+ return isAnyValueTypeName(field.typeName);
364
+ }
365
+ exports.isAnyValueType = isAnyValueType;
366
+ function isAnyValueTypeName(typeName) {
367
+ return typeName === 'google.protobuf.Value' || typeName === '.google.protobuf.Value';
368
+ }
369
+ exports.isAnyValueTypeName = isAnyValueTypeName;
362
370
  function isBytesValueType(field) {
363
371
  return field.typeName === '.google.protobuf.BytesValue';
364
372
  }
365
373
  exports.isBytesValueType = isBytesValueType;
374
+ function isListValueType(field) {
375
+ return isListValueTypeName(field.typeName);
376
+ }
377
+ exports.isListValueType = isListValueType;
378
+ function isListValueTypeName(typeName) {
379
+ return typeName === 'google.protobuf.ListValue' || typeName === '.google.protobuf.ListValue';
380
+ }
381
+ exports.isListValueTypeName = isListValueTypeName;
382
+ function isStructType(field) {
383
+ return isStructTypeName(field.typeName);
384
+ }
385
+ exports.isStructType = isStructType;
386
+ function isStructTypeName(typeName) {
387
+ return typeName === 'google.protobuf.Struct' || typeName === '.google.protobuf.Struct';
388
+ }
389
+ exports.isStructTypeName = isStructTypeName;
366
390
  function isLongValueType(field) {
367
391
  return field.typeName === '.google.protobuf.Int64Value' || field.typeName === '.google.protobuf.UInt64Value';
368
392
  }
@@ -388,6 +412,12 @@ function valueTypeName(ctx, typeName) {
388
412
  return ts_poet_1.code `boolean`;
389
413
  case '.google.protobuf.BytesValue':
390
414
  return ts_poet_1.code `Uint8Array`;
415
+ case '.google.protobuf.ListValue':
416
+ return ts_poet_1.code `Array<any>`;
417
+ case '.google.protobuf.Value':
418
+ return ts_poet_1.code `any`;
419
+ case '.google.protobuf.Struct':
420
+ return ts_poet_1.code `{[key: string]: any}`;
391
421
  default:
392
422
  return undefined;
393
423
  }
@@ -404,7 +434,9 @@ function wrapperTypeName(typeName) {
404
434
  case '.google.protobuf.UInt64Value':
405
435
  case '.google.protobuf.BoolValue':
406
436
  case '.google.protobuf.BytesValue':
437
+ case '.google.protobuf.ListValue':
407
438
  case '.google.protobuf.Timestamp':
439
+ case '.google.protobuf.Struct':
408
440
  return typeName.split('.')[3];
409
441
  default:
410
442
  return undefined;
@@ -525,7 +557,7 @@ function requestType(ctx, methodDesc) {
525
557
  }
526
558
  exports.requestType = requestType;
527
559
  function responseType(ctx, methodDesc) {
528
- return messageToTypeName(ctx, methodDesc.outputType);
560
+ return messageToTypeName(ctx, methodDesc.outputType, { keepValueType: true });
529
561
  }
530
562
  exports.responseType = responseType;
531
563
  function responsePromise(ctx, methodDesc) {
package/build/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FormattedMethodDescriptor = exports.assertInstanceOf = exports.maybePrefixPackage = exports.prefixDisableLinter = exports.maybeAddComment = exports.upperFirst = exports.lowerFirst = exports.singular = exports.fail = exports.readToBuffer = exports.protoFilesToGenerate = void 0;
3
+ exports.determineFieldJsonName = exports.FormattedMethodDescriptor = exports.assertInstanceOf = exports.maybePrefixPackage = exports.prefixDisableLinter = exports.maybeAddComment = exports.upperFirst = exports.lowerFirst = exports.singular = exports.fail = exports.readToBuffer = exports.protoFilesToGenerate = void 0;
4
4
  const ts_poet_1 = require("ts-poet");
5
5
  const options_1 = require("./options");
6
6
  const case_1 = require("./case");
@@ -144,3 +144,12 @@ class FormattedMethodDescriptor {
144
144
  }
145
145
  }
146
146
  exports.FormattedMethodDescriptor = FormattedMethodDescriptor;
147
+ function determineFieldJsonName(field, options) {
148
+ // By default jsonName is camelCased by the protocol compilier unless the user has
149
+ // set a "json_name" option on this field.
150
+ if (field.jsonName.length > 0) {
151
+ return field.jsonName;
152
+ }
153
+ return case_1.maybeSnakeToCamel(field.name, options);
154
+ }
155
+ exports.determineFieldJsonName = determineFieldJsonName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-proto",
3
- "version": "1.87.1",
3
+ "version": "1.90.1",
4
4
  "description": "",
5
5
  "main": "build/plugin.js",
6
6
  "repository": "github:stephenh/ts-proto",
@@ -25,9 +25,9 @@
25
25
  "@grpc/proto-loader": "^0.5.6",
26
26
  "@improbable-eng/grpc-web": "^0.14.0",
27
27
  "@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
28
- "@nestjs/common": "^7.6.15",
29
- "@nestjs/core": "^7.6.15",
30
- "@nestjs/microservices": "^7.6.15",
28
+ "@nestjs/common": "^8.2.2",
29
+ "@nestjs/core": "^8.2.2",
30
+ "@nestjs/microservices": "^8.2.2",
31
31
  "@semantic-release/changelog": "^5.0.1",
32
32
  "@semantic-release/commit-analyzer": "^8.0.1",
33
33
  "@semantic-release/git": "^9.0.0",
@@ -36,11 +36,10 @@
36
36
  "@semantic-release/release-notes-generator": "^9.0.2",
37
37
  "@types/jest": "^26.0.22",
38
38
  "@types/node": "^14.14.37",
39
- "grpc": "^1.24.6",
40
39
  "jest": "^26.6.3",
41
40
  "prettier": "^2.2.1",
42
41
  "reflect-metadata": "^0.1.13",
43
- "rxjs": "^6.6.7",
42
+ "rxjs": "^7.4.0",
44
43
  "semantic-release": "^17.4.2",
45
44
  "ts-jest": "^26.5.4",
46
45
  "ts-node": "^9.1.1",