ts-proto 1.88.0 → 1.91.0

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,36 @@
1
+ # [1.91.0](https://github.com/stephenh/ts-proto/compare/v1.90.1...v1.91.0) (2021-11-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * use Long.fromValue instead of Long.fromString for improved robustness regarding already parsed objects ([#405](https://github.com/stephenh/ts-proto/issues/405)) ([7bdc3ee](https://github.com/stephenh/ts-proto/commit/7bdc3eee05ed1318e18e27aa7d5bb2680060f8b6))
7
+
8
+
9
+ ### Features
10
+
11
+ * Include dockerized protoc ([#404](https://github.com/stephenh/ts-proto/issues/404)) ([7564a78](https://github.com/stephenh/ts-proto/commit/7564a7887ccd0bb80cac19d313ee9bd8daae778d))
12
+
13
+ ## [1.90.1](https://github.com/stephenh/ts-proto/compare/v1.90.0...v1.90.1) (2021-11-27)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * 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))
19
+
20
+ # [1.90.0](https://github.com/stephenh/ts-proto/compare/v1.89.0...v1.90.0) (2021-11-24)
21
+
22
+
23
+ ### Features
24
+
25
+ * Add support for 'json_name' annotation ([#408](https://github.com/stephenh/ts-proto/issues/408)) ([b519717](https://github.com/stephenh/ts-proto/commit/b5197174bcaacb8f163cd197d52ab9c645d21d4c))
26
+
27
+ # [1.89.0](https://github.com/stephenh/ts-proto/compare/v1.88.0...v1.89.0) (2021-11-24)
28
+
29
+
30
+ ### Features
31
+
32
+ * Improve map reading (fromJSON/fromPartial) ([#410](https://github.com/stephenh/ts-proto/issues/410)) ([057d438](https://github.com/stephenh/ts-proto/commit/057d438548d95c354331f7d2d767ccff952ad5c6))
33
+
1
34
  # [1.88.0](https://github.com/stephenh/ts-proto/compare/v1.87.1...v1.88.0) (2021-11-22)
2
35
 
3
36
 
package/README.markdown CHANGED
@@ -18,7 +18,7 @@
18
18
  - [Supported options](#supported-options)
19
19
  - [Only Types](#only-types)
20
20
  - [NestJS Support](NESTJS.markdown)
21
- - [Building](#building)
21
+ - [Development](#development)
22
22
  - [Assumptions](#assumptions)
23
23
  - [Todo](#todo)
24
24
  - [OneOf Handling](#oneof-handling)
@@ -362,15 +362,69 @@ Kudos to our sponsors:
362
362
 
363
363
  If you need ts-proto customizations or priority support for your company, you can ping me at [via email](mailto:stephen.haberman@gmail.com).
364
364
 
365
- # Building
365
+ # Development
366
366
 
367
- After running `yarn install`, run `./integration/pbjs.sh` to create the integration test types. These pbjs-generated files are not currently checked in.
367
+ **Requirements**
368
368
 
369
- After this, the tests should pass.
369
+ - [Docker](https://www.docker.com) or [protoc](https://github.com/protocolbuffers/protobuf/releases) v3.19.1
370
+ - `yarn` — `npm install -g yarn`
370
371
 
371
- After making changes to `ts-proto`, you can run `cd integration` and `./codegen.sh` to re-generate the test case `*.ts` output files that are in each `integration/<test-case>/` directory.
372
+ **Setup**
372
373
 
373
- The test suite's proto files (i.e. `simple.proto`, `batching.proto`, etc.) currently have serialized/`.bin` copies checked into git (i.e. `simple.bin`, `batching.bin`, etc.), so that the test suite can run without having to invoke the `protoc` build chain. I.e. if you change the `simple.proto`/etc. files, you'll need to run `./integration/update-bins.sh`, which does require having the `protoc` executable available.
374
+ The commands below assume you have **Docker** installed. To use a **local** copy of `protoc` without docker, use commands suffixed with `:local`
375
+
376
+ - Check out the [repository]() for the latest code.
377
+ - Run `yarn install` to install the dependencies.
378
+ - Run `yarn build:test` or `yarn build:test:local` to generate the test files.
379
+ > _This runs the following commands:_
380
+ > - `proto2bin` — Converts integration test `.proto` files to `.bin`.
381
+ > - `bin2ts` — Runs `ts-proto` on the `.bin` files to generate `.ts` files.
382
+ > - `proto2pbjs` — Generates a reference implementation using `pbjs` for testing compatibility.
383
+ - Run `yarn test`
384
+
385
+ **Workflow**
386
+
387
+ - Modifying the plugin implementation:
388
+ - Run `yarn bin2ts` or `yarn bin2ts:local`.
389
+ _Since the proto files were not changed, you only need to regenerate the typescript files._
390
+ - Run `yarn test` to verify the typescript files are compatible with the reference implementation, and pass other tests.
391
+ - Updating or adding `.proto` files in the integration directory:
392
+ - Run `yarn build:test` to regenerate the integration test files.
393
+ - Run `yarn test` to retest.
394
+
395
+ **Contributing**
396
+
397
+ - Run `yarn build:test` and `yarn test` to make sure everything works.
398
+ - Run `yarn prettier` to format the typescript files.
399
+ - Commit the changes:
400
+ - Also include the generated `.bin` files for the tests where you added or modified `.proto` files.
401
+ > These are checked into git so that the test suite can run without having to invoke the `protoc` build chain.
402
+ - Also include the generated `.ts` files.
403
+ - Create a pull request
404
+
405
+ **Dockerized Protoc**
406
+
407
+ The repository includes a dockerized version of `protoc`, which is configured in [docker-compose.yml](docker-compose.yml).
408
+
409
+ It can be useful in case you want to manually invoke the plugin with a known version of `protoc`.
410
+
411
+ Usage:
412
+
413
+ ```bash
414
+ # Include the protoc alias in your shell.
415
+ . aliases.sh
416
+
417
+ # Run protoc as usual. The ts-proto directory is available in /ts-proto.
418
+ protoc --plugin=/ts-proto/protoc-gen-ts_proto --ts_proto_out=./output -I=./protos ./protoc/*.proto
419
+
420
+ # Or use the ts-protoc alias which specifies the plugin path for you.
421
+ ts-protoc --ts_proto_out=./output -I=./protos ./protoc/*.proto
422
+ ```
423
+
424
+ - All paths must be relative paths _within_ the current working directory of the host. `../` is not allowed
425
+ - Within the docker container, the absolute path to the project root is `/ts-proto`
426
+ - The container mounts the current working directory in `/host`, and sets it as its working directory.
427
+ - Once `aliases.sh` is sourced, you can use the `protoc` command in any folder.
374
428
 
375
429
  # Assumptions
376
430
 
@@ -379,7 +433,6 @@ The test suite's proto files (i.e. `simple.proto`, `batching.proto`, etc.) curre
379
433
  # Todo
380
434
 
381
435
  - Support the string-based encoding of duration in `fromJSON`/`toJSON`
382
- - Support the `json_name` annotation
383
436
  - Make `oneof=unions` the default behavior in 2.0
384
437
  - Probably change `forceLong` default in 2.0, should default to `forceLong=long`
385
438
  - Make `esModuleInterop=true` the default in 2.0
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':
@@ -32,8 +40,6 @@ function generateEncoder(ctx, typeName) {
32
40
  return ts_poet_1.code `${TypeValue}.encode({value: value ?? false}).finish()`;
33
41
  case 'BytesValue':
34
42
  return ts_poet_1.code `${TypeValue}.encode({value: value ?? new Uint8Array()}).finish()`;
35
- case 'ListValue':
36
- return ts_poet_1.code `${TypeValue}.encode({value: value ?? []]}).finish()`;
37
43
  }
38
44
  throw new Error(`unknown wrapper type: ${name}`);
39
45
  }
@@ -41,13 +47,17 @@ exports.generateEncoder = generateEncoder;
41
47
  function generateDecoder(ctx, typeName) {
42
48
  let name = types_1.wrapperTypeName(typeName);
43
49
  if (!name) {
44
- 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)`;
45
51
  }
46
52
  let TypeValue;
47
53
  if (name == 'Timestamp') {
48
54
  TypeValue = ts_poet_1.imp(`${name}@./google/protobuf/timestamp`);
49
55
  return ts_poet_1.code `${TypeValue}.decode(value)`;
50
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
+ }
51
61
  TypeValue = ts_poet_1.imp(`${name}@./google/protobuf/wrappers`);
52
62
  return ts_poet_1.code `${TypeValue}.decode(value).value`;
53
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' })}
@@ -184,7 +186,6 @@ function makeUtils(options) {
184
186
  ...makeDeepPartial(options, longs),
185
187
  ...makeTimestampMethods(options, longs),
186
188
  ...longs,
187
- ...makeWrappingUtils(),
188
189
  };
189
190
  }
190
191
  exports.makeUtils = makeUtils;
@@ -241,57 +242,6 @@ function makeLongUtils(options, bytes) {
241
242
  `);
242
243
  return { numberToLong, longToNumber, longToString, longInit, Long };
243
244
  }
244
- function makeWrappingUtils() {
245
- const wrapAnyValue = ts_poet_1.conditionalOutput('wrapAnyValue', ts_poet_1.code `function wrapAnyValue(value: any): Value {
246
- if (value === null) {
247
- return {nullValue: 0} as Value;
248
- } else if (typeof value === 'boolean') {
249
- return {boolValue: value} as Value;
250
- } else if (typeof value === 'number') {
251
- return {numberValue: value} as Value;
252
- } else if (typeof value === 'string') {
253
- return {stringValue: value} as Value;
254
- } else if (Array.isArray(value)) {
255
- return {listValue: value} as Value;
256
- } else if (typeof value === 'object') {
257
- return {structValue: value} as Value;
258
- } else if (typeof value === 'undefined') {
259
- return {} as Value;
260
- } else {
261
- throw new Error('Unsupported any value type: ' + typeof value);
262
- }
263
- }`);
264
- const unwrapAnyValue = ts_poet_1.conditionalOutput('unwrapAnyValue', ts_poet_1.code `function unwrapAnyValue(value: Value): string | number | boolean | Object | null | Array<any> | undefined {
265
- if (value.stringValue !== undefined) {
266
- return value.stringValue;
267
- } else if (value.numberValue !== undefined) {
268
- return value.numberValue;
269
- } else if (value.boolValue !== undefined) {
270
- return value.boolValue;
271
- } else if (value.structValue !== undefined) {
272
- return value.structValue;
273
- } else if (value.listValue !== undefined) {
274
- return value.listValue;
275
- } else if (value.nullValue !== undefined) {
276
- return null;
277
- }
278
- }`);
279
- const wrapStruct = ts_poet_1.conditionalOutput('wrapStruct', ts_poet_1.code `function wrapStruct(object: {[key: string]: any}): Struct {
280
- const struct = Struct.fromPartial({});
281
- Object.keys(object).forEach(key => {
282
- struct.fields[key] = object[key];
283
- });
284
- return struct;
285
- }`);
286
- const unwrapStruct = ts_poet_1.conditionalOutput('unwrapStruct', ts_poet_1.code `function unwrapStruct(struct: Struct): {[key: string]: any} {
287
- const object: { [key: string]: any } = {};
288
- Object.keys(struct.fields).forEach(key => {
289
- object[key] = struct.fields[key];
290
- });
291
- return object;
292
- }`);
293
- return { wrapAnyValue, unwrapAnyValue, wrapStruct, unwrapStruct };
294
- }
295
245
  function makeByteUtils() {
296
246
  const globalThis = ts_poet_1.conditionalOutput('globalThis', ts_poet_1.code `
297
247
  declare var self: any | undefined;
@@ -580,16 +530,13 @@ function generateDecode(ctx, fullName, messageDesc) {
580
530
  }
581
531
  }
582
532
  else if (types_1.isValueType(ctx, field)) {
533
+ const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
583
534
  const unwrap = (decodedValue) => {
584
- if (types_1.isAnyValueType(field))
585
- return ts_poet_1.code `${ctx.utils.unwrapAnyValue}(${decodedValue})`;
586
- if (types_1.isStructType(field))
587
- return ts_poet_1.code `${ctx.utils.unwrapStruct}(${decodedValue})`;
588
- if (types_1.isListValueType(field))
589
- return ts_poet_1.code `${decodedValue}.values`;
535
+ if (types_1.isListValueType(field) || types_1.isStructType(field) || types_1.isAnyValueType(field)) {
536
+ return ts_poet_1.code `${type}.unwrap(${decodedValue})`;
537
+ }
590
538
  return ts_poet_1.code `${decodedValue}.value`;
591
539
  };
592
- const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
593
540
  const decoder = ts_poet_1.code `${type}.decode(reader, reader.uint32())`;
594
541
  readSnippet = ts_poet_1.code `${unwrap(decoder)}`;
595
542
  }
@@ -687,17 +634,14 @@ function generateEncode(ctx, fullName, messageDesc) {
687
634
  }
688
635
  else if (types_1.isValueType(ctx, field)) {
689
636
  const maybeTypeField = options.outputTypeRegistry ? `$type: '${field.typeName.slice(1)}',` : '';
637
+ const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
690
638
  const wrappedValue = (place) => {
691
- if (types_1.isAnyValueType(field))
692
- return ts_poet_1.code `${ctx.utils.wrapAnyValue}(${place})`;
693
- if (types_1.isListValueType(field))
694
- return ts_poet_1.code `{values: ${place}}`;
695
- if (types_1.isStructType(field))
696
- return ts_poet_1.code `${ctx.utils.wrapStruct}(${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
+ }
697
642
  return ts_poet_1.code `{${maybeTypeField} value: ${place}!}`;
698
643
  };
699
644
  const tag = ((field.number << 3) | 2) >>> 0;
700
- const type = types_1.basicTypeName(ctx, field, { keepValueType: true });
701
645
  writeSnippet = (place) => ts_poet_1.code `${type}.encode(${wrappedValue(place)}, writer.uint32(${tag}).fork()).ldelim()`;
702
646
  }
703
647
  else if (types_1.isMessage(field)) {
@@ -814,6 +758,7 @@ function generateFromJson(ctx, fullName, messageDesc) {
814
758
  // add a check for each incoming field
815
759
  messageDesc.field.forEach((field) => {
816
760
  const fieldName = case_1.maybeSnakeToCamel(field.name, options);
761
+ const jsonName = utils_1.determineFieldJsonName(field, options);
817
762
  // get a generic 'reader.doSomething' bit that is specific to the basic type
818
763
  const readSnippet = (from) => {
819
764
  if (types_1.isEnum(field)) {
@@ -878,7 +823,7 @@ function generateFromJson(ctx, fullName, messageDesc) {
878
823
  }
879
824
  }
880
825
  else if (types_1.isLong(valueType) && options.forceLong === options_1.LongOption.LONG) {
881
- return ts_poet_1.code `Long.fromString(${from} as string)`;
826
+ return ts_poet_1.code `Long.fromValue(${from} as Long | string)`;
882
827
  }
883
828
  else if (types_1.isEnum(valueType)) {
884
829
  return ts_poet_1.code `${from} as number`;
@@ -919,33 +864,32 @@ function generateFromJson(ctx, fullName, messageDesc) {
919
864
  // and then use the snippet to handle repeated fields if necessary
920
865
  if (types_1.isRepeated(field)) {
921
866
  if (types_1.isMapType(ctx, messageDesc, field)) {
922
- chunks.push(ts_poet_1.code `message.${fieldName} = {};`);
923
- chunks.push(ts_poet_1.code `if (object.${fieldName} !== undefined && object.${fieldName} !== null) {`);
867
+ const fieldType = types_1.toTypeName(ctx, messageDesc, field);
924
868
  const i = maybeCastToNumber(ctx, messageDesc, field, 'key');
925
869
  chunks.push(ts_poet_1.code `
926
- Object.entries(object.${fieldName}).forEach(([key, value]) => {
927
- message.${fieldName}[${i}] = ${readSnippet('value')};
928
- });
870
+ message.${fieldName} = Object.entries(object.${jsonName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => {
871
+ acc[${i}] = ${readSnippet('value')};
872
+ return acc;
873
+ }, {});
929
874
  `);
930
- chunks.push(ts_poet_1.code `}`);
931
875
  }
932
876
  else if (types_1.isAnyValueType(field)) {
933
877
  chunks.push(ts_poet_1.code `
934
- message.${fieldName} = Array.isArray(object?.${fieldName}) ? [...object.${fieldName}] : [];
878
+ message.${fieldName} = Array.isArray(object?.${jsonName}) ? [...object.${jsonName}] : [];
935
879
  `);
936
880
  }
937
881
  else {
938
882
  // Explicit `any` type required to make TS with noImplicitAny happy. `object` is also `any` here.
939
883
  chunks.push(ts_poet_1.code `
940
- message.${fieldName} = (object.${fieldName} ?? []).map((e: any) => ${readSnippet('e')});
884
+ message.${fieldName} = (object.${jsonName} ?? []).map((e: any) => ${readSnippet('e')});
941
885
  `);
942
886
  }
943
887
  }
944
888
  else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
945
- 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) {`);
946
890
  const oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
947
891
  chunks.push(ts_poet_1.code `
948
- message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${fieldName}`)} }
892
+ message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${jsonName}`)} }
949
893
  `);
950
894
  chunks.push(ts_poet_1.code `}`);
951
895
  }
@@ -965,8 +909,8 @@ function generateFromJson(ctx, fullName, messageDesc) {
965
909
  else {
966
910
  const fallback = types_1.isWithinOneOf(field) ? 'undefined' : types_1.defaultValue(ctx, field);
967
911
  chunks.push(ts_poet_1.code `
968
- message.${fieldName} = (object.${fieldName} !== undefined && object.${fieldName} !== null)
969
- ? ${readSnippet(`object.${fieldName}`)}
912
+ message.${fieldName} = (object.${jsonName} !== undefined && object.${jsonName} !== null)
913
+ ? ${readSnippet(`object.${jsonName}`)}
970
914
  : ${fallback};
971
915
  `);
972
916
  }
@@ -987,6 +931,7 @@ function generateToJson(ctx, fullName, messageDesc) {
987
931
  // then add a case for each field
988
932
  messageDesc.field.forEach((field) => {
989
933
  const fieldName = case_1.maybeSnakeToCamel(field.name, options);
934
+ const jsonName = utils_1.determineFieldJsonName(field, options);
990
935
  const readSnippet = (from) => {
991
936
  if (types_1.isEnum(field)) {
992
937
  const toJson = types_1.getEnumMethod(typeMap, field.typeName, 'ToJSON');
@@ -1062,10 +1007,10 @@ function generateToJson(ctx, fullName, messageDesc) {
1062
1007
  if (types_1.isMapType(ctx, messageDesc, field)) {
1063
1008
  // Maps might need their values transformed, i.e. bytes --> base64
1064
1009
  chunks.push(ts_poet_1.code `
1065
- obj.${fieldName} = {};
1010
+ obj.${jsonName} = {};
1066
1011
  if (message.${fieldName}) {
1067
1012
  Object.entries(message.${fieldName}).forEach(([k, v]) => {
1068
- obj.${fieldName}[k] = ${readSnippet('v')};
1013
+ obj.${jsonName}[k] = ${readSnippet('v')};
1069
1014
  });
1070
1015
  }
1071
1016
  `);
@@ -1074,9 +1019,9 @@ function generateToJson(ctx, fullName, messageDesc) {
1074
1019
  // Arrays might need their elements transformed
1075
1020
  chunks.push(ts_poet_1.code `
1076
1021
  if (message.${fieldName}) {
1077
- obj.${fieldName} = message.${fieldName}.map(e => ${readSnippet('e')});
1022
+ obj.${jsonName} = message.${fieldName}.map(e => ${readSnippet('e')});
1078
1023
  } else {
1079
- obj.${fieldName} = [];
1024
+ obj.${jsonName} = [];
1080
1025
  }
1081
1026
  `);
1082
1027
  }
@@ -1084,11 +1029,11 @@ function generateToJson(ctx, fullName, messageDesc) {
1084
1029
  // oneofs in a union are only output as `oneof name = ...`
1085
1030
  const oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
1086
1031
  const v = readSnippet(`message.${oneofName}?.${fieldName}`);
1087
- 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});`);
1088
1033
  }
1089
1034
  else {
1090
1035
  const v = readSnippet(`message.${fieldName}`);
1091
- 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});`);
1092
1037
  }
1093
1038
  });
1094
1039
  chunks.push(ts_poet_1.code `return obj;`);
@@ -1164,17 +1109,16 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1164
1109
  // and then use the snippet to handle repeated fields if necessary
1165
1110
  if (types_1.isRepeated(field)) {
1166
1111
  if (types_1.isMapType(ctx, messageDesc, field)) {
1167
- chunks.push(ts_poet_1.code `message.${fieldName} = {};`);
1168
- chunks.push(ts_poet_1.code `if (object.${fieldName} !== undefined && object.${fieldName} !== null) {`);
1112
+ const fieldType = types_1.toTypeName(ctx, messageDesc, field);
1169
1113
  const i = maybeCastToNumber(ctx, messageDesc, field, 'key');
1170
1114
  chunks.push(ts_poet_1.code `
1171
- Object.entries(object.${fieldName}).forEach(([key, value]) => {
1115
+ message.${fieldName} = Object.entries(object.${fieldName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => {
1172
1116
  if (value !== undefined) {
1173
- message.${fieldName}[${i}] = ${readSnippet('value')};
1117
+ acc[${i}] = ${readSnippet('value')};
1174
1118
  }
1175
- });
1119
+ return acc;
1120
+ }, {});
1176
1121
  `);
1177
- chunks.push(ts_poet_1.code `}`);
1178
1122
  }
1179
1123
  else {
1180
1124
  chunks.push(ts_poet_1.code `
@@ -1214,6 +1158,82 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1214
1158
  chunks.push(ts_poet_1.code `}`);
1215
1159
  return ts_poet_1.joinCode(chunks, { on: '\n' });
1216
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
+ }
1217
1237
  exports.contextTypeVar = 'Context extends DataLoaders';
1218
1238
  function maybeCastToNumber(ctx, messageDesc, field, variableName) {
1219
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.isStructType = exports.isListValueTypeName = exports.isListValueType = exports.isBytesValueType = 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;
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");
@@ -360,9 +360,13 @@ function isValueType(ctx, field) {
360
360
  }
361
361
  exports.isValueType = isValueType;
362
362
  function isAnyValueType(field) {
363
- return field.typeName === '.google.protobuf.Value';
363
+ return isAnyValueTypeName(field.typeName);
364
364
  }
365
365
  exports.isAnyValueType = isAnyValueType;
366
+ function isAnyValueTypeName(typeName) {
367
+ return typeName === 'google.protobuf.Value' || typeName === '.google.protobuf.Value';
368
+ }
369
+ exports.isAnyValueTypeName = isAnyValueTypeName;
366
370
  function isBytesValueType(field) {
367
371
  return field.typeName === '.google.protobuf.BytesValue';
368
372
  }
@@ -376,9 +380,13 @@ function isListValueTypeName(typeName) {
376
380
  }
377
381
  exports.isListValueTypeName = isListValueTypeName;
378
382
  function isStructType(field) {
379
- return field.typeName === '.google.protobuf.Struct';
383
+ return isStructTypeName(field.typeName);
380
384
  }
381
385
  exports.isStructType = isStructType;
386
+ function isStructTypeName(typeName) {
387
+ return typeName === 'google.protobuf.Struct' || typeName === '.google.protobuf.Struct';
388
+ }
389
+ exports.isStructTypeName = isStructTypeName;
382
390
  function isLongValueType(field) {
383
391
  return field.typeName === '.google.protobuf.Int64Value' || field.typeName === '.google.protobuf.UInt64Value';
384
392
  }
@@ -428,6 +436,7 @@ function wrapperTypeName(typeName) {
428
436
  case '.google.protobuf.BytesValue':
429
437
  case '.google.protobuf.ListValue':
430
438
  case '.google.protobuf.Timestamp':
439
+ case '.google.protobuf.Struct':
431
440
  return typeName.split('.')[3];
432
441
  default:
433
442
  return undefined;
@@ -548,7 +557,7 @@ function requestType(ctx, methodDesc) {
548
557
  }
549
558
  exports.requestType = requestType;
550
559
  function responseType(ctx, methodDesc) {
551
- return messageToTypeName(ctx, methodDesc.outputType);
560
+ return messageToTypeName(ctx, methodDesc.outputType, { keepValueType: true });
552
561
  }
553
562
  exports.responseType = responseType;
554
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.88.0",
3
+ "version": "1.91.0",
4
4
  "description": "",
5
5
  "main": "build/plugin.js",
6
6
  "repository": "github:stephenh/ts-proto",
@@ -9,10 +9,19 @@
9
9
  },
10
10
  "scripts": {
11
11
  "build": "yarn tsc",
12
- "setup": "cd ./integration && ./pbjs.sh && ./update-bins.sh && ./codegen.sh",
12
+ "build:test": "yarn proto2bin && yarn proto2pbjs && yarn bin2ts",
13
+ "build:test:local": "yarn proto2bin:local && yarn proto2pbjs:local && yarn bin2ts:local",
14
+ "proto2bin": "docker-compose run --rm protoc update-bins.sh",
15
+ "proto2bin-node": "docker-compose run --rm node update-bins.sh",
16
+ "proto2pbjs": "docker-compose run --rm protoc pbjs.sh",
17
+ "bin2ts": "docker-compose run --rm protoc codegen.sh",
18
+ "proto2bin:local": "integration/update-bins.sh",
19
+ "proto2pbjs:local": "integration/pbjs.sh",
20
+ "bin2ts:local": "integration/codegen.sh",
13
21
  "test": "yarn jest -c jest.config.js --maxWorkers=2",
14
22
  "prettier": "prettier --write {src,tests}/**/*.ts",
15
- "prettier:check": "prettier --list-different {src,tests}/**/*.ts"
23
+ "prettier:check": "prettier --list-different {src,tests}/**/*.ts",
24
+ "setup:docker": "docker-compose build"
16
25
  },
17
26
  "files": [
18
27
  "build"
@@ -25,9 +34,9 @@
25
34
  "@grpc/proto-loader": "^0.5.6",
26
35
  "@improbable-eng/grpc-web": "^0.14.0",
27
36
  "@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",
37
+ "@nestjs/common": "^8.2.2",
38
+ "@nestjs/core": "^8.2.2",
39
+ "@nestjs/microservices": "^8.2.2",
31
40
  "@semantic-release/changelog": "^5.0.1",
32
41
  "@semantic-release/commit-analyzer": "^8.0.1",
33
42
  "@semantic-release/git": "^9.0.0",
@@ -36,11 +45,10 @@
36
45
  "@semantic-release/release-notes-generator": "^9.0.2",
37
46
  "@types/jest": "^26.0.22",
38
47
  "@types/node": "^14.14.37",
39
- "grpc": "^1.24.6",
40
48
  "jest": "^26.6.3",
41
49
  "prettier": "^2.2.1",
42
50
  "reflect-metadata": "^0.1.13",
43
- "rxjs": "^6.6.7",
51
+ "rxjs": "^7.4.0",
44
52
  "semantic-release": "^17.4.2",
45
53
  "ts-jest": "^26.5.4",
46
54
  "ts-node": "^9.1.1",