From 729b8d176c75ecc0cbbd137cc6811116a64e310a Mon Sep 17 00:00:00 2001 From: Gil Date: Wed, 31 Jan 2018 11:23:55 -0800 Subject: 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 where needed * Avoid using C++ keywords as variables * Remove #if __cplusplus guards --- Firestore/Source/API/FIRDocumentChange.m | 129 ----- Firestore/Source/API/FIRDocumentChange.mm | 129 +++++ Firestore/Source/API/FIRDocumentReference.m | 311 ------------ Firestore/Source/API/FIRDocumentReference.mm | 311 ++++++++++++ Firestore/Source/API/FIRDocumentSnapshot.m | 252 ---------- Firestore/Source/API/FIRDocumentSnapshot.mm | 252 ++++++++++ Firestore/Source/API/FIRFieldPath.m | 101 ---- Firestore/Source/API/FIRFieldPath.mm | 101 ++++ Firestore/Source/API/FIRFieldValue.m | 96 ---- Firestore/Source/API/FIRFieldValue.mm | 96 ++++ Firestore/Source/API/FIRFirestore.m | 317 ------------ Firestore/Source/API/FIRFirestore.mm | 317 ++++++++++++ Firestore/Source/API/FIRFirestoreSettings.m | 92 ---- Firestore/Source/API/FIRFirestoreSettings.mm | 92 ++++ Firestore/Source/API/FIRFirestoreVersion.m | 29 -- Firestore/Source/API/FIRFirestoreVersion.mm | 29 ++ Firestore/Source/API/FIRListenerRegistration.m | 59 --- Firestore/Source/API/FIRListenerRegistration.mm | 59 +++ Firestore/Source/API/FIRQuery.m | 633 ------------------------ Firestore/Source/API/FIRQuery.mm | 633 ++++++++++++++++++++++++ Firestore/Source/API/FIRQuerySnapshot.m | 151 ------ Firestore/Source/API/FIRQuerySnapshot.mm | 151 ++++++ Firestore/Source/API/FIRSetOptions.m | 63 --- Firestore/Source/API/FIRSetOptions.mm | 63 +++ Firestore/Source/API/FIRSnapshotMetadata.m | 70 --- Firestore/Source/API/FIRSnapshotMetadata.mm | 70 +++ Firestore/Source/API/FIRSnapshotOptions.m | 72 --- Firestore/Source/API/FIRSnapshotOptions.mm | 72 +++ Firestore/Source/API/FIRTransaction.m | 148 ------ Firestore/Source/API/FIRTransaction.mm | 148 ++++++ Firestore/Source/API/FIRWriteBatch.m | 121 ----- Firestore/Source/API/FIRWriteBatch.mm | 121 +++++ Firestore/Source/API/FSTUserDataConverter.m | 598 ---------------------- Firestore/Source/API/FSTUserDataConverter.mm | 598 ++++++++++++++++++++++ 34 files changed, 3242 insertions(+), 3242 deletions(-) delete mode 100644 Firestore/Source/API/FIRDocumentChange.m create mode 100644 Firestore/Source/API/FIRDocumentChange.mm delete mode 100644 Firestore/Source/API/FIRDocumentReference.m create mode 100644 Firestore/Source/API/FIRDocumentReference.mm delete mode 100644 Firestore/Source/API/FIRDocumentSnapshot.m create mode 100644 Firestore/Source/API/FIRDocumentSnapshot.mm delete mode 100644 Firestore/Source/API/FIRFieldPath.m create mode 100644 Firestore/Source/API/FIRFieldPath.mm delete mode 100644 Firestore/Source/API/FIRFieldValue.m create mode 100644 Firestore/Source/API/FIRFieldValue.mm delete mode 100644 Firestore/Source/API/FIRFirestore.m create mode 100644 Firestore/Source/API/FIRFirestore.mm delete mode 100644 Firestore/Source/API/FIRFirestoreSettings.m create mode 100644 Firestore/Source/API/FIRFirestoreSettings.mm delete mode 100644 Firestore/Source/API/FIRFirestoreVersion.m create mode 100644 Firestore/Source/API/FIRFirestoreVersion.mm delete mode 100644 Firestore/Source/API/FIRListenerRegistration.m create mode 100644 Firestore/Source/API/FIRListenerRegistration.mm delete mode 100644 Firestore/Source/API/FIRQuery.m create mode 100644 Firestore/Source/API/FIRQuery.mm delete mode 100644 Firestore/Source/API/FIRQuerySnapshot.m create mode 100644 Firestore/Source/API/FIRQuerySnapshot.mm delete mode 100644 Firestore/Source/API/FIRSetOptions.m create mode 100644 Firestore/Source/API/FIRSetOptions.mm delete mode 100644 Firestore/Source/API/FIRSnapshotMetadata.m create mode 100644 Firestore/Source/API/FIRSnapshotMetadata.mm delete mode 100644 Firestore/Source/API/FIRSnapshotOptions.m create mode 100644 Firestore/Source/API/FIRSnapshotOptions.mm delete mode 100644 Firestore/Source/API/FIRTransaction.m create mode 100644 Firestore/Source/API/FIRTransaction.mm delete mode 100644 Firestore/Source/API/FIRWriteBatch.m create mode 100644 Firestore/Source/API/FIRWriteBatch.mm delete mode 100644 Firestore/Source/API/FSTUserDataConverter.m create mode 100644 Firestore/Source/API/FSTUserDataConverter.mm (limited to 'Firestore/Source/API') diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m deleted file mode 100644 index d1d9999..0000000 --- a/Firestore/Source/API/FIRDocumentChange.m +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 "FIRDocumentChange.h" - -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRDocumentChange () - -- (instancetype)initWithType:(FIRDocumentChangeType)type - document:(FIRDocumentSnapshot *)document - oldIndex:(NSUInteger)oldIndex - newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRDocumentChange (Internal) - -+ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { - if (change.type == FSTDocumentViewChangeTypeAdded) { - return FIRDocumentChangeTypeAdded; - } else if (change.type == FSTDocumentViewChangeTypeModified || - change.type == FSTDocumentViewChangeTypeMetadata) { - return FIRDocumentChangeTypeModified; - } else if (change.type == FSTDocumentViewChangeTypeRemoved) { - return FIRDocumentChangeTypeRemoved; - } else { - FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type); - } -} - -+ (NSArray *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot - firestore:(FIRFirestore *)firestore { - if (snapshot.oldDocuments.isEmpty) { - // Special case the first snapshot because index calculation is easy and fast - FSTDocument *_Nullable lastDocument = nil; - NSUInteger index = 0; - NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRQueryDocumentSnapshot *document = - [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; - FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, - @"Invalid event type for first snapshot"); - FSTAssert(!lastDocument || - snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending, - @"Got added events in wrong order"); - [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded - document:document - oldIndex:NSNotFound - newIndex:index++]]; - } - return changes; - } else { - // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index - // of a document. - FSTDocumentSet *indexTracker = snapshot.oldDocuments; - NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRQueryDocumentSnapshot *document = - [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; - - NSUInteger oldIndex = NSNotFound; - NSUInteger newIndex = NSNotFound; - if (change.type != FSTDocumentViewChangeTypeAdded) { - oldIndex = [indexTracker indexOfKey:change.document.key]; - FSTAssert(oldIndex != NSNotFound, @"Index for document not found"); - indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; - } - if (change.type != FSTDocumentViewChangeTypeRemoved) { - indexTracker = [indexTracker documentSetByAddingDocument:change.document]; - newIndex = [indexTracker indexOfKey:change.document.key]; - } - [FIRDocumentChange documentChangeTypeForChange:change]; - FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; - [changes addObject:[[FIRDocumentChange alloc] initWithType:type - document:document - oldIndex:oldIndex - newIndex:newIndex]]; - } - return changes; - } -} - -@end - -@implementation FIRDocumentChange - -- (instancetype)initWithType:(FIRDocumentChangeType)type - document:(FIRQueryDocumentSnapshot *)document - oldIndex:(NSUInteger)oldIndex - newIndex:(NSUInteger)newIndex { - if (self = [super init]) { - _type = type; - _document = document; - _oldIndex = oldIndex; - _newIndex = newIndex; - } - return self; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentChange.mm b/Firestore/Source/API/FIRDocumentChange.mm new file mode 100644 index 0000000..d1d9999 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentChange.mm @@ -0,0 +1,129 @@ +/* + * 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 "FIRDocumentChange.h" + +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentChange () + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRDocumentChange (Internal) + ++ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { + if (change.type == FSTDocumentViewChangeTypeAdded) { + return FIRDocumentChangeTypeAdded; + } else if (change.type == FSTDocumentViewChangeTypeModified || + change.type == FSTDocumentViewChangeTypeMetadata) { + return FIRDocumentChangeTypeModified; + } else if (change.type == FSTDocumentViewChangeTypeRemoved) { + return FIRDocumentChangeTypeRemoved; + } else { + FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type); + } +} + ++ (NSArray *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore { + if (snapshot.oldDocuments.isEmpty) { + // Special case the first snapshot because index calculation is easy and fast + FSTDocument *_Nullable lastDocument = nil; + NSUInteger index = 0; + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; + FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, + @"Invalid event type for first snapshot"); + FSTAssert(!lastDocument || + snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending, + @"Got added events in wrong order"); + [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded + document:document + oldIndex:NSNotFound + newIndex:index++]]; + } + return changes; + } else { + // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index + // of a document. + FSTDocumentSet *indexTracker = snapshot.oldDocuments; + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; + + NSUInteger oldIndex = NSNotFound; + NSUInteger newIndex = NSNotFound; + if (change.type != FSTDocumentViewChangeTypeAdded) { + oldIndex = [indexTracker indexOfKey:change.document.key]; + FSTAssert(oldIndex != NSNotFound, @"Index for document not found"); + indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; + } + if (change.type != FSTDocumentViewChangeTypeRemoved) { + indexTracker = [indexTracker documentSetByAddingDocument:change.document]; + newIndex = [indexTracker indexOfKey:change.document.key]; + } + [FIRDocumentChange documentChangeTypeForChange:change]; + FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; + [changes addObject:[[FIRDocumentChange alloc] initWithType:type + document:document + oldIndex:oldIndex + newIndex:newIndex]]; + } + return changes; + } +} + +@end + +@implementation FIRDocumentChange + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRQueryDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex { + if (self = [super init]) { + _type = type; + _document = document; + _oldIndex = oldIndex; + _newIndex = newIndex; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m deleted file mode 100644 index 05253f7..0000000 --- a/Firestore/Source/API/FIRDocumentReference.m +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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 "FIRDocumentReference.h" - -#import - -#import "FIRFirestoreErrors.h" -#import "FIRSnapshotMetadata.h" -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FIRDocumentListenOptions - -@interface FIRDocumentListenOptions () - -- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges - NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; - -@end - -@implementation FIRDocumentListenOptions - -+ (instancetype)options { - return [[FIRDocumentListenOptions alloc] init]; -} - -- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { - if (self = [super init]) { - _includeMetadataChanges = includeMetadataChanges; - } - return self; -} - -- (instancetype)init { - return [self initWithIncludeMetadataChanges:NO]; -} - -- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges { - return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges]; -} - -@end - -#pragma mark - FIRDocumentReference - -@interface FIRDocumentReference () -- (instancetype)initWithKey:(FSTDocumentKey *)key - firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; -@property(nonatomic, strong, readonly) FSTDocumentKey *key; -@end - -@implementation FIRDocumentReference (Internal) - -+ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { - if (path.length % 2 != 0) { - FSTThrowInvalidArgument( - @"Invalid document reference. Document references must have an even " - "number of segments, but %@ has %d", - path.canonicalString, path.length); - } - return - [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:path] firestore:firestore]; -} - -+ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { - return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore]; -} - -@end - -@implementation FIRDocumentReference - -- (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { - if (self = [super init]) { - _key = key; - _firestore = firestore; - } - return self; -} - -#pragma mark - NSObject Methods - -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToReference:other]; -} - -- (BOOL)isEqualToReference:(nullable FIRDocumentReference *)reference { - if (self == reference) return YES; - if (reference == nil) return NO; - return [self.firestore isEqual:reference.firestore] && [self.key isEqualToKey:reference.key]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.key hash]; - return hash; -} - -#pragma mark - Public Methods - -- (NSString *)documentID { - return [self.key.path lastSegment]; -} - -- (FIRCollectionReference *)parent { - FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment]; - return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore]; -} - -- (NSString *)path { - return [self.key.path canonicalString]; -} - -- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { - if (!collectionPath) { - FSTThrowInvalidArgument(@"Collection path cannot be nil."); - } - FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; - FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath]; - return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; -} - -- (void)setData:(NSDictionary *)documentData { - return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; -} - -- (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options { - return [self setData:documentData options:options completion:nil]; -} - -- (void)setData:(NSDictionary *)documentData - completion:(nullable void (^)(NSError *_Nullable error))completion { - return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; -} - -- (void)setData:(NSDictionary *)documentData - options:(FIRSetOptions *)options - completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedSetData *parsed = options.isMerge - ? [self.firestore.dataConverter parsedMergeData:documentData] - : [self.firestore.dataConverter parsedSetData:documentData]; - return [self.firestore.client - writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] - completion:completion]; -} - -- (void)updateData:(NSDictionary *)fields { - return [self updateData:fields completion:nil]; -} - -- (void)updateData:(NSDictionary *)fields - completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - return [self.firestore.client - writeMutations:[parsed mutationsWithKey:self.key - precondition:[FSTPrecondition preconditionWithExists:YES]] - completion:completion]; -} - -- (void)deleteDocument { - return [self deleteDocumentWithCompletion:nil]; -} - -- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - FSTDeleteMutation *mutation = - [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; - return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; -} - -- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { - FSTListenOptions *listenOptions = - [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; - - dispatch_semaphore_t registered = dispatch_semaphore_create(0); - __block id listenerRegistration; - FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { - if (error) { - completion(nil, error); - return; - } - - // Remove query first before passing event to user to avoid user actions affecting the - // now stale query. - dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); - [listenerRegistration remove]; - - if (!snapshot.exists && snapshot.metadata.fromCache) { - // TODO(dimond): Reconsider how to raise missing documents when offline. - // If we're online and the document doesn't exist then we call the completion with - // a document with document.exists set to false. If we're offline however, we call the - // completion handler with an error. Two options: - // 1) Cache the negative response from the server so we can deliver that even when you're - // offline. - // 2) Actually call the completion handler with an error if the document doesn't exist when - // you are offline. - // TODO(dimond): Use proper error domain - completion(nil, - [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:@{ - NSLocalizedDescriptionKey : - @"Failed to get document because the client is offline.", - }]); - } else { - completion(snapshot, nil); - } - }; - - listenerRegistration = - [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; - dispatch_semaphore_signal(registered); -} - -- (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { - return [self addSnapshotListenerWithOptions:nil listener:listener]; -} - -- (id)addSnapshotListenerWithOptions: - (nullable FIRDocumentListenOptions *)options - listener:(FIRDocumentSnapshotBlock)listener { - return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] - listener:listener]; -} - -- (id) -addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions - listener:(FIRDocumentSnapshotBlock)listener { - FIRFirestore *firestore = self.firestore; - FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; - FSTDocumentKey *key = self.key; - - FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { - if (error) { - listener(nil, error); - return; - } - - FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); - FSTDocument *document = [snapshot.documents documentForKey:key]; - - FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:key - document:document - fromCache:snapshot.fromCache]; - listener(result, nil); - }; - - FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue - snapshotHandler:snapshotHandler]; - - FSTQueryListener *internalListener = - [firestore.client listenToQuery:query - options:internalOptions - viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; - return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client - asyncListener:asyncListener - internalListener:internalListener]; -} - -/** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { - return - [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges - includeDocumentMetadataChanges:options.includeMetadataChanges - waitForSyncWhenOnline:NO]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm new file mode 100644 index 0000000..05253f7 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -0,0 +1,311 @@ +/* + * 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 "FIRDocumentReference.h" + +#import + +#import "FIRFirestoreErrors.h" +#import "FIRSnapshotMetadata.h" +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRDocumentListenOptions + +@interface FIRDocumentListenOptions () + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; + +@end + +@implementation FIRDocumentListenOptions + ++ (instancetype)options { + return [[FIRDocumentListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { + if (self = [super init]) { + _includeMetadataChanges = includeMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeMetadataChanges:NO]; +} + +- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges { + return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges]; +} + +@end + +#pragma mark - FIRDocumentReference + +@interface FIRDocumentReference () +- (instancetype)initWithKey:(FSTDocumentKey *)key + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +@implementation FIRDocumentReference (Internal) + ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + if (path.length % 2 != 0) { + FSTThrowInvalidArgument( + @"Invalid document reference. Document references must have an even " + "number of segments, but %@ has %d", + path.canonicalString, path.length); + } + return + [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:path] firestore:firestore]; +} + ++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { + return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore]; +} + +@end + +@implementation FIRDocumentReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _key = key; + _firestore = firestore; + } + return self; +} + +#pragma mark - NSObject Methods + +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToReference:other]; +} + +- (BOOL)isEqualToReference:(nullable FIRDocumentReference *)reference { + if (self == reference) return YES; + if (reference == nil) return NO; + return [self.firestore isEqual:reference.firestore] && [self.key isEqualToKey:reference.key]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.key hash]; + return hash; +} + +#pragma mark - Public Methods + +- (NSString *)documentID { + return [self.key.path lastSegment]; +} + +- (FIRCollectionReference *)parent { + FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment]; + return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore]; +} + +- (NSString *)path { + return [self.key.path canonicalString]; +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; + FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath]; + return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; +} + +- (void)setData:(NSDictionary *)documentData { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; +} + +- (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options { + return [self setData:documentData options:options completion:nil]; +} + +- (void)setData:(NSDictionary *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; +} + +- (void)setData:(NSDictionary *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = options.isMerge + ? [self.firestore.dataConverter parsedMergeData:documentData] + : [self.firestore.dataConverter parsedSetData:documentData]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] + completion:completion]; +} + +- (void)updateData:(NSDictionary *)fields { + return [self updateData:fields completion:nil]; +} + +- (void)updateData:(NSDictionary *)fields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key + precondition:[FSTPrecondition preconditionWithExists:YES]] + completion:completion]; +} + +- (void)deleteDocument { + return [self deleteDocumentWithCompletion:nil]; +} + +- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + FSTDeleteMutation *mutation = + [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; + return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; +} + +- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + FSTListenOptions *listenOptions = + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id listenerRegistration; + FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + if (!snapshot.exists && snapshot.metadata.fromCache) { + // TODO(dimond): Reconsider how to raise missing documents when offline. + // If we're online and the document doesn't exist then we call the completion with + // a document with document.exists set to false. If we're offline however, we call the + // completion handler with an error. Two options: + // 1) Cache the negative response from the server so we can deliver that even when you're + // offline. + // 2) Actually call the completion handler with an error if the document doesn't exist when + // you are offline. + // TODO(dimond): Use proper error domain + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get document because the client is offline.", + }]); + } else { + completion(snapshot, nil); + } + }; + + listenerRegistration = + [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRDocumentSnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; + FSTDocumentKey *key = self.key; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); + FSTDocument *document = [snapshot.documents documentForKey:key]; + + FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:key + document:document + fromCache:snapshot.fromCache]; + listener(result, nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { + return + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges + includeDocumentMetadataChanges:options.includeMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m deleted file mode 100644 index 10709e8..0000000 --- a/Firestore/Source/API/FIRDocumentSnapshot.m +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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 "FIRDocumentSnapshot.h" - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.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/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRDocumentSnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey; -@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; -@property(nonatomic, assign, readonly) BOOL fromCache; - -@end - -@implementation FIRDocumentSnapshot (Internal) - -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache { - return [[[self class] alloc] initWithFirestore:firestore - documentKey:documentKey - document:document - fromCache:fromCache]; -} - -@end - -@implementation FIRDocumentSnapshot { - FIRSnapshotMetadata *_cachedMetadata; -} - -@dynamic metadata; - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache { - if (self = [super init]) { - _firestore = firestore; - _internalKey = documentKey; - _internalDocument = document; - _fromCache = fromCache; - } - return self; -} - -// NSObject Methods -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. - if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; - - return [self isEqualToSnapshot:other]; -} - -- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { - if (self == snapshot) return YES; - if (snapshot == nil) return NO; - - return [self.firestore isEqual:snapshot.firestore] && - [self.internalKey isEqual:snapshot.internalKey] && - (self.internalDocument == snapshot.internalDocument || - [self.internalDocument isEqual:snapshot.internalDocument]) && - self.fromCache == snapshot.fromCache; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.internalKey hash]; - hash = hash * 31u + [self.internalDocument hash]; - hash = hash * 31u + (self.fromCache ? 1 : 0); - return hash; -} - -@dynamic exists; - -- (BOOL)exists { - return _internalDocument != nil; -} - -- (FIRDocumentReference *)reference { - return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; -} - -- (NSString *)documentID { - return [self.internalKey.path lastSegment]; -} - -- (FIRSnapshotMetadata *)metadata { - if (!_cachedMetadata) { - _cachedMetadata = [FIRSnapshotMetadata - snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations - fromCache:self.fromCache]; - } - return _cachedMetadata; -} - -- (nullable NSDictionary *)data { - return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; -} - -- (nullable NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { - return self.internalDocument == nil - ? nil - : [self convertedObject:[self.internalDocument data] - options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; -} - -- (nullable id)valueForField:(id)field { - return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; -} - -- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { - FIRFieldPath *fieldPath; - - if ([field isKindOfClass:[NSString class]]) { - fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; - } else if ([field isKindOfClass:[FIRFieldPath class]]) { - fieldPath = field; - } else { - FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); - } - - FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; - return fieldValue == nil - ? nil - : [self convertedValue:fieldValue - options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; -} - -- (nullable id)objectForKeyedSubscript:(id)key { - return [self valueForField:key]; -} - -- (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { - if ([value isKindOfClass:[FSTObjectValue class]]) { - return [self convertedObject:(FSTObjectValue *)value options:options]; - } else if ([value isKindOfClass:[FSTArrayValue class]]) { - return [self convertedArray:(FSTArrayValue *)value options:options]; - } else if ([value isKindOfClass:[FSTReferenceValue class]]) { - FSTReferenceValue *ref = (FSTReferenceValue *)value; - FSTDatabaseID *refDatabase = ref.databaseID; - FSTDatabaseID *database = self.firestore.databaseID; - if (![refDatabase isEqualToDatabaseId:database]) { - // TODO(b/32073923): Log this as a proper warning. - NSLog( - @"WARNING: Document %@ contains a document reference within a different database " - "(%@/%@) which is not supported. It will be treated as a reference within the " - "current database (%@/%@) instead.", - self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, - database.databaseID); - } - return [FIRDocumentReference referenceWithKey:[ref valueWithOptions:options] - firestore:self.firestore]; - } else { - return [value valueWithOptions:options]; - } -} - -- (NSDictionary *)convertedObject:(FSTObjectValue *)objectValue - options:(FSTFieldValueOptions *)options { - NSMutableDictionary *result = [NSMutableDictionary dictionary]; - [objectValue.internalValue - enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { - result[key] = [self convertedValue:value options:options]; - }]; - return result; -} - -- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue - options:(FSTFieldValueOptions *)options { - NSArray *internalValue = arrayValue.internalValue; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; - [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { - [result addObject:[self convertedValue:value options:options]]; - }]; - return result; -} - -@end - -@interface FIRQueryDocumentSnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(FSTDocument *)document - fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRQueryDocumentSnapshot - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(FSTDocumentKey *)documentKey - document:(FSTDocument *)document - fromCache:(BOOL)fromCache { - self = [super initWithFirestore:firestore - documentKey:documentKey - document:document - fromCache:fromCache]; - return self; -} - -- (NSDictionary *)data { - NSDictionary *data = [super data]; - FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); - return data; -} - -- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { - NSDictionary *data = [super dataWithOptions:options]; - FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); - return data; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.mm b/Firestore/Source/API/FIRDocumentSnapshot.mm new file mode 100644 index 0000000..10709e8 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentSnapshot.mm @@ -0,0 +1,252 @@ +/* + * 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 "FIRDocumentSnapshot.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.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/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey; +@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; +@property(nonatomic, assign, readonly) BOOL fromCache; + +@end + +@implementation FIRDocumentSnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + return [[[self class] alloc] initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; +} + +@end + +@implementation FIRDocumentSnapshot { + FIRSnapshotMetadata *_cachedMetadata; +} + +@dynamic metadata; + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + if (self = [super init]) { + _firestore = firestore; + _internalKey = documentKey; + _internalDocument = document; + _fromCache = fromCache; + } + return self; +} + +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. + if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + + return [self.firestore isEqual:snapshot.firestore] && + [self.internalKey isEqual:snapshot.internalKey] && + (self.internalDocument == snapshot.internalDocument || + [self.internalDocument isEqual:snapshot.internalDocument]) && + self.fromCache == snapshot.fromCache; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.internalKey hash]; + hash = hash * 31u + [self.internalDocument hash]; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + +@dynamic exists; + +- (BOOL)exists { + return _internalDocument != nil; +} + +- (FIRDocumentReference *)reference { + return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; +} + +- (NSString *)documentID { + return [self.internalKey.path lastSegment]; +} + +- (FIRSnapshotMetadata *)metadata { + if (!_cachedMetadata) { + _cachedMetadata = [FIRSnapshotMetadata + snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations + fromCache:self.fromCache]; + } + return _cachedMetadata; +} + +- (nullable NSDictionary *)data { + return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; +} + +- (nullable NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { + return self.internalDocument == nil + ? nil + : [self convertedObject:[self.internalDocument data] + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; +} + +- (nullable id)valueForField:(id)field { + return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; +} + +- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { + FIRFieldPath *fieldPath; + + if ([field isKindOfClass:[NSString class]]) { + fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; + } else if ([field isKindOfClass:[FIRFieldPath class]]) { + fieldPath = field; + } else { + FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); + } + + FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; + return fieldValue == nil + ? nil + : [self convertedValue:fieldValue + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; +} + +- (nullable id)objectForKeyedSubscript:(id)key { + return [self valueForField:key]; +} + +- (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { + if ([value isKindOfClass:[FSTObjectValue class]]) { + return [self convertedObject:(FSTObjectValue *)value options:options]; + } else if ([value isKindOfClass:[FSTArrayValue class]]) { + return [self convertedArray:(FSTArrayValue *)value options:options]; + } else if ([value isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)value; + FSTDatabaseID *refDatabase = ref.databaseID; + FSTDatabaseID *database = self.firestore.databaseID; + if (![refDatabase isEqualToDatabaseId:database]) { + // TODO(b/32073923): Log this as a proper warning. + NSLog( + @"WARNING: Document %@ contains a document reference within a different database " + "(%@/%@) which is not supported. It will be treated as a reference within the " + "current database (%@/%@) instead.", + self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, + database.databaseID); + } + return [FIRDocumentReference referenceWithKey:[ref valueWithOptions:options] + firestore:self.firestore]; + } else { + return [value valueWithOptions:options]; + } +} + +- (NSDictionary *)convertedObject:(FSTObjectValue *)objectValue + options:(FSTFieldValueOptions *)options { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [objectValue.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { + result[key] = [self convertedValue:value options:options]; + }]; + return result; +} + +- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue + options:(FSTFieldValueOptions *)options { + NSArray *internalValue = arrayValue.internalValue; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; + [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { + [result addObject:[self convertedValue:value options:options]]; + }]; + return result; +} + +@end + +@interface FIRQueryDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryDocumentSnapshot + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache { + self = [super initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; + return self; +} + +- (NSDictionary *)data { + NSDictionary *data = [super data]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { + NSDictionary *data = [super dataWithOptions:options]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m deleted file mode 100644 index f4e532f..0000000 --- a/Firestore/Source/API/FIRFieldPath.m +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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/API/FIRFieldPath+Internal.h" - -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRFieldPath - -- (instancetype)initWithFields:(NSArray *)fieldNames { - if (fieldNames.count == 0) { - FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty."); - } - - for (int i = 0; i < fieldNames.count; ++i) { - if (fieldNames[i].length == 0) { - FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i); - } - } - - return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]]; -} - -+ (instancetype)documentID { - return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath]; -} - -- (instancetype)initPrivate:(FSTFieldPath *)fieldPath { - if (self = [super init]) { - _internalValue = fieldPath; - } - return self; -} - -+ (instancetype)pathWithDotSeparatedString:(NSString *)path { - if ([[FIRFieldPath reservedCharactersRegex] - numberOfMatchesInString:path - options:0 - range:NSMakeRange(0, path.length)] > 0) { - FSTThrowInvalidArgument( - @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path); - } - @try { - return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]]; - } @catch (NSException *exception) { - FSTThrowInvalidArgument( - @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or " - @"contain '..'", - path); - } -} - -/** Matches any characters in a field path string that are reserved. */ -+ (NSRegularExpression *)reservedCharactersRegex { - static NSRegularExpression *regex = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil]; - }); - return regex; -} - -- (id)copyWithZone:(NSZone *__nullable)zone { - return [[[self class] alloc] initPrivate:self.internalValue]; -} - -- (BOOL)isEqual:(nullable id)object { - if (self == object) { - return YES; - } - - if (![object isKindOfClass:[FIRFieldPath class]]) { - return NO; - } - - return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue]; -} - -- (NSUInteger)hash { - return [self.internalValue hash]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.mm b/Firestore/Source/API/FIRFieldPath.mm new file mode 100644 index 0000000..f4e532f --- /dev/null +++ b/Firestore/Source/API/FIRFieldPath.mm @@ -0,0 +1,101 @@ +/* + * 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/API/FIRFieldPath+Internal.h" + +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRFieldPath + +- (instancetype)initWithFields:(NSArray *)fieldNames { + if (fieldNames.count == 0) { + FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty."); + } + + for (int i = 0; i < fieldNames.count; ++i) { + if (fieldNames[i].length == 0) { + FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i); + } + } + + return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]]; +} + ++ (instancetype)documentID { + return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath]; +} + +- (instancetype)initPrivate:(FSTFieldPath *)fieldPath { + if (self = [super init]) { + _internalValue = fieldPath; + } + return self; +} + ++ (instancetype)pathWithDotSeparatedString:(NSString *)path { + if ([[FIRFieldPath reservedCharactersRegex] + numberOfMatchesInString:path + options:0 + range:NSMakeRange(0, path.length)] > 0) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path); + } + @try { + return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]]; + } @catch (NSException *exception) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or " + @"contain '..'", + path); + } +} + +/** Matches any characters in a field path string that are reserved. */ ++ (NSRegularExpression *)reservedCharactersRegex { + static NSRegularExpression *regex = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil]; + }); + return regex; +} + +- (id)copyWithZone:(NSZone *__nullable)zone { + return [[[self class] alloc] initPrivate:self.internalValue]; +} + +- (BOOL)isEqual:(nullable id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRFieldPath class]]) { + return NO; + } + + return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.m b/Firestore/Source/API/FIRFieldValue.m deleted file mode 100644 index 7ae4fb0..0000000 --- a/Firestore/Source/API/FIRFieldValue.m +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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/API/FIRFieldValue+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRFieldValue () -- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; -@end - -#pragma mark - FSTDeleteFieldValue - -@interface FSTDeleteFieldValue () -/** Returns a single shared instance of the class. */ -+ (instancetype)deleteFieldValue; -@end - -@implementation FSTDeleteFieldValue - -- (instancetype)initPrivate { - self = [super initPrivate]; - return self; -} - -+ (instancetype)deleteFieldValue { - static FSTDeleteFieldValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate]; - }); - return sharedInstance; -} - -@end - -#pragma mark - FSTServerTimestampFieldValue - -@interface FSTServerTimestampFieldValue () -/** Returns a single shared instance of the class. */ -+ (instancetype)serverTimestampFieldValue; -@end - -@implementation FSTServerTimestampFieldValue - -- (instancetype)initPrivate { - self = [super initPrivate]; - return self; -} - -+ (instancetype)serverTimestampFieldValue { - static FSTServerTimestampFieldValue *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate]; - }); - return sharedInstance; -} - -@end - -#pragma mark - FIRFieldValue - -@implementation FIRFieldValue - -- (instancetype)initPrivate { - self = [super init]; - return self; -} - -+ (instancetype)fieldValueForDelete { - return [FSTDeleteFieldValue deleteFieldValue]; -} - -+ (instancetype)fieldValueForServerTimestamp { - return [FSTServerTimestampFieldValue serverTimestampFieldValue]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.mm b/Firestore/Source/API/FIRFieldValue.mm new file mode 100644 index 0000000..7ae4fb0 --- /dev/null +++ b/Firestore/Source/API/FIRFieldValue.mm @@ -0,0 +1,96 @@ +/* + * 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/API/FIRFieldValue+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldValue () +- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; +@end + +#pragma mark - FSTDeleteFieldValue + +@interface FSTDeleteFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)deleteFieldValue; +@end + +@implementation FSTDeleteFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)deleteFieldValue { + static FSTDeleteFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FSTServerTimestampFieldValue + +@interface FSTServerTimestampFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)serverTimestampFieldValue; +@end + +@implementation FSTServerTimestampFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)serverTimestampFieldValue { + static FSTServerTimestampFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FIRFieldValue + +@implementation FIRFieldValue + +- (instancetype)initPrivate { + self = [super init]; + return self; +} + ++ (instancetype)fieldValueForDelete { + return [FSTDeleteFieldValue deleteFieldValue]; +} + ++ (instancetype)fieldValueForServerTimestamp { + return [FSTServerTimestampFieldValue serverTimestampFieldValue]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m deleted file mode 100644 index 9df3711..0000000 --- a/Firestore/Source/API/FIRFirestore.m +++ /dev/null @@ -1,317 +0,0 @@ -/* - * 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 "FIRFirestore.h" - -#import -#import -#import - -#import "FIRFirestoreSettings.h" -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRTransaction+Internal.h" -#import "Firestore/Source/API/FIRWriteBatch+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" - -#import "Firestore/Source/Auth/FSTCredentialsProvider.h" -#import "Firestore/Source/Core/FSTDatabaseInfo.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" -#import "Firestore/Source/Util/FSTLogger.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; - -@interface FIRFirestore () - -@property(nonatomic, strong) FSTDatabaseID *databaseID; -@property(nonatomic, strong) NSString *persistenceKey; -@property(nonatomic, strong) id credentialsProvider; -@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; - -// Note that `client` is updated after initialization, but marking this readwrite would generate an -// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. -@property(nonatomic, strong, readonly) FSTFirestoreClient *client; -@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; - -@end - -@implementation FIRFirestore { - // All guarded by @synchronized(self) - FIRFirestoreSettings *_settings; - FSTFirestoreClient *_client; -} - -+ (NSMutableDictionary *)instances { - static dispatch_once_t token = 0; - static NSMutableDictionary *instances; - dispatch_once(&token, ^{ - instances = [NSMutableDictionary dictionary]; - }); - return instances; -} - -+ (instancetype)firestore { - FIRApp *app = [FIRApp defaultApp]; - if (!app) { - FSTThrowInvalidUsage(@"FIRAppNotConfiguredException", - @"Failed to get FirebaseApp instance. Please call FirebaseApp.configure() " - @"before using Firestore"); - } - return [self firestoreForApp:app database:kDefaultDatabaseID]; -} - -+ (instancetype)firestoreForApp:(FIRApp *)app { - return [self firestoreForApp:app database:kDefaultDatabaseID]; -} - -// TODO(b/62410906): make this public -+ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { - if (!app) { - FSTThrowInvalidArgument( - @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd " - "like to use the default FirebaseApp instance."); - } - if (!database) { - FSTThrowInvalidArgument( - @"database identifier may not be nil. Use '%@' if you want the default " - "database", - kDefaultDatabaseID); - } - NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database]; - - NSMutableDictionary *instances = self.instances; - @synchronized(instances) { - FIRFirestore *firestore = instances[key]; - if (!firestore) { - NSString *projectID = app.options.projectID; - FSTAssert(projectID, @"FirebaseOptions.projectID cannot be nil."); - - FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue - queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; - - id credentialsProvider; - credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; - - NSString *persistenceKey = app.name; - - firestore = [[FIRFirestore alloc] initWithProjectID:projectID - database:database - persistenceKey:persistenceKey - credentialsProvider:credentialsProvider - workerDispatchQueue:workerDispatchQueue - firebaseApp:app]; - instances[key] = firestore; - } - - return firestore; - } -} - -- (instancetype)initWithProjectID:(NSString *)projectID - database:(NSString *)database - persistenceKey:(NSString *)persistenceKey - credentialsProvider:(id)credentialsProvider - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue - firebaseApp:(FIRApp *)app { - if (self = [super init]) { - _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; - FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { - if ([input isKindOfClass:[FIRDocumentReference class]]) { - FIRDocumentReference *documentReference = (FIRDocumentReference *)input; - return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key - databaseID:documentReference.firestore.databaseID]; - } else { - return input; - } - }; - _dataConverter = - [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; - _persistenceKey = persistenceKey; - _credentialsProvider = credentialsProvider; - _workerDispatchQueue = workerDispatchQueue; - _app = app; - _settings = [[FIRFirestoreSettings alloc] init]; - } - return self; -} - -- (FIRFirestoreSettings *)settings { - @synchronized(self) { - // Disallow mutation of our internal settings - return [_settings copy]; - } -} - -- (void)setSettings:(FIRFirestoreSettings *)settings { - @synchronized(self) { - // As a special exception, don't throw if the same settings are passed repeatedly. This should - // make it more friendly to create a Firestore instance. - if (_client && ![_settings isEqual:settings]) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"Firestore instance has already been started and its settings can no " - "longer be changed. You can only set settings before calling any " - "other methods on a Firestore instance."); - } - _settings = [settings copy]; - } -} - -/** - * Ensures that the FirestoreClient is configured and returns it. - */ -- (FSTFirestoreClient *)client { - [self ensureClientConfigured]; - return _client; -} - -- (void)ensureClientConfigured { - @synchronized(self) { - if (!_client) { - // These values are validated elsewhere; this is just double-checking: - FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); - FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); - - FSTDatabaseInfo *databaseInfo = - [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID - persistenceKey:_persistenceKey - host:_settings.host - sslEnabled:_settings.sslEnabled]; - - FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; - - _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo - usePersistence:_settings.persistenceEnabled - credentialsProvider:_credentialsProvider - userDispatchQueue:userDispatchQueue - workerDispatchQueue:_workerDispatchQueue]; - } - } -} - -- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { - if (!collectionPath) { - FSTThrowInvalidArgument(@"Collection path cannot be nil."); - } - [self ensureClientConfigured]; - FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; - return [FIRCollectionReference referenceWithPath:path firestore:self]; -} - -- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { - if (!documentPath) { - FSTThrowInvalidArgument(@"Document path cannot be nil."); - } - [self ensureClientConfigured]; - FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; - return [FIRDocumentReference referenceWithPath:path firestore:self]; -} - -- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock - dispatchQueue:(dispatch_queue_t)queue - completion: - (void (^)(id _Nullable result, NSError *_Nullable error))completion { - // We wrap the function they provide in order to use internal implementation classes for - // FSTTransaction, and to run the user callback block on the proper queue. - if (!updateBlock) { - FSTThrowInvalidArgument(@"Transaction block cannot be nil."); - } else if (!completion) { - FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); - } - - FSTTransactionBlock wrappedUpdate = - ^(FSTTransaction *internalTransaction, - void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { - FIRTransaction *transaction = - [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; - dispatch_async(queue, ^{ - NSError *_Nullable error = nil; - id _Nullable result = updateBlock(transaction, &error); - if (error) { - // Force the result to be nil in the case of an error, in case the user set both. - result = nil; - } - internalCompletion(result, error); - }); - }; - [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; -} - -- (FIRWriteBatch *)batch { - [self ensureClientConfigured]; - - return [FIRWriteBatch writeBatchWithFirestore:self]; -} - -- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock - completion: - (void (^)(id _Nullable result, NSError *_Nullable error))completion { - static dispatch_queue_t transactionDispatchQueue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction", - DISPATCH_QUEUE_CONCURRENT); - }); - [self runTransactionWithBlock:updateBlock - dispatchQueue:transactionDispatchQueue - completion:completion]; -} - -- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - FSTFirestoreClient *client; - @synchronized(self) { - client = _client; - _client = nil; - } - - if (!client) { - // We should be dispatching the callback on the user dispatch queue but if the client is nil - // here that queue was never created. - completion(nil); - } else { - [client shutdownWithCompletion:completion]; - } -} - -+ (BOOL)isLoggingEnabled { - return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); -} - -+ (void)enableLogging:(BOOL)logging { - FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); -} - -- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - [self ensureClientConfigured]; - [self.client enableNetworkWithCompletion:completion]; -} - -- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { - [self ensureClientConfigured]; - [self.client disableNetworkWithCompletion:completion]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm new file mode 100644 index 0000000..10367bd --- /dev/null +++ b/Firestore/Source/API/FIRFirestore.mm @@ -0,0 +1,317 @@ +/* + * 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 "FIRFirestore.h" + +#import +#import +#import + +#import "FIRFirestoreSettings.h" +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRTransaction+Internal.h" +#import "Firestore/Source/API/FIRWriteBatch+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" + +#import "Firestore/Source/Auth/FSTCredentialsProvider.h" +#import "Firestore/Source/Core/FSTDatabaseInfo.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTDispatchQueue.h" +#import "Firestore/Source/Util/FSTLogger.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +extern "C" NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; + +@interface FIRFirestore () + +@property(nonatomic, strong) FSTDatabaseID *databaseID; +@property(nonatomic, strong) NSString *persistenceKey; +@property(nonatomic, strong) id credentialsProvider; +@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; + +// Note that `client` is updated after initialization, but marking this readwrite would generate an +// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +@implementation FIRFirestore { + // All guarded by @synchronized(self) + FIRFirestoreSettings *_settings; + FSTFirestoreClient *_client; +} + ++ (NSMutableDictionary *)instances { + static dispatch_once_t token = 0; + static NSMutableDictionary *instances; + dispatch_once(&token, ^{ + instances = [NSMutableDictionary dictionary]; + }); + return instances; +} + ++ (instancetype)firestore { + FIRApp *app = [FIRApp defaultApp]; + if (!app) { + FSTThrowInvalidUsage(@"FIRAppNotConfiguredException", + @"Failed to get FirebaseApp instance. Please call FirebaseApp.configure() " + @"before using Firestore"); + } + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + ++ (instancetype)firestoreForApp:(FIRApp *)app { + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + +// TODO(b/62410906): make this public ++ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { + if (!app) { + FSTThrowInvalidArgument( + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd " + "like to use the default FirebaseApp instance."); + } + if (!database) { + FSTThrowInvalidArgument( + @"database identifier may not be nil. Use '%@' if you want the default " + "database", + kDefaultDatabaseID); + } + NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database]; + + NSMutableDictionary *instances = self.instances; + @synchronized(instances) { + FIRFirestore *firestore = instances[key]; + if (!firestore) { + NSString *projectID = app.options.projectID; + FSTAssert(projectID, @"FirebaseOptions.projectID cannot be nil."); + + FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; + + id credentialsProvider; + credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; + + NSString *persistenceKey = app.name; + + firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:database + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + instances[key] = firestore; + } + + return firestore; + } +} + +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app { + if (self = [super init]) { + _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { + if ([input isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *documentReference = (FIRDocumentReference *)input; + return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key + databaseID:documentReference.firestore.databaseID]; + } else { + return input; + } + }; + _dataConverter = + [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; + _persistenceKey = persistenceKey; + _credentialsProvider = credentialsProvider; + _workerDispatchQueue = workerDispatchQueue; + _app = app; + _settings = [[FIRFirestoreSettings alloc] init]; + } + return self; +} + +- (FIRFirestoreSettings *)settings { + @synchronized(self) { + // Disallow mutation of our internal settings + return [_settings copy]; + } +} + +- (void)setSettings:(FIRFirestoreSettings *)settings { + @synchronized(self) { + // As a special exception, don't throw if the same settings are passed repeatedly. This should + // make it more friendly to create a Firestore instance. + if (_client && ![_settings isEqual:settings]) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"Firestore instance has already been started and its settings can no " + "longer be changed. You can only set settings before calling any " + "other methods on a Firestore instance."); + } + _settings = [settings copy]; + } +} + +/** + * Ensures that the FirestoreClient is configured and returns it. + */ +- (FSTFirestoreClient *)client { + [self ensureClientConfigured]; + return _client; +} + +- (void)ensureClientConfigured { + @synchronized(self) { + if (!_client) { + // These values are validated elsewhere; this is just double-checking: + FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); + FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); + + FSTDatabaseInfo *databaseInfo = + [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID + persistenceKey:_persistenceKey + host:_settings.host + sslEnabled:_settings.sslEnabled]; + + FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; + + _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo + usePersistence:_settings.persistenceEnabled + credentialsProvider:_credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:_workerDispatchQueue]; + } + } +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + [self ensureClientConfigured]; + FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; + return [FIRCollectionReference referenceWithPath:path firestore:self]; +} + +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { + if (!documentPath) { + FSTThrowInvalidArgument(@"Document path cannot be nil."); + } + [self ensureClientConfigured]; + FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; + return [FIRDocumentReference referenceWithPath:path firestore:self]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock + dispatchQueue:(dispatch_queue_t)queue + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + // We wrap the function they provide in order to use internal implementation classes for + // FSTTransaction, and to run the user callback block on the proper queue. + if (!updateBlock) { + FSTThrowInvalidArgument(@"Transaction block cannot be nil."); + } else if (!completion) { + FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); + } + + FSTTransactionBlock wrappedUpdate = + ^(FSTTransaction *internalTransaction, + void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { + FIRTransaction *transaction = + [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; + dispatch_async(queue, ^{ + NSError *_Nullable error = nil; + id _Nullable result = updateBlock(transaction, &error); + if (error) { + // Force the result to be nil in the case of an error, in case the user set both. + result = nil; + } + internalCompletion(result, error); + }); + }; + [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; +} + +- (FIRWriteBatch *)batch { + [self ensureClientConfigured]; + + return [FIRWriteBatch writeBatchWithFirestore:self]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + static dispatch_queue_t transactionDispatchQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction", + DISPATCH_QUEUE_CONCURRENT); + }); + [self runTransactionWithBlock:updateBlock + dispatchQueue:transactionDispatchQueue + completion:completion]; +} + +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + FSTFirestoreClient *client; + @synchronized(self) { + client = _client; + _client = nil; + } + + if (!client) { + // We should be dispatching the callback on the user dispatch queue but if the client is nil + // here that queue was never created. + completion(nil); + } else { + [client shutdownWithCompletion:completion]; + } +} + ++ (BOOL)isLoggingEnabled { + return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); +} + ++ (void)enableLogging:(BOOL)logging { + FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); +} + +- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + [self ensureClientConfigured]; + [self.client enableNetworkWithCompletion:completion]; +} + +- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { + [self ensureClientConfigured]; + [self.client disableNetworkWithCompletion:completion]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreSettings.m b/Firestore/Source/API/FIRFirestoreSettings.m deleted file mode 100644 index 9677ff6..0000000 --- a/Firestore/Source/API/FIRFirestoreSettings.m +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 "FIRFirestoreSettings.h" - -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const kDefaultHost = @"firestore.googleapis.com"; -static const BOOL kDefaultSSLEnabled = YES; -static const BOOL kDefaultPersistenceEnabled = YES; - -@implementation FIRFirestoreSettings - -- (instancetype)init { - if (self = [super init]) { - _host = kDefaultHost; - _sslEnabled = kDefaultSSLEnabled; - _dispatchQueue = dispatch_get_main_queue(); - _persistenceEnabled = kDefaultPersistenceEnabled; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) { - return NO; - } - - FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; - return [self.host isEqual:otherSettings.host] && - self.isSSLEnabled == otherSettings.isSSLEnabled && - self.dispatchQueue == otherSettings.dispatchQueue && - self.isPersistenceEnabled == otherSettings.isPersistenceEnabled; -} - -- (NSUInteger)hash { - NSUInteger result = [self.host hash]; - result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); - // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). - result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); - return result; -} - -- (id)copyWithZone:(nullable NSZone *)zone { - FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init]; - copy.host = _host; - copy.sslEnabled = _sslEnabled; - copy.dispatchQueue = _dispatchQueue; - copy.persistenceEnabled = _persistenceEnabled; - return copy; -} - -- (void)setHost:(NSString *)host { - if (!host) { - FSTThrowInvalidArgument( - @"host setting may not be nil. You should generally just use the default value " - "(which is %@)", - kDefaultHost); - } - _host = [host mutableCopy]; -} - -- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { - if (!dispatchQueue) { - FSTThrowInvalidArgument( - @"dispatch queue setting may not be nil. Create a new dispatch queue with " - "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " - "(which is the main queue, returned from dispatch_get_main_queue())"); - } - _dispatchQueue = dispatchQueue; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreSettings.mm b/Firestore/Source/API/FIRFirestoreSettings.mm new file mode 100644 index 0000000..9677ff6 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreSettings.mm @@ -0,0 +1,92 @@ +/* + * 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 "FIRFirestoreSettings.h" + +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDefaultHost = @"firestore.googleapis.com"; +static const BOOL kDefaultSSLEnabled = YES; +static const BOOL kDefaultPersistenceEnabled = YES; + +@implementation FIRFirestoreSettings + +- (instancetype)init { + if (self = [super init]) { + _host = kDefaultHost; + _sslEnabled = kDefaultSSLEnabled; + _dispatchQueue = dispatch_get_main_queue(); + _persistenceEnabled = kDefaultPersistenceEnabled; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) { + return NO; + } + + FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; + return [self.host isEqual:otherSettings.host] && + self.isSSLEnabled == otherSettings.isSSLEnabled && + self.dispatchQueue == otherSettings.dispatchQueue && + self.isPersistenceEnabled == otherSettings.isPersistenceEnabled; +} + +- (NSUInteger)hash { + NSUInteger result = [self.host hash]; + result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); + // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). + result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); + return result; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init]; + copy.host = _host; + copy.sslEnabled = _sslEnabled; + copy.dispatchQueue = _dispatchQueue; + copy.persistenceEnabled = _persistenceEnabled; + return copy; +} + +- (void)setHost:(NSString *)host { + if (!host) { + FSTThrowInvalidArgument( + @"host setting may not be nil. You should generally just use the default value " + "(which is %@)", + kDefaultHost); + } + _host = [host mutableCopy]; +} + +- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { + if (!dispatchQueue) { + FSTThrowInvalidArgument( + @"dispatch queue setting may not be nil. Create a new dispatch queue with " + "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " + "(which is the main queue, returned from dispatch_get_main_queue())"); + } + _dispatchQueue = dispatchQueue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreVersion.m b/Firestore/Source/API/FIRFirestoreVersion.m deleted file mode 100644 index 4f8bb28..0000000 --- a/Firestore/Source/API/FIRFirestoreVersion.m +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -#ifndef FIRFirestore_VERSION -#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation" -#endif - -// The following two macros supply the incantation so that the C -// preprocessor does not try to parse the version as a floating -// point number. See -// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/ -#define STR(x) STR_EXPAND(x) -#define STR_EXPAND(x) #x - -const unsigned char *const FirebaseFirestoreVersionString = - (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRFirestoreVersion.mm b/Firestore/Source/API/FIRFirestoreVersion.mm new file mode 100644 index 0000000..b1fe480 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreVersion.mm @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#ifndef FIRFirestore_VERSION +#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation" +#endif + +// The following two macros supply the incantation so that the C +// preprocessor does not try to parse the version as a floating +// point number. See +// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/ +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +extern "C" const unsigned char *const FirebaseFirestoreVersionString = + (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRListenerRegistration.m b/Firestore/Source/API/FIRListenerRegistration.m deleted file mode 100644 index 9f4ddd5..0000000 --- a/Firestore/Source/API/FIRListenerRegistration.m +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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/API/FIRListenerRegistration+Internal.h" - -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTListenerRegistration () - -/** The client that was used to register this listen. */ -@property(nonatomic, strong, readonly) FSTFirestoreClient *client; - -/** The async listener that is used to mute events synchronously. */ -@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener; - -/** The internal FSTQueryListener that can be used to unlisten the query. */ -@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener; - -@end - -@implementation FSTListenerRegistration - -- (instancetype)initWithClient:(FSTFirestoreClient *)client - asyncListener:(FSTAsyncQueryListener *)asyncListener - internalListener:(FSTQueryListener *)internalListener { - if (self = [super init]) { - _client = client; - _asyncListener = asyncListener; - _internalListener = internalListener; - } - return self; -} - -- (void)remove { - [self.asyncListener mute]; - [self.client removeListener:self.internalListener]; - _internalListener = nil; - _asyncListener = nil; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRListenerRegistration.mm b/Firestore/Source/API/FIRListenerRegistration.mm new file mode 100644 index 0000000..9f4ddd5 --- /dev/null +++ b/Firestore/Source/API/FIRListenerRegistration.mm @@ -0,0 +1,59 @@ +/* + * 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/API/FIRListenerRegistration+Internal.h" + +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTListenerRegistration () + +/** The client that was used to register this listen. */ +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; + +/** The async listener that is used to mute events synchronously. */ +@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener; + +/** The internal FSTQueryListener that can be used to unlisten the query. */ +@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener; + +@end + +@implementation FSTListenerRegistration + +- (instancetype)initWithClient:(FSTFirestoreClient *)client + asyncListener:(FSTAsyncQueryListener *)asyncListener + internalListener:(FSTQueryListener *)internalListener { + if (self = [super init]) { + _client = client; + _asyncListener = asyncListener; + _internalListener = internalListener; + } + return self; +} + +- (void)remove { + [self.asyncListener mute]; + [self.client removeListener:self.internalListener]; + _internalListener = nil; + _asyncListener = nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m deleted file mode 100644 index 1bbf91e..0000000 --- a/Firestore/Source/API/FIRQuery.m +++ /dev/null @@ -1,633 +0,0 @@ -/* - * 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 "FIRQuery.h" - -#import "FIRDocumentReference.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" -#import "Firestore/Source/API/FIRQuery_Init.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQueryListenOptions () - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRQueryListenOptions - -+ (instancetype)options { - return [[FIRQueryListenOptions alloc] init]; -} - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { - if (self = [super init]) { - _includeQueryMetadataChanges = includeQueryMetadataChanges; - _includeDocumentMetadataChanges = includeDocumentMetadataChanges; - } - return self; -} - -- (instancetype)init { - return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; -} - -- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { - return [[FIRQueryListenOptions alloc] - initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges - includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; -} - -- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { - return [[FIRQueryListenOptions alloc] - initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges - includeDocumentMetadataChanges:includeDocumentMetadataChanges]; -} - -@end - -@interface FIRQuery () -@property(nonatomic, strong, readonly) FSTQuery *query; -@end - -@implementation FIRQuery (Internal) -+ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { - return [[FIRQuery alloc] initWithQuery:query firestore:firestore]; -} -@end - -@implementation FIRQuery - -#pragma mark - Constructor Methods - -- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { - if (self = [super init]) { - _query = query; - _firestore = firestore; - } - return self; -} - -#pragma mark - NSObject Methods - -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToQuery:other]; -} - -- (BOOL)isEqualToQuery:(nullable FIRQuery *)query { - if (self == query) return YES; - if (query == nil) return NO; - - return [self.firestore isEqual:query.firestore] && [self.query isEqual:query.query]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.query hash]; - return hash; -} - -#pragma mark - Public Methods - -- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, - NSError *_Nullable error))completion { - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; - - dispatch_semaphore_t registered = dispatch_semaphore_create(0); - __block id listenerRegistration; - FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) { - if (error) { - completion(nil, error); - return; - } - - // Remove query first before passing event to user to avoid user actions affecting the - // now stale query. - dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); - [listenerRegistration remove]; - - completion(snapshot, nil); - }; - - listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; - dispatch_semaphore_signal(registered); -} - -- (id)addSnapshotListener:(FIRQuerySnapshotBlock)listener { - return [self addSnapshotListenerWithOptions:nil listener:listener]; -} - -- (id)addSnapshotListenerWithOptions: - (nullable FIRQueryListenOptions *)options - listener:(FIRQuerySnapshotBlock)listener { - return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] - listener:listener]; -} - -- (id) -addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions - listener:(FIRQuerySnapshotBlock)listener { - FIRFirestore *firestore = self.firestore; - FSTQuery *query = self.query; - - FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { - if (error) { - listener(nil, error); - return; - } - - FIRSnapshotMetadata *metadata = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites - fromCache:snapshot.fromCache]; - - listener([FIRQuerySnapshot snapshotWithFirestore:firestore - originalQuery:query - snapshot:snapshot - metadata:metadata], - nil); - }; - - FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue - snapshotHandler:snapshotHandler]; - - FSTQueryListener *internalListener = - [firestore.client listenToQuery:query - options:internalOptions - viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; - return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client - asyncListener:asyncListener - internalListener:internalListener]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual - field:field - value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { - return - [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual - field:field - value:value]; -} - -- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { - return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual - path:path.internalValue - value:value]; -} - -- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate { - NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate; - if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { - FSTThrowInvalidArgument(@"Invalid query. Predicate cannot have an aggregate modifier."); - } - NSString *path; - id value = nil; - if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType && - [comparison.rightExpression expressionType] == NSConstantValueExpressionType) { - path = comparison.leftExpression.keyPath; - value = comparison.rightExpression.constantValue; - switch (comparison.predicateOperatorType) { - case NSEqualToPredicateOperatorType: - return [self queryWhereField:path isEqualTo:value]; - case NSLessThanPredicateOperatorType: - return [self queryWhereField:path isLessThan:value]; - case NSLessThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isLessThanOrEqualTo:value]; - case NSGreaterThanPredicateOperatorType: - return [self queryWhereField:path isGreaterThan:value]; - case NSGreaterThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isGreaterThanOrEqualTo:value]; - default:; // Fallback below to throw assertion. - } - } else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType && - [comparison.rightExpression expressionType] == NSKeyPathExpressionType) { - path = comparison.rightExpression.keyPath; - value = comparison.leftExpression.constantValue; - switch (comparison.predicateOperatorType) { - case NSEqualToPredicateOperatorType: - return [self queryWhereField:path isEqualTo:value]; - case NSLessThanPredicateOperatorType: - return [self queryWhereField:path isGreaterThan:value]; - case NSLessThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isGreaterThanOrEqualTo:value]; - case NSGreaterThanPredicateOperatorType: - return [self queryWhereField:path isLessThan:value]; - case NSGreaterThanOrEqualToPredicateOperatorType: - return [self queryWhereField:path isLessThanOrEqualTo:value]; - default:; // Fallback below to throw assertion. - } - } else { - FSTThrowInvalidArgument( - @"Invalid query. Predicate comparisons must include a key path and a constant."); - } - // Fallback cases of unsupported comparison operator. - switch (comparison.predicateOperatorType) { - case NSCustomSelectorPredicateOperatorType: - FSTThrowInvalidArgument(@"Invalid query. Custom predicate filters are not supported."); - break; - default: - FSTThrowInvalidArgument(@"Invalid query. Operator type %lu is not supported.", - (unsigned long)comparison.predicateOperatorType); - } -} - -- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate { - NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; - if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) { - FSTThrowInvalidArgument(@"Invalid query. Only compound queries using AND are supported."); - } - FIRQuery *query = self; - for (NSPredicate *pred in compound.subpredicates) { - query = [query queryFilteredUsingPredicate:pred]; - } - return query; -} - -- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate { - if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { - return [self queryFilteredUsingComparisonPredicate:predicate]; - } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { - return [self queryFilteredUsingCompoundPredicate:predicate]; - } else if ([predicate isKindOfClass:[[NSPredicate - predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { - return true; - }] class]]) { - FSTThrowInvalidArgument( - @"Invalid query. Block-based predicates are not " - "supported. Please use predicateWithFormat to " - "create predicates instead."); - } else { - FSTThrowInvalidArgument( - @"Invalid query. Expect comparison or compound of " - "comparison predicate. Please use " - "predicateWithFormat to create predicates."); - } -} - -- (FIRQuery *)queryOrderedByField:(NSString *)field { - return - [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; -} - -- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { - return [self queryOrderedByFieldPath:fieldPath descending:NO]; -} - -- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { - return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] - descending:descending]; -} - -- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { - [self validateNewOrderByPath:fieldPath.internalValue]; - if (self.query.startAt) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid query. You must not specify a starting point before specifying the order by."); - } - if (self.query.endAt) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid query. You must not specify an ending point before specifying the order by."); - } - FSTSortOrder *sortOrder = - [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] - firestore:self.firestore]; -} - -- (FIRQuery *)queryLimitedTo:(NSInteger)limit { - if (limit <= 0) { - FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", - (long)limit); - } - return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; -} - -- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; - return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] - firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { - FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { - FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; - return - [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; -} - -#pragma mark - Private Methods - -/** Private helper for all of the queryWhereField: methods. */ -- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator - field:(NSString *)field - value:(id)value { - return [self queryWithFilterOperator:filterOperator - path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue - value:value]; -} - -- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator - path:(FSTFieldPath *)fieldPath - value:(id)value { - FSTFieldValue *fieldValue; - if ([fieldPath isKeyFieldPath]) { - if ([value isKindOfClass:[NSString class]]) { - NSString *documentKey = (NSString *)value; - if ([documentKey containsString:@"/"]) { - FSTThrowInvalidArgument( - @"Invalid query. When querying by document ID you must provide " - "a valid document ID, but '%@' contains a '/' character.", - documentKey); - } else if (documentKey.length == 0) { - FSTThrowInvalidArgument( - @"Invalid query. When querying by document ID you must provide " - "a valid document ID, but it was an empty string."); - } - FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; - fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] - databaseID:self.firestore.databaseID]; - } else if ([value isKindOfClass:[FIRDocumentReference class]]) { - FIRDocumentReference *ref = (FIRDocumentReference *)value; - fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; - } else { - FSTThrowInvalidArgument( - @"Invalid query. When querying by document ID you must provide a " - "valid string or DocumentReference, but it was of type: %@", - NSStringFromClass([value class])); - } - } else { - fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; - } - - id filter; - if ([fieldValue isEqual:[FSTNullValue nullValue]]) { - if (filterOperator != FSTRelationFilterOperatorEqual) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid Query. You can only perform equality comparisons on nil / " - "NSNull."); - } - filter = [[FSTNullFilter alloc] initWithField:fieldPath]; - } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { - if (filterOperator != FSTRelationFilterOperatorEqual) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid Query. You can only perform equality comparisons on NaN."); - } - filter = [[FSTNanFilter alloc] initWithField:fieldPath]; - } else { - filter = [FSTRelationFilter filterWithField:fieldPath - filterOperator:filterOperator - value:fieldValue]; - [self validateNewRelationFilter:filter]; - } - return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] - firestore:self.firestore]; -} - -- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { - if ([filter isInequality]) { - FSTFieldPath *existingField = [self.query inequalityFilterField]; - if (existingField && ![existingField isEqual:filter.field]) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid Query. All where filters with an inequality " - "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " - "field. But you have inequality filters on '%@' and '%@'", - existingField, filter.field); - } - - FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; - if (firstOrderByField) { - [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; - } - } -} - -- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { - if (![self.query firstSortOrderField]) { - // This is the first order by. It must match any inequality. - FSTFieldPath *inequalityField = [self.query inequalityFilterField]; - if (inequalityField) { - [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; - } - } -} - -- (void)validateOrderByField:(FSTFieldPath *)orderByField - matchesInequalityField:(FSTFieldPath *)inequalityField { - if (!([orderByField isEqual:inequalityField])) { - FSTThrowInvalidUsage( - @"InvalidQueryException", - @"Invalid query. You have a where filter with an " - "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " - "and so you must also use '%@' as your first queryOrderedBy field, but your first " - "queryOrderedBy is currently on field '%@' instead.", - inequalityField, inequalityField, orderByField); - } -} - -/** - * Create a FSTBound from a query given the document. - * - * Note that the FSTBound will always include the key of the document and the position will be - * unambiguous. - * - * Will throw if the document does not contain all fields of the order by of the query. - */ -- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { - if (![snapshot exists]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. You are trying to start or end a query using a document " - @"that doesn't exist."); - } - FSTDocument *document = snapshot.internalDocument; - NSMutableArray *components = [NSMutableArray array]; - - // Because people expect to continue/end a query at the exact document provided, we need to - // use the implicit sort order rather than the explicit sort order, because it's guaranteed to - // contain the document key. That way the position becomes unambiguous and the query - // continues/ends exactly at the provided document. Without the key (by using the explicit sort - // orders), multiple documents could match the position, yielding duplicate results. - for (FSTSortOrder *sortOrder in self.query.sortOrders) { - if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { - [components addObject:[FSTReferenceValue referenceValue:document.key - databaseID:self.firestore.databaseID]]; - } else { - FSTFieldValue *value = [document fieldForPath:sortOrder.field]; - if (value != nil) { - [components addObject:value]; - } else { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. You are trying to start or end a query using a " - "document for which the field '%@' (used as the order by) " - "does not exist.", - sortOrder.field.canonicalString); - } - } - } - return [FSTBound boundWithPosition:components isBefore:isBefore]; -} - -/** Converts a list of field values to an FSTBound. */ -- (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isBefore { - // Use explicit sort order because it has to match the query the user made - NSArray *explicitSortOrders = self.query.explicitSortOrders; - if (fieldValues.count > explicitSortOrders.count) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. You are trying to start or end a query using more values " - @"than were specified in the order by."); - } - - NSMutableArray *components = [NSMutableArray array]; - [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { - FSTSortOrder *sortOrder = explicitSortOrders[idx]; - if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { - if (![rawValue isKindOfClass:[NSString class]]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. Expected a string for the document ID."); - } - NSString *documentID = (NSString *)rawValue; - if ([documentID containsString:@"/"]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. Document ID '%@' contains a slash.", documentID); - } - FSTDocumentKey *key = - [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; - [components - addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; - } else { - FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; - [components addObject:fieldValue]; - } - }]; - - return [FSTBound boundWithPosition:components isBefore:isBefore]; -} - -/** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { - return [[FSTListenOptions alloc] - initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges - includeDocumentMetadataChanges:options.includeDocumentMetadataChanges - waitForSyncWhenOnline:NO]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm new file mode 100644 index 0000000..1bbf91e --- /dev/null +++ b/Firestore/Source/API/FIRQuery.mm @@ -0,0 +1,633 @@ +/* + * 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 "FIRQuery.h" + +#import "FIRDocumentReference.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "Firestore/Source/API/FIRQuery_Init.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQueryListenOptions () + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryListenOptions + ++ (instancetype)options { + return [[FIRQueryListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; +} + +- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges + includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; +} + +- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges + includeDocumentMetadataChanges:includeDocumentMetadataChanges]; +} + +@end + +@interface FIRQuery () +@property(nonatomic, strong, readonly) FSTQuery *query; +@end + +@implementation FIRQuery (Internal) ++ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + return [[FIRQuery alloc] initWithQuery:query firestore:firestore]; +} +@end + +@implementation FIRQuery + +#pragma mark - Constructor Methods + +- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _query = query; + _firestore = firestore; + } + return self; +} + +#pragma mark - NSObject Methods + +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToQuery:other]; +} + +- (BOOL)isEqualToQuery:(nullable FIRQuery *)query { + if (self == query) return YES; + if (query == nil) return NO; + + return [self.firestore isEqual:query.firestore] && [self.query isEqual:query.query]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.query hash]; + return hash; +} + +#pragma mark - Public Methods + +- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion { + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id listenerRegistration; + FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + completion(snapshot, nil); + }; + + listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id)addSnapshotListener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRQuerySnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = self.query; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites + fromCache:snapshot.fromCache]; + + listener([FIRQuerySnapshot snapshotWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata], + nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { + return + [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate { + NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate; + if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { + FSTThrowInvalidArgument(@"Invalid query. Predicate cannot have an aggregate modifier."); + } + NSString *path; + id value = nil; + if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType && + [comparison.rightExpression expressionType] == NSConstantValueExpressionType) { + path = comparison.leftExpression.keyPath; + value = comparison.rightExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType && + [comparison.rightExpression expressionType] == NSKeyPathExpressionType) { + path = comparison.rightExpression.keyPath; + value = comparison.leftExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else { + FSTThrowInvalidArgument( + @"Invalid query. Predicate comparisons must include a key path and a constant."); + } + // Fallback cases of unsupported comparison operator. + switch (comparison.predicateOperatorType) { + case NSCustomSelectorPredicateOperatorType: + FSTThrowInvalidArgument(@"Invalid query. Custom predicate filters are not supported."); + break; + default: + FSTThrowInvalidArgument(@"Invalid query. Operator type %lu is not supported.", + (unsigned long)comparison.predicateOperatorType); + } +} + +- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate { + NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; + if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) { + FSTThrowInvalidArgument(@"Invalid query. Only compound queries using AND are supported."); + } + FIRQuery *query = self; + for (NSPredicate *pred in compound.subpredicates) { + query = [query queryFilteredUsingPredicate:pred]; + } + return query; +} + +- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate { + if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { + return [self queryFilteredUsingComparisonPredicate:predicate]; + } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { + return [self queryFilteredUsingCompoundPredicate:predicate]; + } else if ([predicate isKindOfClass:[[NSPredicate + predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { + return true; + }] class]]) { + FSTThrowInvalidArgument( + @"Invalid query. Block-based predicates are not " + "supported. Please use predicateWithFormat to " + "create predicates instead."); + } else { + FSTThrowInvalidArgument( + @"Invalid query. Expect comparison or compound of " + "comparison predicate. Please use " + "predicateWithFormat to create predicates."); + } +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field { + return + [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { + return [self queryOrderedByFieldPath:fieldPath descending:NO]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { + return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] + descending:descending]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { + [self validateNewOrderByPath:fieldPath.internalValue]; + if (self.query.startAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify a starting point before specifying the order by."); + } + if (self.query.endAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify an ending point before specifying the order by."); + } + FSTSortOrder *sortOrder = + [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] + firestore:self.firestore]; +} + +- (FIRQuery *)queryLimitedTo:(NSInteger)limit { + if (limit <= 0) { + FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", + (long)limit); + } + return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; +} + +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +#pragma mark - Private Methods + +/** Private helper for all of the queryWhereField: methods. */ +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + field:(NSString *)field + value:(id)value { + return [self queryWithFilterOperator:filterOperator + path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue + value:value]; +} + +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + path:(FSTFieldPath *)fieldPath + value:(id)value { + FSTFieldValue *fieldValue; + if ([fieldPath isKeyFieldPath]) { + if ([value isKindOfClass:[NSString class]]) { + NSString *documentKey = (NSString *)value; + if ([documentKey containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but '%@' contains a '/' character.", + documentKey); + } else if (documentKey.length == 0) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but it was an empty string."); + } + FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] + databaseID:self.firestore.databaseID]; + } else if ([value isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *ref = (FIRDocumentReference *)value; + fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + } else { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide a " + "valid string or DocumentReference, but it was of type: %@", + NSStringFromClass([value class])); + } + } else { + fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; + } + + id filter; + if ([fieldValue isEqual:[FSTNullValue nullValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on nil / " + "NSNull."); + } + filter = [[FSTNullFilter alloc] initWithField:fieldPath]; + } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on NaN."); + } + filter = [[FSTNanFilter alloc] initWithField:fieldPath]; + } else { + filter = [FSTRelationFilter filterWithField:fieldPath + filterOperator:filterOperator + value:fieldValue]; + [self validateNewRelationFilter:filter]; + } + return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] + firestore:self.firestore]; +} + +- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { + if ([filter isInequality]) { + FSTFieldPath *existingField = [self.query inequalityFilterField]; + if (existingField && ![existingField isEqual:filter.field]) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid Query. All where filters with an inequality " + "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on '%@' and '%@'", + existingField, filter.field); + } + + FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; + if (firstOrderByField) { + [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; + } + } +} + +- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { + if (![self.query firstSortOrderField]) { + // This is the first order by. It must match any inequality. + FSTFieldPath *inequalityField = [self.query inequalityFilterField]; + if (inequalityField) { + [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; + } + } +} + +- (void)validateOrderByField:(FSTFieldPath *)orderByField + matchesInequalityField:(FSTFieldPath *)inequalityField { + if (!([orderByField isEqual:inequalityField])) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You have a where filter with an " + "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " + "and so you must also use '%@' as your first queryOrderedBy field, but your first " + "queryOrderedBy is currently on field '%@' instead.", + inequalityField, inequalityField, orderByField); + } +} + +/** + * Create a FSTBound from a query given the document. + * + * Note that the FSTBound will always include the key of the document and the position will be + * unambiguous. + * + * Will throw if the document does not contain all fields of the order by of the query. + */ +- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { + if (![snapshot exists]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a document " + @"that doesn't exist."); + } + FSTDocument *document = snapshot.internalDocument; + NSMutableArray *components = [NSMutableArray array]; + + // Because people expect to continue/end a query at the exact document provided, we need to + // use the implicit sort order rather than the explicit sort order, because it's guaranteed to + // contain the document key. That way the position becomes unambiguous and the query + // continues/ends exactly at the provided document. Without the key (by using the explicit sort + // orders), multiple documents could match the position, yielding duplicate results. + for (FSTSortOrder *sortOrder in self.query.sortOrders) { + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + [components addObject:[FSTReferenceValue referenceValue:document.key + databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *value = [document fieldForPath:sortOrder.field]; + if (value != nil) { + [components addObject:value]; + } else { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a " + "document for which the field '%@' (used as the order by) " + "does not exist.", + sortOrder.field.canonicalString); + } + } + } + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts a list of field values to an FSTBound. */ +- (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isBefore { + // Use explicit sort order because it has to match the query the user made + NSArray *explicitSortOrders = self.query.explicitSortOrders; + if (fieldValues.count > explicitSortOrders.count) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using more values " + @"than were specified in the order by."); + } + + NSMutableArray *components = [NSMutableArray array]; + [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { + FSTSortOrder *sortOrder = explicitSortOrders[idx]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + if (![rawValue isKindOfClass:[NSString class]]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Expected a string for the document ID."); + } + NSString *documentID = (NSString *)rawValue; + if ([documentID containsString:@"/"]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Document ID '%@' contains a slash.", documentID); + } + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; + [components + addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; + [components addObject:fieldValue]; + } + }]; + + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { + return [[FSTListenOptions alloc] + initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges + includeDocumentMetadataChanges:options.includeDocumentMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m deleted file mode 100644 index abee84c..0000000 --- a/Firestore/Source/API/FIRQuerySnapshot.m +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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/API/FIRQuerySnapshot+Internal.h" - -#import "FIRFirestore.h" -#import "FIRSnapshotMetadata.h" -#import "Firestore/Source/API/FIRDocumentChange+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQuerySnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) FSTQuery *originalQuery; -@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; - -@end - -@implementation FIRQuerySnapshot (Internal) - -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata { - return [[FIRQuerySnapshot alloc] initWithFirestore:firestore - originalQuery:query - snapshot:snapshot - metadata:metadata]; -} - -@end - -@implementation FIRQuerySnapshot { - // Cached value of the documents property. - NSArray *_documents; - - // Cached value of the documentChanges property. - NSArray *_documentChanges; -} - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata { - if (self = [super init]) { - _firestore = firestore; - _originalQuery = query; - _snapshot = snapshot; - _metadata = metadata; - } - return self; -} - -// NSObject Methods -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToSnapshot:other]; -} - -- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { - if (self == snapshot) return YES; - if (snapshot == nil) return NO; - - return [self.firestore isEqual:snapshot.firestore] && - [self.originalQuery isEqual:snapshot.originalQuery] && - [self.snapshot isEqual:snapshot.snapshot] && [self.metadata isEqual:snapshot.metadata]; -} - -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.originalQuery hash]; - hash = hash * 31u + [self.snapshot hash]; - hash = hash * 31u + [self.metadata hash]; - return hash; -} - -@dynamic empty; - -- (FIRQuery *)query { - return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; -} - -- (BOOL)isEmpty { - return self.snapshot.documents.isEmpty; -} - -// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) -// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints -// where we can. See cr/146959032 for additional context. -- (NSInteger)count { - return self.snapshot.documents.count; -} - -- (NSArray *)documents { - if (!_documents) { - FSTDocumentSet *documentSet = self.snapshot.documents; - FIRFirestore *firestore = self.firestore; - BOOL fromCache = self.metadata.fromCache; - - NSMutableArray *result = [NSMutableArray array]; - for (FSTDocument *document in documentSet.documentEnumerator) { - [result addObject:[FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:document.key - document:document - fromCache:fromCache]]; - } - - _documents = result; - } - return _documents; -} - -- (NSArray *)documentChanges { - if (!_documentChanges) { - _documentChanges = - [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; - } - return _documentChanges; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.mm b/Firestore/Source/API/FIRQuerySnapshot.mm new file mode 100644 index 0000000..abee84c --- /dev/null +++ b/Firestore/Source/API/FIRQuerySnapshot.mm @@ -0,0 +1,151 @@ +/* + * 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/API/FIRQuerySnapshot+Internal.h" + +#import "FIRFirestore.h" +#import "FIRSnapshotMetadata.h" +#import "Firestore/Source/API/FIRDocumentChange+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQuerySnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTQuery *originalQuery; +@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; + +@end + +@implementation FIRQuerySnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + return [[FIRQuerySnapshot alloc] initWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata]; +} + +@end + +@implementation FIRQuerySnapshot { + // Cached value of the documents property. + NSArray *_documents; + + // Cached value of the documentChanges property. + NSArray *_documentChanges; +} + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + if (self = [super init]) { + _firestore = firestore; + _originalQuery = query; + _snapshot = snapshot; + _metadata = metadata; + } + return self; +} + +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + + return [self.firestore isEqual:snapshot.firestore] && + [self.originalQuery isEqual:snapshot.originalQuery] && + [self.snapshot isEqual:snapshot.snapshot] && [self.metadata isEqual:snapshot.metadata]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.originalQuery hash]; + hash = hash * 31u + [self.snapshot hash]; + hash = hash * 31u + [self.metadata hash]; + return hash; +} + +@dynamic empty; + +- (FIRQuery *)query { + return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; +} + +- (BOOL)isEmpty { + return self.snapshot.documents.isEmpty; +} + +// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) +// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints +// where we can. See cr/146959032 for additional context. +- (NSInteger)count { + return self.snapshot.documents.count; +} + +- (NSArray *)documents { + if (!_documents) { + FSTDocumentSet *documentSet = self.snapshot.documents; + FIRFirestore *firestore = self.firestore; + BOOL fromCache = self.metadata.fromCache; + + NSMutableArray *result = [NSMutableArray array]; + for (FSTDocument *document in documentSet.documentEnumerator) { + [result addObject:[FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; + } + + _documents = result; + } + return _documents; +} + +- (NSArray *)documentChanges { + if (!_documentChanges) { + _documentChanges = + [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; + } + return _documentChanges; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m deleted file mode 100644 index b41086c..0000000 --- a/Firestore/Source/API/FIRSetOptions.m +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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/API/FIRSetOptions+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FIRSetOptions - -- (instancetype)initWithMerge:(BOOL)merge { - if (self = [super init]) { - _merge = merge; - } - return self; -} - -+ (instancetype)merge { - return [[FIRSetOptions alloc] initWithMerge:YES]; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } else if (![other isKindOfClass:[FIRSetOptions class]]) { - return NO; - } - - FIRSetOptions *otherOptions = (FIRSetOptions *)other; - return otherOptions.merge == self.merge; -} - -- (NSUInteger)hash { - return self.merge ? 1231 : 1237; -} -@end - -@implementation FIRSetOptions (Internal) - -+ (instancetype)overwrite { - static FIRSetOptions *overwriteInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO]; - }); - return overwriteInstance; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.mm b/Firestore/Source/API/FIRSetOptions.mm new file mode 100644 index 0000000..b41086c --- /dev/null +++ b/Firestore/Source/API/FIRSetOptions.mm @@ -0,0 +1,63 @@ +/* + * 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/API/FIRSetOptions+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRSetOptions + +- (instancetype)initWithMerge:(BOOL)merge { + if (self = [super init]) { + _merge = merge; + } + return self; +} + ++ (instancetype)merge { + return [[FIRSetOptions alloc] initWithMerge:YES]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRSetOptions class]]) { + return NO; + } + + FIRSetOptions *otherOptions = (FIRSetOptions *)other; + return otherOptions.merge == self.merge; +} + +- (NSUInteger)hash { + return self.merge ? 1231 : 1237; +} +@end + +@implementation FIRSetOptions (Internal) + ++ (instancetype)overwrite { + static FIRSetOptions *overwriteInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO]; + }); + return overwriteInstance; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m deleted file mode 100644 index 27747ce..0000000 --- a/Firestore/Source/API/FIRSnapshotMetadata.m +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 "FIRSnapshotMetadata.h" - -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotMetadata () - -- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; - -@end - -@implementation FIRSnapshotMetadata (Internal) - -+ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { - return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; -} - -@end - -@implementation FIRSnapshotMetadata - -- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { - if (self = [super init]) { - _pendingWrites = pendingWrites; - _fromCache = fromCache; - } - return self; -} - -// NSObject Methods -- (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; - - return [self isEqualToMetadata:other]; -} - -- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { - if (self == metadata) return YES; - if (metadata == nil) return NO; - - return self.pendingWrites == metadata.pendingWrites && self.fromCache == metadata.fromCache; -} - -- (NSUInteger)hash { - NSUInteger hash = self.pendingWrites ? 1 : 0; - hash = hash * 31u + (self.fromCache ? 1 : 0); - return hash; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata.mm b/Firestore/Source/API/FIRSnapshotMetadata.mm new file mode 100644 index 0000000..27747ce --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotMetadata.mm @@ -0,0 +1,70 @@ +/* + * 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 "FIRSnapshotMetadata.h" + +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadata () + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; + +@end + +@implementation FIRSnapshotMetadata (Internal) + ++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; +} + +@end + +@implementation FIRSnapshotMetadata + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + if (self = [super init]) { + _pendingWrites = pendingWrites; + _fromCache = fromCache; + } + return self; +} + +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToMetadata:other]; +} + +- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { + if (self == metadata) return YES; + if (metadata == nil) return NO; + + return self.pendingWrites == metadata.pendingWrites && self.fromCache == metadata.fromCache; +} + +- (NSUInteger)hash { + NSUInteger hash = self.pendingWrites ? 1 : 0; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions.m b/Firestore/Source/API/FIRSnapshotOptions.m deleted file mode 100644 index 72ea3cc..0000000 --- a/Firestore/Source/API/FIRSnapshotOptions.m +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 "FIRDocumentSnapshot.h" - -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotOptions () - -@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; - -@end - -@implementation FIRSnapshotOptions - -- (instancetype)initWithServerTimestampBehavior: - (FSTServerTimestampBehavior)serverTimestampBehavior { - self = [super init]; - - if (self) { - _serverTimestampBehavior = serverTimestampBehavior; - } - - return self; -} - -+ (instancetype)defaultOptions { - static FIRSnapshotOptions *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = - [[FIRSnapshotOptions alloc] initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; - }); - - return sharedInstance; -} - -+ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { - switch (serverTimestampBehavior) { - case FIRServerTimestampBehaviorEstimate: - return [[FIRSnapshotOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; - case FIRServerTimestampBehaviorPrevious: - return [[FIRSnapshotOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; - case FIRServerTimestampBehaviorNone: - return [FIRSnapshotOptions defaultOptions]; - default: - FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); - } -} - -@end - -NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/API/FIRSnapshotOptions.mm b/Firestore/Source/API/FIRSnapshotOptions.mm new file mode 100644 index 0000000..72ea3cc --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions.mm @@ -0,0 +1,72 @@ +/* + * 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 "FIRDocumentSnapshot.h" + +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotOptions () + +@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; + +@end + +@implementation FIRSnapshotOptions + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + + return self; +} + ++ (instancetype)defaultOptions { + static FIRSnapshotOptions *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = + [[FIRSnapshotOptions alloc] initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; + }); + + return sharedInstance; +} + ++ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { + switch (serverTimestampBehavior) { + case FIRServerTimestampBehaviorEstimate: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; + case FIRServerTimestampBehaviorPrevious: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; + case FIRServerTimestampBehaviorNone: + return [FIRSnapshotOptions defaultOptions]; + default: + FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); + } +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/API/FIRTransaction.m b/Firestore/Source/API/FIRTransaction.m deleted file mode 100644 index 5edff19..0000000 --- a/Firestore/Source/API/FIRTransaction.m +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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/API/FIRTransaction+Internal.h" - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTTransaction.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FIRTransaction - -@interface FIRTransaction () - -- (instancetype)initWithTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@end - -@implementation FIRTransaction (Internal) - -+ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore { - return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; -} - -@end - -@implementation FIRTransaction - -- (instancetype)initWithTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore { - self = [super init]; - if (self) { - _internalTransaction = transaction; - _firestore = firestore; - } - return self; -} - -- (FIRTransaction *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document { - return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; -} - -- (FIRTransaction *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document - options:(FIRSetOptions *)options { - [self validateReference:document]; - FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; - [self.internalTransaction setData:parsed forDocument:document.key]; - return self; -} - -- (FIRTransaction *)updateData:(NSDictionary *)fields - forDocument:(FIRDocumentReference *)document { - [self validateReference:document]; - FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.internalTransaction updateData:parsed forDocument:document.key]; - return self; -} - -- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { - [self validateReference:document]; - [self.internalTransaction deleteDocument:document.key]; - return self; -} - -- (void)getDocument:(FIRDocumentReference *)document - completion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { - [self validateReference:document]; - [self.internalTransaction - lookupDocumentsForKeys:@[ document.key ] - completion:^(NSArray *_Nullable documents, - NSError *_Nullable error) { - if (error) { - completion(nil, error); - return; - } - FSTAssert(documents.count == 1, - @"Mismatch in docs returned from document lookup."); - FSTMaybeDocument *internalDoc = documents.firstObject; - if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { - completion(nil, nil); - return; - } - FIRDocumentSnapshot *doc = - [FIRDocumentSnapshot snapshotWithFirestore:self.firestore - documentKey:internalDoc.key - document:(FSTDocument *)internalDoc - fromCache:NO]; - completion(doc, nil); - }]; -} - -- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document - error:(NSError *__autoreleasing *)error { - [self validateReference:document]; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - __block FIRDocumentSnapshot *result; - // We have to explicitly assign the innerError into a local to cause it to retain correctly. - __block NSError *outerError = nil; - [self getDocument:document - completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { - result = snapshot; - outerError = innerError; - dispatch_semaphore_signal(semaphore); - }]; - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); - if (error) { - *error = outerError; - } - return result; -} - -- (void)validateReference:(FIRDocumentReference *)reference { - if (reference.firestore != self.firestore) { - FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRTransaction.mm b/Firestore/Source/API/FIRTransaction.mm new file mode 100644 index 0000000..5edff19 --- /dev/null +++ b/Firestore/Source/API/FIRTransaction.mm @@ -0,0 +1,148 @@ +/* + * 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/API/FIRTransaction+Internal.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTTransaction.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRTransaction + +@interface FIRTransaction () + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@end + +@implementation FIRTransaction (Internal) + ++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; +} + +@end + +@implementation FIRTransaction + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _internalTransaction = transaction; + _firestore = firestore; + } + return self; +} + +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self validateReference:document]; + FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] + : [self.firestore.dataConverter parsedSetData:data]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.internalTransaction updateData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + [self.internalTransaction deleteDocument:document.key]; + return self; +} + +- (void)getDocument:(FIRDocumentReference *)document + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + [self validateReference:document]; + [self.internalTransaction + lookupDocumentsForKeys:@[ document.key ] + completion:^(NSArray *_Nullable documents, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + FSTAssert(documents.count == 1, + @"Mismatch in docs returned from document lookup."); + FSTMaybeDocument *internalDoc = documents.firstObject; + if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { + completion(nil, nil); + return; + } + FIRDocumentSnapshot *doc = + [FIRDocumentSnapshot snapshotWithFirestore:self.firestore + documentKey:internalDoc.key + document:(FSTDocument *)internalDoc + fromCache:NO]; + completion(doc, nil); + }]; +} + +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error { + [self validateReference:document]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block FIRDocumentSnapshot *result; + // We have to explicitly assign the innerError into a local to cause it to retain correctly. + __block NSError *outerError = nil; + [self getDocument:document + completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { + result = snapshot; + outerError = innerError; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (error) { + *error = outerError; + } + return result; +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m deleted file mode 100644 index b1cfa09..0000000 --- a/Firestore/Source/API/FIRWriteBatch.m +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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/API/FIRWriteBatch+Internal.h" - -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FIRWriteBatch - -@interface FIRWriteBatch () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) NSMutableArray *mutations; -@property(nonatomic, assign) BOOL committed; - -@end - -@implementation FIRWriteBatch (Internal) - -+ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { - return [[FIRWriteBatch alloc] initWithFirestore:firestore]; -} - -@end - -@implementation FIRWriteBatch - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore { - self = [super init]; - if (self) { - _firestore = firestore; - _mutations = [NSMutableArray array]; - } - return self; -} - -- (FIRWriteBatch *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document { - return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; -} - -- (FIRWriteBatch *)setData:(NSDictionary *)data - forDocument:(FIRDocumentReference *)document - options:(FIRSetOptions *)options { - [self verifyNotCommitted]; - [self validateReference:document]; - FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; - [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key - precondition:[FSTPrecondition none]]]; - return self; -} - -- (FIRWriteBatch *)updateData:(NSDictionary *)fields - forDocument:(FIRDocumentReference *)document { - [self verifyNotCommitted]; - [self validateReference:document]; - FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.mutations - addObjectsFromArray:[parsed mutationsWithKey:document.key - precondition:[FSTPrecondition preconditionWithExists:YES]]]; - return self; -} - -- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { - [self verifyNotCommitted]; - [self validateReference:document]; - [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key - precondition:[FSTPrecondition none]]]; - return self; -} - -- (void)commit { - [self commitWithCompletion:nil]; -} - -- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - [self verifyNotCommitted]; - self.committed = TRUE; - [self.firestore.client writeMutations:self.mutations completion:completion]; -} - -- (void)verifyNotCommitted { - if (self.committed) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"A write batch can no longer be used after commit has been called."); - } -} - -- (void)validateReference:(FIRDocumentReference *)reference { - if (reference.firestore != self.firestore) { - FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm new file mode 100644 index 0000000..b1cfa09 --- /dev/null +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -0,0 +1,121 @@ +/* + * 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/API/FIRWriteBatch+Internal.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/API/FSTUserDataConverter.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRWriteBatch + +@interface FIRWriteBatch () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) NSMutableArray *mutations; +@property(nonatomic, assign) BOOL committed; + +@end + +@implementation FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { + return [[FIRWriteBatch alloc] initWithFirestore:firestore]; +} + +@end + +@implementation FIRWriteBatch + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _firestore = firestore; + _mutations = [NSMutableArray array]; + } + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data] + : [self.firestore.dataConverter parsedSetData:data]; + [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (FIRWriteBatch *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition preconditionWithExists:YES]]]; + return self; +} + +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (void)commit { + [self commitWithCompletion:nil]; +} + +- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + [self verifyNotCommitted]; + self.committed = TRUE; + [self.firestore.client writeMutations:self.mutations completion:completion]; +} + +- (void)verifyNotCommitted { + if (self.committed) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"A write batch can no longer be used after commit has been called."); + } +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.m b/Firestore/Source/API/FSTUserDataConverter.m deleted file mode 100644 index 414aadb..0000000 --- a/Firestore/Source/API/FSTUserDataConverter.m +++ /dev/null @@ -1,598 +0,0 @@ -/* - * 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/API/FSTUserDataConverter.h" - -#import "FIRGeoPoint.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFieldPath+Internal.h" -#import "Firestore/Source/API/FIRFieldValue+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/Core/FSTTimestamp.h" -#import "Firestore/Source/Model/FSTDatabaseID.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" -#import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTPath.h" -#import "Firestore/Source/Util/FSTAssert.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const RESERVED_FIELD_DESIGNATOR = @"__"; - -#pragma mark - FSTParsedSetData - -@implementation FSTParsedSetData -- (instancetype)initWithData:(FSTObjectValue *)data - fieldMask:(nullable FSTFieldMask *)fieldMask - fieldTransforms:(NSArray *)fieldTransforms { - self = [super init]; - if (self) { - _data = data; - _fieldMask = fieldMask; - _fieldTransforms = fieldTransforms; - } - return self; -} - -- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key - precondition:(FSTPrecondition *)precondition { - NSMutableArray *mutations = [NSMutableArray array]; - if (self.fieldMask) { - [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:self.fieldMask - value:self.data - precondition:precondition]]; - } else { - [mutations addObject:[[FSTSetMutation alloc] initWithKey:key - value:self.data - precondition:precondition]]; - } - if (self.fieldTransforms.count > 0) { - [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key - fieldTransforms:self.fieldTransforms]]; - } - return mutations; -} - -@end - -#pragma mark - FSTParsedUpdateData - -@implementation FSTParsedUpdateData -- (instancetype)initWithData:(FSTObjectValue *)data - fieldMask:(FSTFieldMask *)fieldMask - fieldTransforms:(NSArray *)fieldTransforms { - self = [super init]; - if (self) { - _data = data; - _fieldMask = fieldMask; - _fieldTransforms = fieldTransforms; - } - return self; -} - -- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key - precondition:(FSTPrecondition *)precondition { - NSMutableArray *mutations = [NSMutableArray array]; - [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:self.fieldMask - value:self.data - precondition:precondition]]; - if (self.fieldTransforms.count > 0) { - [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key - fieldTransforms:self.fieldTransforms]]; - } - return mutations; -} - -@end - -/** - * Represents what type of API method provided the data being parsed; useful for determining which - * error conditions apply during parsing and providing better error messages. - */ -typedef NS_ENUM(NSInteger, FSTUserDataSource) { - FSTUserDataSourceSet, - FSTUserDataSourceMergeSet, - FSTUserDataSourceUpdate, - FSTUserDataSourceQueryValue, // from a where clause or cursor bound. -}; - -#pragma mark - FSTParseContext - -/** - * A "context" object passed around while parsing user data. - */ -@interface FSTParseContext : NSObject -/** The current path being parsed. */ -// TODO(b/34871131): path should never be nil, but we don't support array paths right now. -@property(nonatomic, strong, readonly, nullable) FSTFieldPath *path; - -/** Whether or not this context corresponds to an element of an array. */ -@property(nonatomic, assign, readonly, getter=isArrayElement) BOOL arrayElement; - -/** - * What type of API method provided the data being parsed; useful for determining which error - * conditions apply during parsing and providing better error messages. - */ -@property(nonatomic, assign) FSTUserDataSource dataSource; -@property(nonatomic, strong, readonly) NSMutableArray *fieldTransforms; -@property(nonatomic, strong, readonly) NSMutableArray *fieldMask; - -- (instancetype)init NS_UNAVAILABLE; -/** - * Initializes a FSTParseContext with the given source and path. - * - * @param dataSource Indicates what kind of API method this data came from. - * @param path A path within the object being parsed. This could be an empty path (in which case - * the context represents the root of the data being parsed), or a nonempty path (indicating the - * context represents a nested location within the data). - * - * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate - * the context represents any location within an array (in which case certain features will not work - * and errors will be somewhat compromised). - */ -- (instancetype)initWithSource:(FSTUserDataSource)dataSource - path:(nullable FSTFieldPath *)path - arrayElement:(BOOL)arrayElement - fieldTransforms:(NSMutableArray *)fieldTransforms - fieldMask:(NSMutableArray *)fieldMask - NS_DESIGNATED_INITIALIZER; - -// Helpers to get a FSTParseContext for a child field. -- (instancetype)contextForField:(NSString *)fieldName; -- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; -- (instancetype)contextForArrayIndex:(NSUInteger)index; - -/** Returns true for the non-query parse contexts (Set, MergeSet and Update) */ -- (BOOL)isWrite; -@end - -@implementation FSTParseContext - -+ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path { - FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource - path:path - arrayElement:NO - fieldTransforms:[NSMutableArray array] - fieldMask:[NSMutableArray array]]; - [context validatePath]; - return context; -} - -- (instancetype)initWithSource:(FSTUserDataSource)dataSource - path:(nullable FSTFieldPath *)path - arrayElement:(BOOL)arrayElement - fieldTransforms:(NSMutableArray *)fieldTransforms - fieldMask:(NSMutableArray *)fieldMask { - if (self = [super init]) { - _dataSource = dataSource; - _path = path; - _arrayElement = arrayElement; - _fieldTransforms = fieldTransforms; - _fieldMask = fieldMask; - } - return self; -} - -- (instancetype)contextForField:(NSString *)fieldName { - FSTParseContext *context = - [[FSTParseContext alloc] initWithSource:self.dataSource - path:[self.path pathByAppendingSegment:fieldName] - arrayElement:NO - fieldTransforms:self.fieldTransforms - fieldMask:self.fieldMask]; - [context validatePathSegment:fieldName]; - return context; -} - -- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath { - FSTParseContext *context = - [[FSTParseContext alloc] initWithSource:self.dataSource - path:[self.path pathByAppendingPath:fieldPath] - arrayElement:NO - fieldTransforms:self.fieldTransforms - fieldMask:self.fieldMask]; - [context validatePath]; - return context; -} - -- (instancetype)contextForArrayIndex:(NSUInteger)index { - // TODO(b/34871131): We don't support array paths right now; so make path nil. - return [[FSTParseContext alloc] initWithSource:self.dataSource - path:nil - arrayElement:YES - fieldTransforms:self.fieldTransforms - fieldMask:self.fieldMask]; -} - -/** - * Returns a string that can be appended to error messages indicating what field caused the error. - */ -- (NSString *)fieldDescription { - // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. - if (!self.path || self.path.empty) { - return @""; - } else { - return [NSString stringWithFormat:@" (found in field %@)", self.path]; - } -} - -- (BOOL)isWrite { - switch (self.dataSource) { - case FSTUserDataSourceSet: // Falls through. - case FSTUserDataSourceMergeSet: // Falls through. - case FSTUserDataSourceUpdate: - return YES; - case FSTUserDataSourceQueryValue: - return NO; - default: - FSTThrowInvalidArgument(@"Unexpected case for FSTUserDataSource: %d", self.dataSource); - } -} - -- (void)validatePath { - // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. - if (self.path == nil) { - return; - } - for (int i = 0; i < self.path.length; i++) { - [self validatePathSegment:[self.path segmentAtIndex:i]]; - } -} - -- (void)validatePathSegment:(NSString *)segment { - if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] && - [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) { - FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@", - RESERVED_FIELD_DESIGNATOR, [self fieldDescription]); - } -} - -@end - -#pragma mark - FSTDocumentKeyReference - -@implementation FSTDocumentKeyReference - -- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID { - self = [super init]; - if (self) { - _key = key; - _databaseID = databaseID; - } - return self; -} - -@end - -#pragma mark - FSTUserDataConverter - -@interface FSTUserDataConverter () -@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID; -@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter; -@end - -@implementation FSTUserDataConverter - -- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID - preConverter:(FSTPreConverterBlock)preConverter { - self = [super init]; - if (self) { - _databaseID = databaseID; - _preConverter = preConverter; - } - return self; -} - -- (FSTParsedSetData *)parsedMergeData:(id)input { - // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust - // Obj-C to verify the type for us. - if (![input isKindOfClass:[NSDictionary class]]) { - FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); - } - - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceMergeSet path:[FSTFieldPath emptyPath]]; - FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; - - return - [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:[[FSTFieldMask alloc] initWithFields:context.fieldMask] - fieldTransforms:context.fieldTransforms]; -} - -- (FSTParsedSetData *)parsedSetData:(id)input { - // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust - // Obj-C to verify the type for us. - if (![input isKindOfClass:[NSDictionary class]]) { - FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); - } - - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]]; - FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; - - return [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:nil - fieldTransforms:context.fieldTransforms]; -} - -- (FSTParsedUpdateData *)parsedUpdateData:(id)input { - // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust - // Obj-C to verify the type for us. - if (![input isKindOfClass:[NSDictionary class]]) { - FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); - } - - NSDictionary *dict = input; - - NSMutableArray *fieldMaskPaths = [NSMutableArray array]; - __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; - - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]]; - [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { - FSTFieldPath *path; - - if ([key isKindOfClass:[NSString class]]) { - path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue; - } else if ([key isKindOfClass:[FIRFieldPath class]]) { - path = ((FIRFieldPath *)key).internalValue; - } else { - FSTThrowInvalidArgument( - @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."); - } - - value = self.preConverter(value); - if ([value isKindOfClass:[FSTDeleteFieldValue class]]) { - // Add it to the field mask, but don't add anything to updateData. - [fieldMaskPaths addObject:path]; - } else { - FSTFieldValue *_Nullable parsedValue = - [self parseData:value context:[context contextForFieldPath:path]]; - if (parsedValue) { - [fieldMaskPaths addObject:path]; - updateData = [updateData objectBySettingValue:parsedValue forPath:path]; - } - } - }]; - - FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths]; - return [[FSTParsedUpdateData alloc] initWithData:updateData - fieldMask:mask - fieldTransforms:context.fieldTransforms]; -} - -- (FSTFieldValue *)parsedQueryValue:(id)input { - FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]]; - FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; - FSTAssert(parsed, @"Parsed data should not be nil."); - FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed."); - return parsed; -} - -/** - * Internal helper for parsing user data. - * - * @param input Data to be parsed. - * @param context A context object representing the current path being parsed, the source of the - * data being parsed, etc. - * - * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be - * included in the resulting parsed data. - */ -- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { - input = self.preConverter(input); - if ([input isKindOfClass:[NSArray class]]) { - // TODO(b/34871131): Include the path containing the array in the error message. - if (context.isArrayElement) { - FSTThrowInvalidArgument(@"Nested arrays are not supported"); - } - NSArray *array = input; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; - [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { - FSTFieldValue *_Nullable parsedEntry = - [self parseData:entry context:[context contextForArrayIndex:idx]]; - if (!parsedEntry) { - // Just include nulls in the array for fields being replaced with a sentinel. - parsedEntry = [FSTNullValue nullValue]; - } - [result addObject:parsedEntry]; - }]; - // If context.path is nil we are already inside an array and we don't support field mask paths - // more granular than the top-level array. - if (context.path) { - [context.fieldMask addObject:context.path]; - } - return [[FSTArrayValue alloc] initWithValueNoCopy:result]; - - } else if ([input isKindOfClass:[NSDictionary class]]) { - NSDictionary *dict = input; - NSMutableDictionary *result = - [NSMutableDictionary dictionaryWithCapacity:dict.count]; - [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { - FSTFieldValue *_Nullable parsedValue = - [self parseData:value context:[context contextForField:key]]; - if (parsedValue) { - result[key] = parsedValue; - } - }]; - return [[FSTObjectValue alloc] initWithDictionary:result]; - - } else { - // If context.path is null, we are inside an array and we should have already added the root of - // the array to the field mask. - if (context.path) { - [context.fieldMask addObject:context.path]; - } - return [self parseScalarValue:input context:context]; - } -} - -/** - * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). - * - * Note that it handles all NSNumber values that are encodable as int64_t or doubles - * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though - * any value outside what is representable by int64_t (a signed 64-bit value) will throw an - * exception. - * - * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be - * included in the resulting parsed data. - */ -- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { - if (!input || [input isMemberOfClass:[NSNull class]]) { - return [FSTNullValue nullValue]; - - } else if ([input isKindOfClass:[NSNumber class]]) { - // Recover the underlying type of the number, using the method described here: - // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber - const char *cType = [input objCType]; - - // Type Encoding values taken from - // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ - // Articles/ocrtTypeEncodings.html - switch (cType[0]) { - case 'q': - return [FSTIntegerValue integerValue:[input longLongValue]]; - - case 'i': // Falls through. - case 's': // Falls through. - case 'l': // Falls through. - case 'I': // Falls through. - case 'S': - // Coerce integer values that aren't long long. Allow unsigned integer types that are - // guaranteed small enough to skip a length check. - return [FSTIntegerValue integerValue:[input longLongValue]]; - - case 'L': // Falls through. - case 'Q': - // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here - // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t. - { - unsigned long long extended = [input unsignedLongLongValue]; - - if (extended > LLONG_MAX) { - FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@", - [input unsignedLongLongValue], [context fieldDescription]); - - } else { - return [FSTIntegerValue integerValue:(int64_t)extended]; - } - } - - case 'f': - return [FSTDoubleValue doubleValue:[input doubleValue]]; - - case 'd': - // Double values are already the right type, so just reuse the existing boxed double. - // - // Note that NSNumber already performs NaN normalization to a single shared instance - // so there's no need to treat NaN specially here. - return [FSTDoubleValue doubleValue:[input doubleValue]]; - - case 'B': // Falls through. - case 'c': // Falls through. - case 'C': - // Boolean values are weird. - // - // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL) - // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that - // legitimate usage of signed chars is impossible, but this should be rare. - // - // Additionally, for consistency, map unsigned chars to bools in the same way. - return [FSTBooleanValue booleanValue:[input boolValue]]; - - default: - // All documented codes should be handled above, so this shouldn't happen. - FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input); - } - - } else if ([input isKindOfClass:[NSString class]]) { - return [FSTStringValue stringValue:input]; - - } else if ([input isKindOfClass:[NSDate class]]) { - return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]]; - - } else if ([input isKindOfClass:[FIRGeoPoint class]]) { - return [FSTGeoPointValue geoPointValue:input]; - - } else if ([input isKindOfClass:[NSData class]]) { - return [FSTBlobValue blobValue:input]; - - } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) { - FSTDocumentKeyReference *reference = input; - if (![reference.databaseID isEqual:self.databaseID]) { - FSTDatabaseID *other = reference.databaseID; - FSTThrowInvalidArgument( - @"Document Reference is for database %@/%@ but should be for database %@/%@%@", - other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID, - [context fieldDescription]); - } - return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; - - } else if ([input isKindOfClass:[FIRFieldValue class]]) { - if ([input isKindOfClass:[FSTDeleteFieldValue class]]) { - if (context.dataSource == FSTUserDataSourceMergeSet) { - return nil; - } else if (context.dataSource == FSTUserDataSourceUpdate) { - FSTAssert(context.path.length > 0, - @"FieldValue.delete() at the top level should have already been handled."); - FSTThrowInvalidArgument( - @"FieldValue.delete() can only appear at the top level of your " - "update data%@", - [context fieldDescription]); - } else { - // We shouldn't encounter delete sentinels for queries or non-merge setData calls. - FSTThrowInvalidArgument( - @"FieldValue.delete() can only be used with updateData() and setData() with " - @"SetOptions.merge()."); - } - } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) { - if (![context isWrite]) { - FSTThrowInvalidArgument( - @"FieldValue.serverTimestamp() can only be used with setData() and updateData()."); - } - if (!context.path) { - FSTThrowInvalidArgument( - @"FieldValue.serverTimestamp() is not currently supported inside arrays%@", - [context fieldDescription]); - } - [context.fieldTransforms - addObject:[[FSTFieldTransform alloc] - initWithPath:context.path - transform:[FSTServerTimestampTransform serverTimestampTransform]]]; - - // Return nil so this value is omitted from the parsed result. - return nil; - } else { - FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class])); - } - - } else { - FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]), - [context fieldDescription]); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm new file mode 100644 index 0000000..414aadb --- /dev/null +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -0,0 +1,598 @@ +/* + * 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/API/FSTUserDataConverter.h" + +#import "FIRGeoPoint.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFieldValue+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSetOptions+Internal.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const RESERVED_FIELD_DESIGNATOR = @"__"; + +#pragma mark - FSTParsedSetData + +@implementation FSTParsedSetData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(nullable FSTFieldMask *)fieldMask + fieldTransforms:(NSArray *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray *mutations = [NSMutableArray array]; + if (self.fieldMask) { + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + } else { + [mutations addObject:[[FSTSetMutation alloc] initWithKey:key + value:self.data + precondition:precondition]]; + } + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +#pragma mark - FSTParsedUpdateData + +@implementation FSTParsedUpdateData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray *mutations = [NSMutableArray array]; + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +/** + * Represents what type of API method provided the data being parsed; useful for determining which + * error conditions apply during parsing and providing better error messages. + */ +typedef NS_ENUM(NSInteger, FSTUserDataSource) { + FSTUserDataSourceSet, + FSTUserDataSourceMergeSet, + FSTUserDataSourceUpdate, + FSTUserDataSourceQueryValue, // from a where clause or cursor bound. +}; + +#pragma mark - FSTParseContext + +/** + * A "context" object passed around while parsing user data. + */ +@interface FSTParseContext : NSObject +/** The current path being parsed. */ +// TODO(b/34871131): path should never be nil, but we don't support array paths right now. +@property(nonatomic, strong, readonly, nullable) FSTFieldPath *path; + +/** Whether or not this context corresponds to an element of an array. */ +@property(nonatomic, assign, readonly, getter=isArrayElement) BOOL arrayElement; + +/** + * What type of API method provided the data being parsed; useful for determining which error + * conditions apply during parsing and providing better error messages. + */ +@property(nonatomic, assign) FSTUserDataSource dataSource; +@property(nonatomic, strong, readonly) NSMutableArray *fieldTransforms; +@property(nonatomic, strong, readonly) NSMutableArray *fieldMask; + +- (instancetype)init NS_UNAVAILABLE; +/** + * Initializes a FSTParseContext with the given source and path. + * + * @param dataSource Indicates what kind of API method this data came from. + * @param path A path within the object being parsed. This could be an empty path (in which case + * the context represents the root of the data being parsed), or a nonempty path (indicating the + * context represents a nested location within the data). + * + * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate + * the context represents any location within an array (in which case certain features will not work + * and errors will be somewhat compromised). + */ +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + arrayElement:(BOOL)arrayElement + fieldTransforms:(NSMutableArray *)fieldTransforms + fieldMask:(NSMutableArray *)fieldMask + NS_DESIGNATED_INITIALIZER; + +// Helpers to get a FSTParseContext for a child field. +- (instancetype)contextForField:(NSString *)fieldName; +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; +- (instancetype)contextForArrayIndex:(NSUInteger)index; + +/** Returns true for the non-query parse contexts (Set, MergeSet and Update) */ +- (BOOL)isWrite; +@end + +@implementation FSTParseContext + ++ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path { + FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource + path:path + arrayElement:NO + fieldTransforms:[NSMutableArray array] + fieldMask:[NSMutableArray array]]; + [context validatePath]; + return context; +} + +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + arrayElement:(BOOL)arrayElement + fieldTransforms:(NSMutableArray *)fieldTransforms + fieldMask:(NSMutableArray *)fieldMask { + if (self = [super init]) { + _dataSource = dataSource; + _path = path; + _arrayElement = arrayElement; + _fieldTransforms = fieldTransforms; + _fieldMask = fieldMask; + } + return self; +} + +- (instancetype)contextForField:(NSString *)fieldName { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingSegment:fieldName] + arrayElement:NO + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePathSegment:fieldName]; + return context; +} + +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingPath:fieldPath] + arrayElement:NO + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePath]; + return context; +} + +- (instancetype)contextForArrayIndex:(NSUInteger)index { + // TODO(b/34871131): We don't support array paths right now; so make path nil. + return [[FSTParseContext alloc] initWithSource:self.dataSource + path:nil + arrayElement:YES + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; +} + +/** + * Returns a string that can be appended to error messages indicating what field caused the error. + */ +- (NSString *)fieldDescription { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (!self.path || self.path.empty) { + return @""; + } else { + return [NSString stringWithFormat:@" (found in field %@)", self.path]; + } +} + +- (BOOL)isWrite { + switch (self.dataSource) { + case FSTUserDataSourceSet: // Falls through. + case FSTUserDataSourceMergeSet: // Falls through. + case FSTUserDataSourceUpdate: + return YES; + case FSTUserDataSourceQueryValue: + return NO; + default: + FSTThrowInvalidArgument(@"Unexpected case for FSTUserDataSource: %d", self.dataSource); + } +} + +- (void)validatePath { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (self.path == nil) { + return; + } + for (int i = 0; i < self.path.length; i++) { + [self validatePathSegment:[self.path segmentAtIndex:i]]; + } +} + +- (void)validatePathSegment:(NSString *)segment { + if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] && + [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) { + FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@", + RESERVED_FIELD_DESIGNATOR, [self fieldDescription]); + } +} + +@end + +#pragma mark - FSTDocumentKeyReference + +@implementation FSTDocumentKeyReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = key; + _databaseID = databaseID; + } + return self; +} + +@end + +#pragma mark - FSTUserDataConverter + +@interface FSTUserDataConverter () +@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID; +@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter; +@end + +@implementation FSTUserDataConverter + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + preConverter:(FSTPreConverterBlock)preConverter { + self = [super init]; + if (self) { + _databaseID = databaseID; + _preConverter = preConverter; + } + return self; +} + +- (FSTParsedSetData *)parsedMergeData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceMergeSet path:[FSTFieldPath emptyPath]]; + FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + + return + [[FSTParsedSetData alloc] initWithData:updateData + fieldMask:[[FSTFieldMask alloc] initWithFields:context.fieldMask] + fieldTransforms:context.fieldTransforms]; +} + +- (FSTParsedSetData *)parsedSetData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]]; + FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + + return [[FSTParsedSetData alloc] initWithData:updateData + fieldMask:nil + fieldTransforms:context.fieldTransforms]; +} + +- (FSTParsedUpdateData *)parsedUpdateData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + NSDictionary *dict = input; + + NSMutableArray *fieldMaskPaths = [NSMutableArray array]; + __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + FSTFieldPath *path; + + if ([key isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue; + } else if ([key isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)key).internalValue; + } else { + FSTThrowInvalidArgument( + @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."); + } + + value = self.preConverter(value); + if ([value isKindOfClass:[FSTDeleteFieldValue class]]) { + // Add it to the field mask, but don't add anything to updateData. + [fieldMaskPaths addObject:path]; + } else { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForFieldPath:path]]; + if (parsedValue) { + [fieldMaskPaths addObject:path]; + updateData = [updateData objectBySettingValue:parsedValue forPath:path]; + } + } + }]; + + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths]; + return [[FSTParsedUpdateData alloc] initWithData:updateData + fieldMask:mask + fieldTransforms:context.fieldTransforms]; +} + +- (FSTFieldValue *)parsedQueryValue:(id)input { + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]]; + FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; + FSTAssert(parsed, @"Parsed data should not be nil."); + FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed."); + return parsed; +} + +/** + * Internal helper for parsing user data. + * + * @param input Data to be parsed. + * @param context A context object representing the current path being parsed, the source of the + * data being parsed, etc. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { + input = self.preConverter(input); + if ([input isKindOfClass:[NSArray class]]) { + // TODO(b/34871131): Include the path containing the array in the error message. + if (context.isArrayElement) { + FSTThrowInvalidArgument(@"Nested arrays are not supported"); + } + NSArray *array = input; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:array.count]; + [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { + FSTFieldValue *_Nullable parsedEntry = + [self parseData:entry context:[context contextForArrayIndex:idx]]; + if (!parsedEntry) { + // Just include nulls in the array for fields being replaced with a sentinel. + parsedEntry = [FSTNullValue nullValue]; + } + [result addObject:parsedEntry]; + }]; + // If context.path is nil we are already inside an array and we don't support field mask paths + // more granular than the top-level array. + if (context.path) { + [context.fieldMask addObject:context.path]; + } + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; + + } else if ([input isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = input; + NSMutableDictionary *result = + [NSMutableDictionary dictionaryWithCapacity:dict.count]; + [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForField:key]]; + if (parsedValue) { + result[key] = parsedValue; + } + }]; + return [[FSTObjectValue alloc] initWithDictionary:result]; + + } else { + // If context.path is null, we are inside an array and we should have already added the root of + // the array to the field mask. + if (context.path) { + [context.fieldMask addObject:context.path]; + } + return [self parseScalarValue:input context:context]; + } +} + +/** + * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). + * + * Note that it handles all NSNumber values that are encodable as int64_t or doubles + * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though + * any value outside what is representable by int64_t (a signed 64-bit value) will throw an + * exception. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { + if (!input || [input isMemberOfClass:[NSNull class]]) { + return [FSTNullValue nullValue]; + + } else if ([input isKindOfClass:[NSNumber class]]) { + // Recover the underlying type of the number, using the method described here: + // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber + const char *cType = [input objCType]; + + // Type Encoding values taken from + // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ + // Articles/ocrtTypeEncodings.html + switch (cType[0]) { + case 'q': + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'i': // Falls through. + case 's': // Falls through. + case 'l': // Falls through. + case 'I': // Falls through. + case 'S': + // Coerce integer values that aren't long long. Allow unsigned integer types that are + // guaranteed small enough to skip a length check. + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'L': // Falls through. + case 'Q': + // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here + // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t. + { + unsigned long long extended = [input unsignedLongLongValue]; + + if (extended > LLONG_MAX) { + FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@", + [input unsignedLongLongValue], [context fieldDescription]); + + } else { + return [FSTIntegerValue integerValue:(int64_t)extended]; + } + } + + case 'f': + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'd': + // Double values are already the right type, so just reuse the existing boxed double. + // + // Note that NSNumber already performs NaN normalization to a single shared instance + // so there's no need to treat NaN specially here. + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'B': // Falls through. + case 'c': // Falls through. + case 'C': + // Boolean values are weird. + // + // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL) + // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that + // legitimate usage of signed chars is impossible, but this should be rare. + // + // Additionally, for consistency, map unsigned chars to bools in the same way. + return [FSTBooleanValue booleanValue:[input boolValue]]; + + default: + // All documented codes should be handled above, so this shouldn't happen. + FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input); + } + + } else if ([input isKindOfClass:[NSString class]]) { + return [FSTStringValue stringValue:input]; + + } else if ([input isKindOfClass:[NSDate class]]) { + return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]]; + + } else if ([input isKindOfClass:[FIRGeoPoint class]]) { + return [FSTGeoPointValue geoPointValue:input]; + + } else if ([input isKindOfClass:[NSData class]]) { + return [FSTBlobValue blobValue:input]; + + } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) { + FSTDocumentKeyReference *reference = input; + if (![reference.databaseID isEqual:self.databaseID]) { + FSTDatabaseID *other = reference.databaseID; + FSTThrowInvalidArgument( + @"Document Reference is for database %@/%@ but should be for database %@/%@%@", + other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID, + [context fieldDescription]); + } + return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; + + } else if ([input isKindOfClass:[FIRFieldValue class]]) { + if ([input isKindOfClass:[FSTDeleteFieldValue class]]) { + if (context.dataSource == FSTUserDataSourceMergeSet) { + return nil; + } else if (context.dataSource == FSTUserDataSourceUpdate) { + FSTAssert(context.path.length > 0, + @"FieldValue.delete() at the top level should have already been handled."); + FSTThrowInvalidArgument( + @"FieldValue.delete() can only appear at the top level of your " + "update data%@", + [context fieldDescription]); + } else { + // We shouldn't encounter delete sentinels for queries or non-merge setData calls. + FSTThrowInvalidArgument( + @"FieldValue.delete() can only be used with updateData() and setData() with " + @"SetOptions.merge()."); + } + } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) { + if (![context isWrite]) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() can only be used with setData() and updateData()."); + } + if (!context.path) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() is not currently supported inside arrays%@", + [context fieldDescription]); + } + [context.fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:context.path + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + + // Return nil so this value is omitted from the parsed result. + return nil; + } else { + FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class])); + } + + } else { + FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]), + [context fieldDescription]); + } +} + +@end + +NS_ASSUME_NONNULL_END -- cgit v1.2.3