Skip to content

Commit 721b83b

Browse files
author
Michael Lehenbauer
committed
Clean up some serializer stuff.
1. The switch to protobufjs 6.x seems to have removed the need for hasTag() (and actually it had broken our usage of it since the tags ended up changing from snake_case to camelCase). So I've removed hasTag() and set `oneofs: false` in protoLoaderOptions so the virtual "fooType" fields are no longer added. 2. 'seconds' in timestamps should always be serialized as a string, since it's an int64 but we weren't doing that. I fixed it, which removed some of the special casing from rsgowman/grpc_load. 3. I reworked the expect...RoundTrip methods into a single unified one which saves some repetition and I think is easier to make sense of. 4. I re-added the disabled useProto3Json assert and instead tweaked the offending test not to try to round-trip through protobufjs.
1 parent c5e87f0 commit 721b83b

File tree

3 files changed

+162
-318
lines changed

3 files changed

+162
-318
lines changed

packages/firestore/src/platform_node/load_protos.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import * as grpc from 'grpc';
2020
import * as path from 'path';
2121
import * as ProtobufJS from 'protobufjs';
2222

23-
export const protoLoaderOptions = {
23+
/** Used by tests so we can match @grpc/proto-loader behavior. */
24+
export const protoLoaderOptions: ProtobufJS.IConversionOptions = {
2425
longs: String,
2526
enums: String,
2627
defaults: true,
27-
oneofs: true
28+
oneofs: false
2829
};
2930

3031
/**
@@ -44,12 +45,13 @@ export function loadProtos(): grpc.GrpcObject {
4445

4546
const packageDefinition = protoLoader.loadSync(firestoreProtoFile, {
4647
...protoLoaderOptions,
47-
...{ includeDirs: [root] }
48+
includeDirs: [root]
4849
});
4950

5051
return grpc.loadPackageDefinition(packageDefinition);
5152
}
5253

54+
/** Used by tests so we can directly create ProtobufJS proto message objects from JSON protos. */
5355
export function loadRawProtos(): ProtobufJS.Root {
5456
const root = path.resolve(
5557
__dirname,

packages/firestore/src/remote/serializer.ts

Lines changed: 28 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class JsonProtoSerializer {
207207
*/
208208
private toTimestamp(timestamp: Timestamp): string {
209209
return {
210-
seconds: timestamp.seconds,
210+
seconds: '' + timestamp.seconds,
211211
nanos: timestamp.nanoseconds
212212
// tslint:disable-next-line:no-any
213213
} as any;
@@ -284,6 +284,10 @@ export class JsonProtoSerializer {
284284
);
285285
return Blob.fromBase64String(blob);
286286
} else {
287+
assert(
288+
!this.options.useProto3Json,
289+
'Expected bytes to be passed in as Uint8Array, but got a string instead.'
290+
);
287291
return Blob.fromUint8Array(blob);
288292
}
289293
}
@@ -444,15 +448,13 @@ export class JsonProtoSerializer {
444448
}
445449

446450
fromValue(obj: api.Value): fieldValue.FieldValue {
447-
// tslint:disable-next-line:no-any
448-
const type = (obj as any)['value_type'];
449-
if (hasTag(obj, type, 'nullValue')) {
451+
if ('nullValue' in obj) {
450452
return fieldValue.NullValue.INSTANCE;
451-
} else if (hasTag(obj, type, 'booleanValue')) {
453+
} else if ('booleanValue' in obj) {
452454
return fieldValue.BooleanValue.of(obj.booleanValue!);
453-
} else if (hasTag(obj, type, 'integerValue')) {
455+
} else if ('integerValue' in obj) {
454456
return new fieldValue.IntegerValue(parseInt64(obj.integerValue!));
455-
} else if (hasTag(obj, type, 'doubleValue')) {
457+
} else if ('doubleValue' in obj) {
456458
if (this.options.useProto3Json) {
457459
// Proto 3 uses the string values 'NaN' and 'Infinity'.
458460
if ((obj.doubleValue as {}) === 'NaN') {
@@ -465,30 +467,30 @@ export class JsonProtoSerializer {
465467
}
466468

467469
return new fieldValue.DoubleValue(obj.doubleValue!);
468-
} else if (hasTag(obj, type, 'stringValue')) {
470+
} else if ('stringValue' in obj) {
469471
return new fieldValue.StringValue(obj.stringValue!);
470-
} else if (hasTag(obj, type, 'mapValue')) {
472+
} else if ('mapValue' in obj) {
471473
return this.fromFields(obj.mapValue!.fields || {});
472-
} else if (hasTag(obj, type, 'arrayValue')) {
474+
} else if ('arrayValue' in obj) {
473475
// "values" is not present if the array is empty
474476
assertPresent(obj.arrayValue, 'arrayValue');
475477
const values = obj.arrayValue!.values || [];
476478
return new fieldValue.ArrayValue(values.map(v => this.fromValue(v)));
477-
} else if (hasTag(obj, type, 'timestampValue')) {
479+
} else if ('timestampValue' in obj) {
478480
assertPresent(obj.timestampValue, 'timestampValue');
479481
return new fieldValue.TimestampValue(
480482
this.fromTimestamp(obj.timestampValue!)
481483
);
482-
} else if (hasTag(obj, type, 'geoPointValue')) {
484+
} else if ('geoPointValue' in obj) {
483485
assertPresent(obj.geoPointValue, 'geoPointValue');
484486
const latitude = obj.geoPointValue!.latitude || 0;
485487
const longitude = obj.geoPointValue!.longitude || 0;
486488
return new fieldValue.GeoPointValue(new GeoPoint(latitude, longitude));
487-
} else if (hasTag(obj, type, 'bytesValue')) {
489+
} else if ('bytesValue' in obj) {
488490
assertPresent(obj.bytesValue, 'bytesValue');
489491
const blob = this.fromBlob(obj.bytesValue!);
490492
return new fieldValue.BlobValue(blob);
491-
} else if (hasTag(obj, type, 'referenceValue')) {
493+
} else if ('referenceValue' in obj) {
492494
assertPresent(obj.referenceValue, 'referenceValue');
493495
const resourceName = this.fromResourceName(obj.referenceValue!);
494496
const dbId = new DatabaseId(resourceName.get(1), resourceName.get(3));
@@ -596,11 +598,9 @@ export class JsonProtoSerializer {
596598
}
597599

598600
fromMaybeDocument(result: api.BatchGetDocumentsResponse): MaybeDocument {
599-
// tslint:disable-next-line:no-any
600-
const type = (result as any)['result'];
601-
if (hasTag(result, type, 'found')) {
601+
if ('found' in result) {
602602
return this.fromFound(result);
603-
} else if (hasTag(result, type, 'missing')) {
603+
} else if ('missing' in result) {
604604
return this.fromMissing(result);
605605
}
606606
return fail('invalid batch get response: ' + JSON.stringify(result));
@@ -687,10 +687,8 @@ export class JsonProtoSerializer {
687687
}
688688

689689
fromWatchChange(change: api.ListenResponse): WatchChange {
690-
// tslint:disable-next-line:no-any
691-
const type = (change as any)['response_type'];
692690
let watchChange: WatchChange;
693-
if (hasTag(change, type, 'targetChange')) {
691+
if ('targetChange' in change) {
694692
assertPresent(change.targetChange, 'targetChange');
695693
// proto3 default value is unset in JSON (undefined), so use 'NO_CHANGE'
696694
// if unset
@@ -708,7 +706,7 @@ export class JsonProtoSerializer {
708706
resumeToken,
709707
cause || null
710708
);
711-
} else if (hasTag(change, type, 'documentChange')) {
709+
} else if ('documentChange' in change) {
712710
assertPresent(change.documentChange, 'documentChange');
713711
assertPresent(change.documentChange!.document, 'documentChange.name');
714712
assertPresent(
@@ -740,7 +738,7 @@ export class JsonProtoSerializer {
740738
doc.key,
741739
doc
742740
);
743-
} else if (hasTag(change, type, 'documentDelete')) {
741+
} else if ('documentDelete' in change) {
744742
assertPresent(change.documentDelete, 'documentDelete');
745743
assertPresent(change.documentDelete!.document, 'documentDelete.document');
746744
const docDelete = change.documentDelete!;
@@ -751,14 +749,14 @@ export class JsonProtoSerializer {
751749
const doc = new NoDocument(key, version);
752750
const removedTargetIds = docDelete.removedTargetIds || [];
753751
watchChange = new DocumentWatchChange([], removedTargetIds, doc.key, doc);
754-
} else if (hasTag(change, type, 'documentRemove')) {
752+
} else if ('documentRemove' in change) {
755753
assertPresent(change.documentRemove, 'documentRemove');
756754
assertPresent(change.documentRemove!.document, 'documentRemove');
757755
const docRemove = change.documentRemove!;
758756
const key = this.fromName(docRemove.document!);
759757
const removedTargetIds = docRemove.removedTargetIds || [];
760758
watchChange = new DocumentWatchChange([], removedTargetIds, key, null);
761-
} else if (hasTag(change, type, 'filter')) {
759+
} else if ('filter' in change) {
762760
// TODO(dimond): implement existence filter parsing with strategy.
763761
assertPresent(change.filter, 'filter');
764762
assertPresent(change.filter!.targetId, 'filter.targetId');
@@ -795,9 +793,7 @@ export class JsonProtoSerializer {
795793
// We have only reached a consistent snapshot for the entire stream if there
796794
// is a read_time set and it applies to all targets (i.e. the list of
797795
// targets is empty). The backend is guaranteed to send such responses.
798-
// tslint:disable-next-line:no-any
799-
const type = (change as any)['response_type'];
800-
if (!hasTag(change, type, 'targetChange')) {
796+
if (!('targetChange' in change)) {
801797
return SnapshotVersion.MIN;
802798
}
803799
const targetChange = change.targetChange!;
@@ -963,26 +959,24 @@ export class JsonProtoSerializer {
963959
}
964960

965961
private fromFieldTransform(proto: api.FieldTransform): FieldTransform {
966-
// tslint:disable-next-line:no-any We need to match generated Proto types.
967-
const type = (proto as any)['transform_type'];
968962
let transform: TransformOperation | null = null;
969-
if (hasTag(proto, type, 'setToServerValue')) {
963+
if ('setToServerValue' in proto) {
970964
assert(
971965
proto.setToServerValue === 'REQUEST_TIME',
972966
'Unknown server value transform proto: ' + JSON.stringify(proto)
973967
);
974968
transform = ServerTimestampTransform.instance;
975-
} else if (hasTag(proto, type, 'appendMissingElements')) {
969+
} else if ('appendMissingElements' in proto) {
976970
const values = proto.appendMissingElements!.values || [];
977971
transform = new ArrayUnionTransformOperation(
978972
values.map(v => this.fromValue(v))
979973
);
980-
} else if (hasTag(proto, type, 'removeAllFromArray')) {
974+
} else if ('removeAllFromArray' in proto) {
981975
const values = proto.removeAllFromArray!.values || [];
982976
transform = new ArrayRemoveTransformOperation(
983977
values.map(v => this.fromValue(v))
984978
);
985-
} else if (hasTag(proto, type, 'increment')) {
979+
} else if ('increment' in proto) {
986980
const operand = this.fromValue(proto.increment!);
987981
assert(
988982
operand instanceof NumberValue,
@@ -1360,35 +1354,3 @@ export class JsonProtoSerializer {
13601354
return FieldMask.fromArray(fields);
13611355
}
13621356
}
1363-
1364-
/**
1365-
* Checks for a specific oneof tag in a protocol buffer message.
1366-
*
1367-
* This intentionally accommodates two distinct cases:
1368-
*
1369-
* 1) Messages containing a type tag: these are the format produced by GRPC in
1370-
* return values. These may contain default-value mappings for all tags in the
1371-
* oneof but the type tag specifies which one was actually set.
1372-
*
1373-
* 2) Messages that don't contain a type tag: these are the format required by
1374-
* GRPC as inputs. If we emitted objects with type tags, ProtoBuf.js would
1375-
* choke claiming that the tags aren't fields in the Message.
1376-
*
1377-
* Allowing both formats here makes the serializer able to consume the outputs
1378-
* it produces: for all messages it supports, fromX(toX(value)) == value.
1379-
*
1380-
* Note that case 2 suffers from ambiguity: if multiple tags are present
1381-
* without a type tag then the callers are structured in such a way that the
1382-
* first invocation will win. Since we only parse in this mode when parsing
1383-
* the output of a serialize method this works, but it's not a general
1384-
* solution.
1385-
*
1386-
* Unfortunately there is no general solution here because proto3 makes it
1387-
* impossible to distinguish unset from explicitly set fields: both have the
1388-
* default value for the type. Without the type tag but multiple value tags
1389-
* it's possible to have default values for each tag in the oneof and not be
1390-
* able to know which was actually in effect.
1391-
*/
1392-
function hasTag(obj: {}, type: string, tag: string): boolean {
1393-
return type === tag || (!type && tag in obj);
1394-
}

0 commit comments

Comments
 (0)