aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source/Remote/FSTSerializerBeta.mm
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2018-01-31 11:23:55 -0800
committerGravatar GitHub <noreply@github.com>2018-01-31 11:23:55 -0800
commit729b8d176c75ecc0cbbd137cc6811116a64e310a (patch)
tree22b793b03611ce5ad615b7c7d9579f5ba5206b4a /Firestore/Source/Remote/FSTSerializerBeta.mm
parent693d0649bfcc9c32201e2431ae08ea85fdbdb617 (diff)
Move all Firestore Objective-C to Objective-C++ (#734)
* Move all Firestore files to Objective-C++ * Update project file references * Don't use module imports from Objective-C++ * Use extern "C" for C-accessible globals * Work around more stringent type checking in Objective-C++ * NSMutableDictionary ivars aren't implicitly casted to NSDictionary * FSTMaybeDocument callback can't be passed a function that accepts FSTDocument * NSComparisonResult can't be multiplied by -1 without casting * Add a #include <inttypes.h> where needed * Avoid using C++ keywords as variables * Remove #if __cplusplus guards
Diffstat (limited to 'Firestore/Source/Remote/FSTSerializerBeta.mm')
-rw-r--r--Firestore/Source/Remote/FSTSerializerBeta.mm1086
1 files changed, 1086 insertions, 0 deletions
diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm
new file mode 100644
index 0000000..cf200ca
--- /dev/null
+++ b/Firestore/Source/Remote/FSTSerializerBeta.mm
@@ -0,0 +1,1086 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Firestore/Source/Remote/FSTSerializerBeta.h"
+
+#include <inttypes.h>
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h"
+#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h"
+#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h"
+#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h"
+#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h"
+#import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h"
+#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h"
+
+#import "FIRFirestoreErrors.h"
+#import "FIRGeoPoint.h"
+#import "Firestore/Source/Core/FSTQuery.h"
+#import "Firestore/Source/Core/FSTSnapshotVersion.h"
+#import "Firestore/Source/Core/FSTTimestamp.h"
+#import "Firestore/Source/Local/FSTQueryData.h"
+#import "Firestore/Source/Model/FSTDatabaseID.h"
+#import "Firestore/Source/Model/FSTDocument.h"
+#import "Firestore/Source/Model/FSTDocumentKey.h"
+#import "Firestore/Source/Model/FSTFieldValue.h"
+#import "Firestore/Source/Model/FSTMutation.h"
+#import "Firestore/Source/Model/FSTMutationBatch.h"
+#import "Firestore/Source/Model/FSTPath.h"
+#import "Firestore/Source/Remote/FSTExistenceFilter.h"
+#import "Firestore/Source/Remote/FSTWatchChange.h"
+#import "Firestore/Source/Util/FSTAssert.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSerializerBeta ()
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+@end
+
+@implementation FSTSerializerBeta
+
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID {
+ self = [super init];
+ if (self) {
+ _databaseID = databaseID;
+ }
+ return self;
+}
+
+#pragma mark - FSTSnapshotVersion <=> GPBTimestamp
+
+- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp {
+ GPBTimestamp *result = [GPBTimestamp message];
+ result.seconds = timestamp.seconds;
+ result.nanos = timestamp.nanos;
+ return result;
+}
+
+- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp {
+ return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos];
+}
+
+- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version {
+ return [self encodedTimestamp:version.timestamp];
+}
+
+- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version {
+ return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]];
+}
+
+#pragma mark - FIRGeoPoint <=> GTPLatLng
+
+- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint {
+ GTPLatLng *latLng = [GTPLatLng message];
+ latLng.latitude = geoPoint.latitude;
+ latLng.longitude = geoPoint.longitude;
+ return latLng;
+}
+
+- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng {
+ return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude];
+}
+
+#pragma mark - FSTDocumentKey <=> Key proto
+
+- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key {
+ return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path];
+}
+
+- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name {
+ FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name];
+ FSTAssert([[path segmentAtIndex:1] isEqualToString:self.databaseID.projectID],
+ @"Tried to deserialize key from different project.");
+ FSTAssert([[path segmentAtIndex:3] isEqualToString:self.databaseID.databaseID],
+ @"Tried to deserialize key from different datbase.");
+ return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]];
+}
+
+- (NSString *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID
+ path:(FSTResourcePath *)path {
+ return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"]
+ pathByAppendingPath:path] canonicalString];
+}
+
+- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name {
+ FSTResourcePath *path = [FSTResourcePath pathWithString:name];
+ FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path);
+ return path;
+}
+
+- (NSString *)encodedQueryPath:(FSTResourcePath *)path {
+ if (path.length == 0) {
+ // If the path is empty, the backend requires we leave off the /documents at the end.
+ return [self encodedDatabaseID];
+ }
+ return [self encodedResourcePathForDatabaseID:self.databaseID path:path];
+}
+
+- (FSTResourcePath *)decodedQueryPath:(NSString *)name {
+ FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name];
+ if (resource.length == 4) {
+ return [FSTResourcePath pathWithSegments:@[]];
+ } else {
+ return [self localResourcePathForQualifiedResourcePath:resource];
+ }
+}
+
+- (FSTResourcePath *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID {
+ return [FSTResourcePath
+ pathWithSegments:@[ @"projects", databaseID.projectID, @"databases", databaseID.databaseID ]];
+}
+
+- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName {
+ FSTAssert(
+ resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"],
+ @"Tried to deserialize invalid key %@", resourceName);
+ return [resourceName pathByRemovingFirstSegments:5];
+}
+
+- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path {
+ return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] &&
+ [[path segmentAtIndex:2] isEqualToString:@"databases"];
+}
+
+- (NSString *)encodedDatabaseID {
+ return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString];
+}
+
+#pragma mark - FSTFieldValue <=> Value proto
+
+- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue {
+ Class fieldClass = [fieldValue class];
+ if (fieldClass == [FSTNullValue class]) {
+ return [self encodedNull];
+
+ } else if (fieldClass == [FSTBooleanValue class]) {
+ return [self encodedBool:[[fieldValue value] boolValue]];
+
+ } else if (fieldClass == [FSTIntegerValue class]) {
+ return [self encodedInteger:[[fieldValue value] longLongValue]];
+
+ } else if (fieldClass == [FSTDoubleValue class]) {
+ return [self encodedDouble:[[fieldValue value] doubleValue]];
+
+ } else if (fieldClass == [FSTStringValue class]) {
+ return [self encodedString:[fieldValue value]];
+
+ } else if (fieldClass == [FSTTimestampValue class]) {
+ return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue];
+
+ } else if (fieldClass == [FSTGeoPointValue class]) {
+ return [self encodedGeoPointValue:[fieldValue value]];
+
+ } else if (fieldClass == [FSTBlobValue class]) {
+ return [self encodedBlobValue:[fieldValue value]];
+
+ } else if (fieldClass == [FSTReferenceValue class]) {
+ FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue;
+ return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]];
+
+ } else if (fieldClass == [FSTObjectValue class]) {
+ GCFSValue *result = [GCFSValue message];
+ result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue];
+ return result;
+
+ } else if (fieldClass == [FSTArrayValue class]) {
+ GCFSValue *result = [GCFSValue message];
+ result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue];
+ return result;
+
+ } else {
+ FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue);
+ }
+}
+
+- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto {
+ switch (valueProto.valueTypeOneOfCase) {
+ case GCFSValue_ValueType_OneOfCase_NullValue:
+ return [FSTNullValue nullValue];
+
+ case GCFSValue_ValueType_OneOfCase_BooleanValue:
+ return [FSTBooleanValue booleanValue:valueProto.booleanValue];
+
+ case GCFSValue_ValueType_OneOfCase_IntegerValue:
+ return [FSTIntegerValue integerValue:valueProto.integerValue];
+
+ case GCFSValue_ValueType_OneOfCase_DoubleValue:
+ return [FSTDoubleValue doubleValue:valueProto.doubleValue];
+
+ case GCFSValue_ValueType_OneOfCase_StringValue:
+ return [FSTStringValue stringValue:valueProto.stringValue];
+
+ case GCFSValue_ValueType_OneOfCase_TimestampValue:
+ return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]];
+
+ case GCFSValue_ValueType_OneOfCase_GeoPointValue:
+ return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]];
+
+ case GCFSValue_ValueType_OneOfCase_BytesValue:
+ return [FSTBlobValue blobValue:valueProto.bytesValue];
+
+ case GCFSValue_ValueType_OneOfCase_ReferenceValue:
+ return [self decodedReferenceValue:valueProto.referenceValue];
+
+ case GCFSValue_ValueType_OneOfCase_ArrayValue:
+ return [self decodedArrayValue:valueProto.arrayValue];
+
+ case GCFSValue_ValueType_OneOfCase_MapValue:
+ return [self decodedMapValue:valueProto.mapValue];
+
+ default:
+ FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto);
+ }
+}
+
+- (GCFSValue *)encodedNull {
+ GCFSValue *result = [GCFSValue message];
+ result.nullValue = GPBNullValue_NullValue;
+ return result;
+}
+
+- (GCFSValue *)encodedBool:(BOOL)value {
+ GCFSValue *result = [GCFSValue message];
+ result.booleanValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedDouble:(double)value {
+ GCFSValue *result = [GCFSValue message];
+ result.doubleValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedInteger:(int64_t)value {
+ GCFSValue *result = [GCFSValue message];
+ result.integerValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedString:(NSString *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.stringValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.timestampValue = [self encodedTimestamp:value];
+ return result;
+}
+
+- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.geoPointValue = [self encodedGeoPoint:value];
+ return result;
+}
+
+- (GCFSValue *)encodedBlobValue:(NSData *)value {
+ GCFSValue *result = [GCFSValue message];
+ result.bytesValue = value;
+ return result;
+}
+
+- (GCFSValue *)encodedReferenceValueForDatabaseID:(FSTDatabaseID *)databaseID
+ key:(FSTDocumentKey *)key {
+ GCFSValue *result = [GCFSValue message];
+ result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path];
+ return result;
+}
+
+- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName {
+ FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName];
+ NSString *project = [path segmentAtIndex:1];
+ NSString *database = [path segmentAtIndex:3];
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:project database:database];
+ FSTDocumentKey *key =
+ [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]];
+ return [FSTReferenceValue referenceValue:key databaseID:databaseID];
+}
+
+- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue {
+ GCFSArrayValue *proto = [GCFSArrayValue message];
+ NSMutableArray<GCFSValue *> *protoContents = [proto valuesArray];
+
+ [[arrayValue internalValue]
+ enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) {
+ GCFSValue *converted = [self encodedFieldValue:value];
+ [protoContents addObject:converted];
+ }];
+ return proto;
+}
+
+- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue {
+ NSMutableArray<FSTFieldValue *> *contents =
+ [NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count];
+
+ [arrayValue.valuesArray
+ enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) {
+ [contents addObject:[self decodedFieldValue:value]];
+ }];
+ return [[FSTArrayValue alloc] initWithValueNoCopy:contents];
+}
+
+- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value {
+ GCFSMapValue *result = [GCFSMapValue message];
+ result.fields = [self encodedFields:value];
+ return result;
+}
+
+- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map {
+ return [self decodedFields:map.fields];
+}
+
+/**
+ * Encodes an FSTObjectValue into a dictionary.
+ * @return a new dictionary that can be assigned to a field in another proto.
+ */
+- (NSMutableDictionary<NSString *, GCFSValue *> *)encodedFields:(FSTObjectValue *)value {
+ FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *fields = value.internalValue;
+ NSMutableDictionary<NSString *, GCFSValue *> *result = [NSMutableDictionary dictionary];
+ [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) {
+ GCFSValue *converted = [self encodedFieldValue:obj];
+ result[key] = converted;
+ }];
+ return result;
+}
+
+- (FSTObjectValue *)decodedFields:(NSDictionary<NSString *, GCFSValue *> *)fields {
+ __block FSTObjectValue *result = [FSTObjectValue objectValue];
+ [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj,
+ BOOL *_Nonnull stop) {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]];
+ FSTFieldValue *value = [self decodedFieldValue:obj];
+ result = [result objectBySettingValue:value forPath:path];
+ }];
+ return result;
+}
+
+#pragma mark - FSTObjectValue <=> Document proto
+
+- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue
+ key:(FSTDocumentKey *)key {
+ GCFSDocument *proto = [GCFSDocument message];
+ proto.name = [self encodedDocumentKey:key];
+ proto.fields = [self encodedFields:objectValue];
+ return proto;
+}
+
+#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto
+
+- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response {
+ switch (response.resultOneOfCase) {
+ case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found:
+ return [self decodedFoundDocument:response];
+ case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing:
+ return [self decodedDeletedDocument:response];
+ default:
+ FSTFail(@"Unknown document type: %@", response);
+ }
+}
+
+- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response {
+ FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document.");
+ FSTDocumentKey *key = [self decodedDocumentKey:response.found.name];
+ FSTObjectValue *value = [self decodedFields:response.found.fields];
+ FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime];
+ FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
+ @"Got a document response with no snapshot version");
+
+ return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
+}
+
+- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response {
+ FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document.");
+ FSTDocumentKey *key = [self decodedDocumentKey:response.missing];
+ FSTSnapshotVersion *version = [self decodedVersion:response.readTime];
+ FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
+ @"Got a no document response with no snapshot version");
+ return [FSTDeletedDocument documentWithKey:key version:version];
+}
+
+#pragma mark - FSTMutation => GCFSWrite proto
+
+- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation {
+ GCFSWrite *proto = [GCFSWrite message];
+
+ Class mutationClass = [mutation class];
+ if (mutationClass == [FSTSetMutation class]) {
+ FSTSetMutation *set = (FSTSetMutation *)mutation;
+ proto.update = [self encodedDocumentWithFields:set.value key:set.key];
+
+ } else if (mutationClass == [FSTPatchMutation class]) {
+ FSTPatchMutation *patch = (FSTPatchMutation *)mutation;
+ proto.update = [self encodedDocumentWithFields:patch.value key:patch.key];
+ proto.updateMask = [self encodedFieldMask:patch.fieldMask];
+
+ } else if (mutationClass == [FSTTransformMutation class]) {
+ FSTTransformMutation *transform = (FSTTransformMutation *)mutation;
+
+ proto.transform = [GCFSDocumentTransform message];
+ proto.transform.document = [self encodedDocumentKey:transform.key];
+ proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms];
+ // NOTE: We set a precondition of exists: true as a safety-check, since we always combine
+ // FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should
+ // end up with an existing document.
+ proto.currentDocument.exists = YES;
+
+ } else if (mutationClass == [FSTDeleteMutation class]) {
+ FSTDeleteMutation *deleteMutation = (FSTDeleteMutation *)mutation;
+ proto.delete_p = [self encodedDocumentKey:deleteMutation.key];
+
+ } else {
+ FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass));
+ }
+
+ if (!mutation.precondition.isNone) {
+ proto.currentDocument = [self encodedPrecondition:mutation.precondition];
+ }
+
+ return proto;
+}
+
+- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation {
+ FSTPrecondition *precondition = [mutation hasCurrentDocument]
+ ? [self decodedPrecondition:mutation.currentDocument]
+ : [FSTPrecondition none];
+
+ switch (mutation.operationOneOfCase) {
+ case GCFSWrite_Operation_OneOfCase_Update:
+ if (mutation.hasUpdateMask) {
+ return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name]
+ fieldMask:[self decodedFieldMask:mutation.updateMask]
+ value:[self decodedFields:mutation.update.fields]
+ precondition:precondition];
+ } else {
+ return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name]
+ value:[self decodedFields:mutation.update.fields]
+ precondition:precondition];
+ }
+
+ case GCFSWrite_Operation_OneOfCase_Delete_p:
+ return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p]
+ precondition:precondition];
+
+ case GCFSWrite_Operation_OneOfCase_Transform: {
+ FSTPreconditionExists exists = precondition.exists;
+ FSTAssert(exists == FSTPreconditionExistsYes,
+ @"Transforms must have precondition \"exists == true\"");
+
+ return [[FSTTransformMutation alloc]
+ initWithKey:[self decodedDocumentKey:mutation.transform.document]
+ fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]];
+ }
+
+ default:
+ // Note that insert is intentionally unhandled, since we don't ever deal in them.
+ FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase);
+ }
+}
+
+- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition {
+ FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition");
+ GCFSPrecondition *message = [GCFSPrecondition message];
+ if (precondition.updateTime) {
+ message.updateTime = [self encodedVersion:precondition.updateTime];
+ } else if (precondition.exists != FSTPreconditionExistsNotSet) {
+ message.exists = precondition.exists == FSTPreconditionExistsYes;
+ } else {
+ FSTFail(@"Unknown precondition: %@", precondition);
+ }
+ return message;
+}
+
+- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition {
+ switch (precondition.conditionTypeOneOfCase) {
+ case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase:
+ return [FSTPrecondition none];
+
+ case GCFSPrecondition_ConditionType_OneOfCase_Exists:
+ return [FSTPrecondition preconditionWithExists:precondition.exists];
+
+ case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime:
+ return [FSTPrecondition
+ preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]];
+
+ default:
+ FSTFail(@"Unrecognized Precondition one-of case %@", precondition);
+ }
+}
+
+- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask {
+ GCFSDocumentMask *mask = [GCFSDocumentMask message];
+ for (FSTFieldPath *field in fieldMask.fields) {
+ [mask.fieldPathsArray addObject:field.canonicalString];
+ }
+ return mask;
+}
+
+- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask {
+ NSMutableArray<FSTFieldPath *> *fields =
+ [NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count];
+ for (NSString *path in fieldMask.fieldPathsArray) {
+ [fields addObject:[FSTFieldPath pathWithServerFormat:path]];
+ }
+ return [[FSTFieldMask alloc] initWithFields:fields];
+}
+
+- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms:
+ (NSArray<FSTFieldTransform *> *)fieldTransforms {
+ NSMutableArray *protos = [NSMutableArray array];
+ for (FSTFieldTransform *fieldTransform in fieldTransforms) {
+ FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]],
+ @"Unknown transform: %@", fieldTransform.transform);
+ GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message];
+ proto.fieldPath = fieldTransform.path.canonicalString;
+ proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime;
+ [protos addObject:proto];
+ }
+ return protos;
+}
+
+- (NSArray<FSTFieldTransform *> *)decodedFieldTransforms:
+ (NSArray<GCFSDocumentTransform_FieldTransform *> *)protos {
+ NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array];
+ for (GCFSDocumentTransform_FieldTransform *proto in protos) {
+ FSTAssert(
+ proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime,
+ @"Unknown transform setToServerValue: %d", proto.setToServerValue);
+ [fieldTransforms
+ addObject:[[FSTFieldTransform alloc]
+ initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath]
+ transform:[FSTServerTimestampTransform serverTimestampTransform]]];
+ }
+ return fieldTransforms;
+}
+
+#pragma mark - FSTMutationResult <= GCFSWriteResult proto
+
+- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation {
+ // NOTE: Deletes don't have an updateTime.
+ FSTSnapshotVersion *_Nullable version =
+ mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil;
+ NSMutableArray *_Nullable transformResults = nil;
+ if (mutation.transformResultsArray.count > 0) {
+ transformResults = [NSMutableArray array];
+ for (GCFSValue *result in mutation.transformResultsArray) {
+ [transformResults addObject:[self decodedFieldValue:result]];
+ }
+ }
+ return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults];
+}
+
+#pragma mark - FSTQueryData => GCFSTarget proto
+
+- (nullable NSMutableDictionary<NSString *, NSString *> *)encodedListenRequestLabelsForQueryData:
+ (FSTQueryData *)queryData {
+ NSString *value = [self encodedLabelForPurpose:queryData.purpose];
+ if (!value) {
+ return nil;
+ }
+
+ NSMutableDictionary<NSString *, NSString *> *result =
+ [NSMutableDictionary dictionaryWithCapacity:1];
+ [result setObject:value forKey:@"goog-listen-tags"];
+ return result;
+}
+
+- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose {
+ switch (purpose) {
+ case FSTQueryPurposeListen:
+ return nil;
+ case FSTQueryPurposeExistenceFilterMismatch:
+ return @"existence-filter-mismatch";
+ case FSTQueryPurposeLimboResolution:
+ return @"limbo-document";
+ default:
+ FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose);
+ }
+}
+
+- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData {
+ GCFSTarget *result = [GCFSTarget message];
+ FSTQuery *query = queryData.query;
+
+ if ([query isDocumentQuery]) {
+ result.documents = [self encodedDocumentsTarget:query];
+ } else {
+ result.query = [self encodedQueryTarget:query];
+ }
+
+ result.targetId = queryData.targetID;
+ if (queryData.resumeToken.length > 0) {
+ result.resumeToken = queryData.resumeToken;
+ }
+
+ return result;
+}
+
+- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query {
+ GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message];
+ NSMutableArray<NSString *> *docs = result.documentsArray;
+ [docs addObject:[self encodedQueryPath:query.path]];
+ return result;
+}
+
+- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target {
+ NSArray<NSString *> *documents = target.documentsArray;
+ FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu",
+ (unsigned long)documents.count);
+
+ NSString *name = documents[0];
+ return [FSTQuery queryWithPath:[self decodedQueryPath:name]];
+}
+
+- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query {
+ // Dissect the path into parent, collectionId, and optional key filter.
+ GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message];
+ if (query.path.length == 0) {
+ queryTarget.parent = [self encodedQueryPath:query.path];
+ } else {
+ FSTResourcePath *path = query.path;
+ FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported.");
+ queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]];
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = path.lastSegment;
+ [queryTarget.structuredQuery.fromArray addObject:from];
+ }
+
+ // Encode the filters.
+ GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters];
+ if (where) {
+ queryTarget.structuredQuery.where = where;
+ }
+
+ NSArray<GCFSStructuredQuery_Order *> *orders = [self encodedSortOrders:query.sortOrders];
+ if (orders.count) {
+ [queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders];
+ }
+
+ if (query.limit != NSNotFound) {
+ queryTarget.structuredQuery.limit.value = (int32_t)query.limit;
+ }
+
+ if (query.startAt) {
+ queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt];
+ }
+
+ if (query.endAt) {
+ queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt];
+ }
+
+ return queryTarget;
+}
+
+- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target {
+ FSTResourcePath *path = [self decodedQueryPath:target.parent];
+
+ GCFSStructuredQuery *query = target.structuredQuery;
+ NSUInteger fromCount = query.fromArray_Count;
+ if (fromCount > 0) {
+ FSTAssert(fromCount == 1,
+ @"StructuredQuery.from with more than one collection is not supported.");
+
+ GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0];
+ path = [path pathByAppendingSegment:from.collectionId];
+ }
+
+ NSArray<id<FSTFilter>> *filterBy;
+ if (query.hasWhere) {
+ filterBy = [self decodedFilters:query.where];
+ } else {
+ filterBy = @[];
+ }
+
+ NSArray<FSTSortOrder *> *orderBy;
+ if (query.orderByArray_Count > 0) {
+ orderBy = [self decodedSortOrders:query.orderByArray];
+ } else {
+ orderBy = @[];
+ }
+
+ NSInteger limit = NSNotFound;
+ if (query.hasLimit) {
+ limit = query.limit.value;
+ }
+
+ FSTBound *_Nullable startAt;
+ if (query.hasStartAt) {
+ startAt = [self decodedBound:query.startAt];
+ }
+
+ FSTBound *_Nullable endAt;
+ if (query.hasEndAt) {
+ endAt = [self decodedBound:query.endAt];
+ }
+
+ return [[FSTQuery alloc] initWithPath:path
+ filterBy:filterBy
+ orderBy:orderBy
+ limit:limit
+ startAt:startAt
+ endAt:endAt];
+}
+
+#pragma mark Filters
+
+- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray<id<FSTFilter>> *)filters {
+ if (filters.count == 0) {
+ return nil;
+ }
+ NSMutableArray<GCFSStructuredQuery_Filter *> *protos = [NSMutableArray array];
+ for (id<FSTFilter> filter in filters) {
+ if ([filter isKindOfClass:[FSTRelationFilter class]]) {
+ [protos addObject:[self encodedRelationFilter:filter]];
+ } else {
+ [protos addObject:[self encodedUnaryFilter:filter]];
+ }
+ }
+ if (protos.count == 1) {
+ // Special case: no existing filters and we only need to add one filter. This can be made the
+ // single root filter without a composite filter.
+ return protos[0];
+ }
+ GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message];
+ composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And;
+ composite.compositeFilter.filtersArray = protos;
+ return composite;
+}
+
+- (NSArray<id<FSTFilter>> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto {
+ NSMutableArray<id<FSTFilter>> *result = [NSMutableArray array];
+
+ NSArray<GCFSStructuredQuery_Filter *> *filters;
+ if (proto.filterTypeOneOfCase ==
+ GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) {
+ FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And,
+ @"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op);
+ filters = proto.compositeFilter.filtersArray;
+ } else {
+ filters = @[ proto ];
+ }
+
+ for (GCFSStructuredQuery_Filter *filter in filters) {
+ switch (filter.filterTypeOneOfCase) {
+ case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter:
+ FSTFail(@"Nested composite filters are not supported");
+
+ case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter:
+ [result addObject:[self decodedRelationFilter:filter.fieldFilter]];
+ break;
+
+ case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter:
+ [result addObject:[self decodedUnaryFilter:filter.unaryFilter]];
+ break;
+
+ default:
+ FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase);
+ }
+ }
+ return result;
+}
+
+- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter {
+ GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter;
+ fieldFilter.field = [self encodedFieldPath:filter.field];
+ fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator];
+ fieldFilter.value = [self encodedFieldValue:filter.value];
+ return proto;
+}
+
+- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto {
+ FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
+ FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op];
+ FSTFieldValue *value = [self decodedFieldValue:proto.value];
+ return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value];
+}
+
+- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id<FSTFilter>)filter {
+ GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message];
+ proto.unaryFilter.field = [self encodedFieldPath:filter.field];
+ if ([filter isKindOfClass:[FSTNanFilter class]]) {
+ proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan;
+ } else if ([filter isKindOfClass:[FSTNullFilter class]]) {
+ proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull;
+ } else {
+ FSTFail(@"Unrecognized filter: %@", filter);
+ }
+ return proto;
+}
+
+- (id<FSTFilter>)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto {
+ FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
+ switch (proto.op) {
+ case GCFSStructuredQuery_UnaryFilter_Operator_IsNan:
+ return [[FSTNanFilter alloc] initWithField:field];
+
+ case GCFSStructuredQuery_UnaryFilter_Operator_IsNull:
+ return [[FSTNullFilter alloc] initWithField:field];
+
+ default:
+ FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op);
+ }
+}
+
+- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath {
+ GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message];
+ ref.fieldPath = fieldPath.canonicalString;
+ return ref;
+}
+
+- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator:
+ (FSTRelationFilterOperator)filterOperator {
+ switch (filterOperator) {
+ case FSTRelationFilterOperatorLessThan:
+ return GCFSStructuredQuery_FieldFilter_Operator_LessThan;
+ case FSTRelationFilterOperatorLessThanOrEqual:
+ return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual;
+ case FSTRelationFilterOperatorEqual:
+ return GCFSStructuredQuery_FieldFilter_Operator_Equal;
+ case FSTRelationFilterOperatorGreaterThanOrEqual:
+ return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual;
+ case FSTRelationFilterOperatorGreaterThan:
+ return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan;
+ default:
+ FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator);
+ }
+}
+
+- (FSTRelationFilterOperator)decodedRelationFilterOperator:
+ (GCFSStructuredQuery_FieldFilter_Operator)filterOperator {
+ switch (filterOperator) {
+ case GCFSStructuredQuery_FieldFilter_Operator_LessThan:
+ return FSTRelationFilterOperatorLessThan;
+ case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual:
+ return FSTRelationFilterOperatorLessThanOrEqual;
+ case GCFSStructuredQuery_FieldFilter_Operator_Equal:
+ return FSTRelationFilterOperatorEqual;
+ case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual:
+ return FSTRelationFilterOperatorGreaterThanOrEqual;
+ case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan:
+ return FSTRelationFilterOperatorGreaterThan;
+ default:
+ FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator);
+ }
+}
+
+#pragma mark Property Orders
+
+- (NSArray<GCFSStructuredQuery_Order *> *)encodedSortOrders:(NSArray<FSTSortOrder *> *)orders {
+ NSMutableArray<GCFSStructuredQuery_Order *> *protos = [NSMutableArray array];
+ for (FSTSortOrder *order in orders) {
+ [protos addObject:[self encodedSortOrder:order]];
+ }
+ return protos;
+}
+
+- (NSArray<FSTSortOrder *> *)decodedSortOrders:(NSArray<GCFSStructuredQuery_Order *> *)protos {
+ NSMutableArray<FSTSortOrder *> *result = [NSMutableArray arrayWithCapacity:protos.count];
+ for (GCFSStructuredQuery_Order *orderProto in protos) {
+ [result addObject:[self decodedSortOrder:orderProto]];
+ }
+ return result;
+}
+
+- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder {
+ GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message];
+ proto.field = [self encodedFieldPath:sortOrder.field];
+ if (sortOrder.ascending) {
+ proto.direction = GCFSStructuredQuery_Direction_Ascending;
+ } else {
+ proto.direction = GCFSStructuredQuery_Direction_Descending;
+ }
+ return proto;
+}
+
+- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto {
+ FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
+ BOOL ascending;
+ switch (proto.direction) {
+ case GCFSStructuredQuery_Direction_Ascending:
+ ascending = YES;
+ break;
+ case GCFSStructuredQuery_Direction_Descending:
+ ascending = NO;
+ break;
+ default:
+ FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction);
+ }
+ return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending];
+}
+
+#pragma mark - Bounds/Cursors
+
+- (GCFSCursor *)encodedBound:(FSTBound *)bound {
+ GCFSCursor *proto = [GCFSCursor message];
+ proto.before = bound.isBefore;
+ for (FSTFieldValue *fieldValue in bound.position) {
+ GCFSValue *value = [self encodedFieldValue:fieldValue];
+ [proto.valuesArray addObject:value];
+ }
+ return proto;
+}
+
+- (FSTBound *)decodedBound:(GCFSCursor *)proto {
+ NSMutableArray<FSTFieldValue *> *indexComponents = [NSMutableArray array];
+
+ for (GCFSValue *valueProto in proto.valuesArray) {
+ FSTFieldValue *value = [self decodedFieldValue:valueProto];
+ [indexComponents addObject:value];
+ }
+
+ return [FSTBound boundWithPosition:indexComponents isBefore:proto.before];
+}
+
+#pragma mark - FSTWatchChange <= GCFSListenResponse proto
+
+- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange {
+ switch (watchChange.responseTypeOneOfCase) {
+ case GCFSListenResponse_ResponseType_OneOfCase_TargetChange:
+ return [self decodedTargetChangeFromWatchChange:watchChange.targetChange];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange:
+ return [self decodedDocumentChange:watchChange.documentChange];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete:
+ return [self decodedDocumentDelete:watchChange.documentDelete];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove:
+ return [self decodedDocumentRemove:watchChange.documentRemove];
+
+ case GCFSListenResponse_ResponseType_OneOfCase_Filter:
+ return [self decodedExistenceFilterWatchChange:watchChange.filter];
+
+ default:
+ FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase);
+ }
+}
+
+- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange {
+ // We have only reached a consistent snapshot for the entire stream if there is a read_time set
+ // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to
+ // send such responses.
+ if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) {
+ return [FSTSnapshotVersion noVersion];
+ }
+ if (watchChange.targetChange.targetIdsArray.count != 0) {
+ return [FSTSnapshotVersion noVersion];
+ }
+ return [self decodedVersion:watchChange.targetChange.readTime];
+}
+
+- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change {
+ FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType];
+ NSMutableArray<NSNumber *> *targetIDs =
+ [NSMutableArray arrayWithCapacity:change.targetIdsArray_Count];
+
+ [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) {
+ [targetIDs addObject:@(value)];
+ }];
+
+ NSError *cause = nil;
+ if (change.hasCause) {
+ cause = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:change.cause.code
+ userInfo:@{NSLocalizedDescriptionKey : change.cause.message}];
+ }
+
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:change.resumeToken
+ cause:cause];
+}
+
+- (FSTWatchTargetChangeState)decodedWatchTargetChangeState:
+ (GCFSTargetChange_TargetChangeType)state {
+ switch (state) {
+ case GCFSTargetChange_TargetChangeType_NoChange:
+ return FSTWatchTargetChangeStateNoChange;
+ case GCFSTargetChange_TargetChangeType_Add:
+ return FSTWatchTargetChangeStateAdded;
+ case GCFSTargetChange_TargetChangeType_Remove:
+ return FSTWatchTargetChangeStateRemoved;
+ case GCFSTargetChange_TargetChangeType_Current:
+ return FSTWatchTargetChangeStateCurrent;
+ case GCFSTargetChange_TargetChangeType_Reset:
+ return FSTWatchTargetChangeStateReset;
+ default:
+ FSTFail(@"Unexpected TargetChange.state: %" PRId32, state);
+ }
+}
+
+- (NSArray<NSNumber *> *)decodedIntegerArray:(GPBInt32Array *)values {
+ NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:values.count];
+ [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) {
+ [result addObject:@(value)];
+ }];
+ return result;
+}
+
+- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change {
+ FSTObjectValue *value = [self decodedFields:change.document.fields];
+ FSTDocumentKey *key = [self decodedDocumentKey:change.document.name];
+ FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime];
+ FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
+ @"Got a document change with no snapshot version");
+ FSTMaybeDocument *document =
+ [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
+
+ NSArray<NSNumber *> *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray];
+ NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
+
+ return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds
+ removedTargetIDs:removedTargetIds
+ documentKey:document.key
+ document:document];
+}
+
+- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change {
+ FSTDocumentKey *key = [self decodedDocumentKey:change.document];
+ // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion]
+ FSTSnapshotVersion *version = [self decodedVersion:change.readTime];
+ FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version];
+
+ NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
+
+ return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:removedTargetIds
+ documentKey:document.key
+ document:document];
+}
+
+- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change {
+ FSTDocumentKey *key = [self decodedDocumentKey:change.document];
+ NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
+
+ return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:removedTargetIds
+ documentKey:key
+ document:nil];
+}
+
+- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter {
+ // TODO(dimond): implement existence filter parsing
+ FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count];
+ FSTTargetID targetID = filter.targetId;
+ return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END