/* * 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" #import #include #include #include #include #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/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Remote/FSTExistenceFilter.h" #import "Firestore/Source/Remote/FSTWatchChange.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "absl/memory/memory.h" #include "absl/types/optional.h" namespace util = firebase::firestore::util; using firebase::Timestamp; using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::ServerTimestampTransform; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TransformOperation; NS_ASSUME_NONNULL_BEGIN @interface FSTSerializerBeta () // Does not own this DatabaseId. @property(nonatomic, assign, readonly) const DatabaseId *databaseID; @end @implementation FSTSerializerBeta - (instancetype)initWithDatabaseID:(const DatabaseId *)databaseID { self = [super init]; if (self) { _databaseID = databaseID; } return self; } #pragma mark - SnapshotVersion <=> GPBTimestamp - (GPBTimestamp *)encodedTimestamp:(const Timestamp &)timestamp { GPBTimestamp *result = [GPBTimestamp message]; result.seconds = timestamp.seconds(); result.nanos = timestamp.nanoseconds(); return result; } - (Timestamp)decodedTimestamp:(GPBTimestamp *)timestamp { return Timestamp{timestamp.seconds, timestamp.nanos}; } - (GPBTimestamp *)encodedVersion:(const SnapshotVersion &)version { return [self encodedTimestamp:version.timestamp()]; } - (SnapshotVersion)decodedVersion:(GPBTimestamp *)version { return SnapshotVersion{[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 - DocumentKey <=> Key proto - (NSString *)encodedDocumentKey:(const DocumentKey &)key { return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path()]; } - (DocumentKey)decodedDocumentKey:(NSString *)name { const ResourcePath path = [self decodedResourcePathWithDatabaseID:name]; HARD_ASSERT(path[1] == self.databaseID->project_id(), "Tried to deserialize key from different project."); HARD_ASSERT(path[3] == self.databaseID->database_id(), "Tried to deserialize key from different datbase."); return DocumentKey{[self localResourcePathForQualifiedResourcePath:path]}; } - (NSString *)encodedResourcePathForDatabaseID:(const DatabaseId *)databaseID path:(const ResourcePath &)path { return util::WrapNSString([self encodedResourcePathForDatabaseID:databaseID] .Append("documents") .Append(path) .CanonicalString()); } - (ResourcePath)decodedResourcePathWithDatabaseID:(NSString *)name { const ResourcePath path = ResourcePath::FromString(util::MakeStringView(name)); HARD_ASSERT([self validQualifiedResourcePath:path], "Tried to deserialize invalid key %s", path.CanonicalString()); return path; } - (NSString *)encodedQueryPath:(const ResourcePath &)path { if (path.size() == 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]; } - (ResourcePath)decodedQueryPath:(NSString *)name { const ResourcePath resource = [self decodedResourcePathWithDatabaseID:name]; if (resource.size() == 4) { return ResourcePath{}; } else { return [self localResourcePathForQualifiedResourcePath:resource]; } } - (ResourcePath)encodedResourcePathForDatabaseID:(const DatabaseId *)databaseID { return ResourcePath{"projects", databaseID->project_id(), "databases", databaseID->database_id()}; } - (ResourcePath)localResourcePathForQualifiedResourcePath:(const ResourcePath &)resourceName { HARD_ASSERT(resourceName.size() > 4 && resourceName[4] == "documents", "Tried to deserialize invalid key %s", resourceName.CanonicalString()); return resourceName.PopFirst(5); } - (BOOL)validQualifiedResourcePath:(const ResourcePath &)path { return path.size() >= 4 && path[0] == "projects" && path[2] == "databases"; } - (NSString *)encodedDatabaseID { return util::WrapNSString( [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]) { FIRTimestamp *value = static_cast([fieldValue value]); return [self encodedTimestampValue:Timestamp{value.seconds, value.nanoseconds}]; } 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 { HARD_FAIL("Unhandled type %s on %s", 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: { Timestamp value = [self decodedTimestamp:valueProto.timestampValue]; return [FSTTimestampValue timestampValue:[FIRTimestamp timestampWithSeconds:value.seconds() nanoseconds:value.nanoseconds()]]; } 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: HARD_FAIL("Unhandled type %s on %s", 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:(const Timestamp &)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:(const DatabaseId *)databaseID key:(const DocumentKey &)key { HARD_ASSERT(*databaseID == *self.databaseID, "Database %s:%s cannot encode reference from %s:%s", self.databaseID->project_id(), self.databaseID->database_id(), databaseID->project_id(), databaseID->database_id()); GCFSValue *result = [GCFSValue message]; result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path()]; return result; } - (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { const ResourcePath path = [self decodedResourcePathWithDatabaseID:resourceName]; const std::string &project = path[1]; const std::string &database = path[3]; const DocumentKey key{[self localResourcePathForQualifiedResourcePath:path]}; const DatabaseId database_id(project, database); HARD_ASSERT(database_id == *self.databaseID, "Database %s:%s cannot encode reference from %s:%s", self.databaseID->project_id(), self.databaseID->database_id(), database_id.project_id(), database_id.database_id()); return [FSTReferenceValue referenceValue:key databaseID:self.databaseID]; } - (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { GCFSArrayValue *proto = [GCFSArrayValue message]; NSMutableArray *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 *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 *)encodedFields:(FSTObjectValue *)value { FSTImmutableSortedDictionary *fields = value.internalValue; NSMutableDictionary *result = [NSMutableDictionary dictionary]; [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { GCFSValue *converted = [self encodedFieldValue:obj]; result[key] = converted; }]; return result; } - (FSTObjectValue *)decodedFields:(NSDictionary *)fields { __block FSTObjectValue *result = [FSTObjectValue objectValue]; [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj, BOOL *_Nonnull stop) { FieldPath path{util::MakeString(key)}; FSTFieldValue *value = [self decodedFieldValue:obj]; result = [result objectBySettingValue:value forPath:path]; }]; return result; } #pragma mark - FSTObjectValue <=> Document proto - (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue key:(const DocumentKey &)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: HARD_FAIL("Unknown document type: %s", response); } } - (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response { HARD_ASSERT(!!response.found, "Tried to deserialize a found document from a deleted document."); const DocumentKey key = [self decodedDocumentKey:response.found.name]; FSTObjectValue *value = [self decodedFields:response.found.fields]; SnapshotVersion version = [self decodedVersion:response.found.updateTime]; HARD_ASSERT(version != SnapshotVersion::None(), "Got a document response with no snapshot version"); return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; } - (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { HARD_ASSERT(!!response.missing, "Tried to deserialize a deleted document from a found document."); const DocumentKey key = [self decodedDocumentKey:response.missing]; SnapshotVersion version = [self decodedVersion:response.readTime]; HARD_ASSERT(version != SnapshotVersion::None(), "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 { HARD_FAIL("Unknown mutation type %s", NSStringFromClass(mutationClass)); } if (!mutation.precondition.IsNone()) { proto.currentDocument = [self encodedPrecondition:mutation.precondition]; } return proto; } - (FSTMutation *)decodedMutation:(GCFSWrite *)mutation { Precondition precondition = [mutation hasCurrentDocument] ? [self decodedPrecondition:mutation.currentDocument] : Precondition::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: { HARD_ASSERT(precondition == Precondition::Exists(true), "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. HARD_FAIL("Unknown mutation operation: %s", mutation.operationOneOfCase); } } - (GCFSPrecondition *)encodedPrecondition:(const Precondition &)precondition { HARD_ASSERT(!precondition.IsNone(), "Can't serialize an empty precondition"); GCFSPrecondition *message = [GCFSPrecondition message]; if (precondition.type() == Precondition::Type::UpdateTime) { message.updateTime = [self encodedVersion:precondition.update_time()]; } else if (precondition.type() == Precondition::Type::Exists) { message.exists = precondition == Precondition::Exists(true); } else { HARD_FAIL("Unknown precondition: %s", precondition.description()); } return message; } - (Precondition)decodedPrecondition:(GCFSPrecondition *)precondition { switch (precondition.conditionTypeOneOfCase) { case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase: return Precondition::None(); case GCFSPrecondition_ConditionType_OneOfCase_Exists: return Precondition::Exists(precondition.exists); case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime: return Precondition::UpdateTime([self decodedVersion:precondition.updateTime]); default: HARD_FAIL("Unrecognized Precondition one-of case %s", precondition); } } - (GCFSDocumentMask *)encodedFieldMask:(const FieldMask &)fieldMask { GCFSDocumentMask *mask = [GCFSDocumentMask message]; for (const FieldPath &field : fieldMask) { [mask.fieldPathsArray addObject:util::WrapNSString(field.CanonicalString())]; } return mask; } - (FieldMask)decodedFieldMask:(GCFSDocumentMask *)fieldMask { std::vector fields; fields.reserve(fieldMask.fieldPathsArray_Count); for (NSString *path in fieldMask.fieldPathsArray) { fields.push_back(FieldPath::FromServerFormat(util::MakeStringView(path))); } return FieldMask(std::move(fields)); } - (NSMutableArray *)encodedFieldTransforms: (const std::vector &)fieldTransforms { NSMutableArray *protos = [NSMutableArray array]; for (const FieldTransform &fieldTransform : fieldTransforms) { GCFSDocumentTransform_FieldTransform *proto = [self encodedFieldTransform:fieldTransform]; [protos addObject:proto]; } return protos; } - (GCFSDocumentTransform_FieldTransform *)encodedFieldTransform: (const FieldTransform &)fieldTransform { GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message]; proto.fieldPath = util::WrapNSString(fieldTransform.path().CanonicalString()); if (fieldTransform.transformation().type() == TransformOperation::Type::ServerTimestamp) { proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime; } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayUnion) { proto.appendMissingElements = [self encodedArrayTransformElements:ArrayTransform::Elements(fieldTransform.transformation())]; } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayRemove) { proto.removeAllFromArray_p = [self encodedArrayTransformElements:ArrayTransform::Elements(fieldTransform.transformation())]; } else { HARD_FAIL("Unknown transform: %s type", fieldTransform.transformation().type()); } return proto; } - (GCFSArrayValue *)encodedArrayTransformElements:(const std::vector &)elements { GCFSArrayValue *proto = [GCFSArrayValue message]; NSMutableArray *protoContents = [proto valuesArray]; for (FSTFieldValue *element : elements) { GCFSValue *converted = [self encodedFieldValue:element]; [protoContents addObject:converted]; } return proto; } - (std::vector)decodedFieldTransforms: (NSArray *)protos { std::vector fieldTransforms; fieldTransforms.reserve(protos.count); for (GCFSDocumentTransform_FieldTransform *proto in protos) { switch (proto.transformTypeOneOfCase) { case GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_SetToServerValue: { HARD_ASSERT( proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime, "Unknown transform setToServerValue: %s", proto.setToServerValue); fieldTransforms.emplace_back( FieldPath::FromServerFormat(util::MakeStringView(proto.fieldPath)), absl::make_unique(ServerTimestampTransform::Get())); break; } case GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_AppendMissingElements: { std::vector elements = [self decodedArrayTransformElements:proto.appendMissingElements]; fieldTransforms.emplace_back( FieldPath::FromServerFormat(util::MakeStringView(proto.fieldPath)), absl::make_unique(TransformOperation::Type::ArrayUnion, std::move(elements))); break; } case GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_RemoveAllFromArray_p: { std::vector elements = [self decodedArrayTransformElements:proto.removeAllFromArray_p]; fieldTransforms.emplace_back( FieldPath::FromServerFormat(util::MakeStringView(proto.fieldPath)), absl::make_unique(TransformOperation::Type::ArrayRemove, std::move(elements))); break; } default: HARD_FAIL("Unknown transform: %s", proto); } } return fieldTransforms; } - (std::vector)decodedArrayTransformElements:(GCFSArrayValue *)proto { __block std::vector elements; [proto.valuesArray enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) { elements.push_back([self decodedFieldValue:value]); }]; return elements; } #pragma mark - FSTMutationResult <= GCFSWriteResult proto - (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { // NOTE: Deletes don't have an updateTime. absl::optional version; if (mutation.updateTime) { version = [self decodedVersion:mutation.updateTime]; } 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:std::move(version) transformResults:transformResults]; } #pragma mark - FSTQueryData => GCFSTarget proto - (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: (FSTQueryData *)queryData { NSString *value = [self encodedLabelForPurpose:queryData.purpose]; if (!value) { return nil; } NSMutableDictionary *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: HARD_FAIL("Unrecognized query purpose: %s", 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 *docs = result.documentsArray; [docs addObject:[self encodedQueryPath:query.path]]; return result; } - (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target { NSArray *documents = target.documentsArray; HARD_ASSERT(documents.count == 1, "DocumentsTarget contained other than 1 document %s", (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.size() == 0) { queryTarget.parent = [self encodedQueryPath:query.path]; } else { const ResourcePath &path = query.path; HARD_ASSERT(path.size() % 2 != 0, "Document queries with filters are not supported."); queryTarget.parent = [self encodedQueryPath:path.PopLast()]; GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; from.collectionId = util::WrapNSString(path.last_segment()); [queryTarget.structuredQuery.fromArray addObject:from]; } // Encode the filters. GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters]; if (where) { queryTarget.structuredQuery.where = where; } NSArray *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 { ResourcePath path = [self decodedQueryPath:target.parent]; GCFSStructuredQuery *query = target.structuredQuery; NSUInteger fromCount = query.fromArray_Count; if (fromCount > 0) { HARD_ASSERT(fromCount == 1, "StructuredQuery.from with more than one collection is not supported."); GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0]; path = path.Append(util::MakeString(from.collectionId)); } NSArray *filterBy; if (query.hasWhere) { filterBy = [self decodedFilters:query.where]; } else { filterBy = @[]; } NSArray *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 *)filters { if (filters.count == 0) { return nil; } NSMutableArray *protos = [NSMutableArray array]; for (FSTFilter *filter in filters) { if ([filter isKindOfClass:[FSTRelationFilter class]]) { [protos addObject:[self encodedRelationFilter:(FSTRelationFilter *)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 *)decodedFilters:(GCFSStructuredQuery_Filter *)proto { NSMutableArray *result = [NSMutableArray array]; NSArray *filters; if (proto.filterTypeOneOfCase == GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) { HARD_ASSERT(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And, "Only AND-type composite filters are supported, got %s", 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: HARD_FAIL("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: HARD_FAIL("Unrecognized Filter.filterType %s", 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 { FieldPath fieldPath = FieldPath::FromServerFormat(util::MakeString(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:(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 { HARD_FAIL("Unrecognized filter: %s", filter); } return proto; } - (FSTFilter *)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto { FieldPath field = FieldPath::FromServerFormat(util::MakeString(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: HARD_FAIL("Unrecognized UnaryFilter.operator %s", proto.op); } } - (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(const FieldPath &)fieldPath { GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message]; ref.fieldPath = util::WrapNSString(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; case FSTRelationFilterOperatorArrayContains: return GCFSStructuredQuery_FieldFilter_Operator_ArrayContains; default: HARD_FAIL("Unhandled FSTRelationFilterOperator: %s", 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; case GCFSStructuredQuery_FieldFilter_Operator_ArrayContains: return FSTRelationFilterOperatorArrayContains; default: HARD_FAIL("Unhandled FieldFilter.operator: %s", filterOperator); } } #pragma mark Property Orders - (NSArray *)encodedSortOrders:(NSArray *)orders { NSMutableArray *protos = [NSMutableArray array]; for (FSTSortOrder *order in orders) { [protos addObject:[self encodedSortOrder:order]]; } return protos; } - (NSArray *)decodedSortOrders:(NSArray *)protos { NSMutableArray *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 { FieldPath fieldPath = FieldPath::FromServerFormat(util::MakeString(proto.field.fieldPath)); BOOL ascending; switch (proto.direction) { case GCFSStructuredQuery_Direction_Ascending: ascending = YES; break; case GCFSStructuredQuery_Direction_Descending: ascending = NO; break; default: HARD_FAIL("Unrecognized GCFSStructuredQuery_Direction %s", 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 *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: HARD_FAIL("Unknown WatchChange.changeType %s", watchChange.responseTypeOneOfCase); } } - (SnapshotVersion)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 SnapshotVersion::None(); } if (watchChange.targetChange.targetIdsArray.count != 0) { return SnapshotVersion::None(); } return [self decodedVersion:watchChange.targetChange.readTime]; } - (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change { FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType]; NSMutableArray *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: HARD_FAIL("Unexpected TargetChange.state: %s", state); } } - (NSArray *)decodedIntegerArray:(GPBInt32Array *)values { NSMutableArray *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]; const DocumentKey key = [self decodedDocumentKey:change.document.name]; SnapshotVersion version = [self decodedVersion:change.document.updateTime]; HARD_ASSERT(version != SnapshotVersion::None(), "Got a document change with no snapshot version"); FSTMaybeDocument *document = [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; NSArray *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray]; NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds removedTargetIDs:removedTargetIds documentKey:document.key document:document]; } - (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { const DocumentKey key = [self decodedDocumentKey:change.document]; // Note that version might be unset in which case we use SnapshotVersion::None() SnapshotVersion version = [self decodedVersion:change.readTime]; FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] removedTargetIDs:removedTargetIds documentKey:document.key document:document]; } - (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change { const DocumentKey key = [self decodedDocumentKey:change.document]; NSArray *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