diff options
author | Gil <mcg@google.com> | 2018-01-31 11:23:55 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-31 11:23:55 -0800 |
commit | 729b8d176c75ecc0cbbd137cc6811116a64e310a (patch) | |
tree | 22b793b03611ce5ad615b7c7d9579f5ba5206b4a /Firestore/Source/Remote/FSTSerializerBeta.mm | |
parent | 693d0649bfcc9c32201e2431ae08ea85fdbdb617 (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.mm | 1086 |
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 |