aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source/API
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Source/API
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (diff)
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0 Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Source/API')
-rw-r--r--Firestore/Source/API/FIRCollectionReference+Internal.h28
-rw-r--r--Firestore/Source/API/FIRCollectionReference.m113
-rw-r--r--Firestore/Source/API/FIRDocumentChange+Internal.h32
-rw-r--r--Firestore/Source/API/FIRDocumentChange.m129
-rw-r--r--Firestore/Source/API/FIRDocumentReference+Internal.h34
-rw-r--r--Firestore/Source/API/FIRDocumentReference.m285
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot+Internal.h37
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot.m175
-rw-r--r--Firestore/Source/API/FIRFieldPath+Internal.h39
-rw-r--r--Firestore/Source/API/FIRFieldPath.m101
-rw-r--r--Firestore/Source/API/FIRFieldValue+Internal.h37
-rw-r--r--Firestore/Source/API/FIRFieldValue.m96
-rw-r--r--Firestore/Source/API/FIRFirestore+Internal.h64
-rw-r--r--Firestore/Source/API/FIRFirestore.m284
-rw-r--r--Firestore/Source/API/FIRFirestoreSettings.m92
-rw-r--r--Firestore/Source/API/FIRFirestoreVersion.h22
-rw-r--r--Firestore/Source/API/FIRFirestoreVersion.m29
-rw-r--r--Firestore/Source/API/FIRGeoPoint+Internal.h26
-rw-r--r--Firestore/Source/API/FIRGeoPoint.m85
-rw-r--r--Firestore/Source/API/FIRListenerRegistration+Internal.h34
-rw-r--r--Firestore/Source/API/FIRListenerRegistration.m57
-rw-r--r--Firestore/Source/API/FIRQuery+Internal.h29
-rw-r--r--Firestore/Source/API/FIRQuery.m520
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot+Internal.h37
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot.m125
-rw-r--r--Firestore/Source/API/FIRQuery_Init.h32
-rw-r--r--Firestore/Source/API/FIRSetOptions+Internal.h33
-rw-r--r--Firestore/Source/API/FIRSetOptions.m65
-rw-r--r--Firestore/Source/API/FIRSnapshotMetadata+Internal.h29
-rw-r--r--Firestore/Source/API/FIRSnapshotMetadata.m49
-rw-r--r--Firestore/Source/API/FIRTransaction+Internal.h27
-rw-r--r--Firestore/Source/API/FIRTransaction.m147
-rw-r--r--Firestore/Source/API/FIRWriteBatch+Internal.h25
-rw-r--r--Firestore/Source/API/FIRWriteBatch.m116
-rw-r--r--Firestore/Source/API/FSTUserDataConverter.h124
-rw-r--r--Firestore/Source/API/FSTUserDataConverter.m568
36 files changed, 3725 insertions, 0 deletions
diff --git a/Firestore/Source/API/FIRCollectionReference+Internal.h b/Firestore/Source/API/FIRCollectionReference+Internal.h
new file mode 100644
index 0000000..1d00cbb
--- /dev/null
+++ b/Firestore/Source/API/FIRCollectionReference+Internal.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "FIRCollectionReference.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTResourcePath;
+
+/** Internal FIRCollectionReference API we don't want exposed in our public header files. */
+@interface FIRCollectionReference (Internal)
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRCollectionReference.m b/Firestore/Source/API/FIRCollectionReference.m
new file mode 100644
index 0000000..1ded4d2
--- /dev/null
+++ b/Firestore/Source/API/FIRCollectionReference.m
@@ -0,0 +1,113 @@
+/*
+ * 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 "FIRCollectionReference.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRQuery+Internal.h"
+#import "FIRQuery_Init.h"
+#import "FSTAssert.h"
+#import "FSTDocumentKey.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTUsageValidation.h"
+#import "FSTUtil.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRCollectionReference ()
+- (instancetype)initWithPath:(FSTResourcePath *)path
+ firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+
+// Mark the super class designated initializer unavailable.
+- (instancetype)initWithQuery:(FSTQuery *)query
+ firestore:(FIRFirestore *)firestore
+ __attribute__((unavailable("Use the initWithPath constructor of FIRCollectionReference.")));
+@end
+
+@implementation FIRCollectionReference (Internal)
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore {
+ return [[FIRCollectionReference alloc] initWithPath:path firestore:firestore];
+}
+@end
+
+@implementation FIRCollectionReference
+
+- (instancetype)initWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore {
+ if (path.length % 2 != 1) {
+ FSTThrowInvalidArgument(
+ @"Invalid collection reference. Collection references must have an odd "
+ "number of segments, but %@ has %d",
+ path.canonicalString, path.length);
+ }
+ self = [super initWithQuery:[FSTQuery queryWithPath:path] firestore:firestore];
+ return self;
+}
+
+// Override the designated initializer from the super class.
+- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
+ FSTFail(@"Use FIRCollectionReference initWithPath: initializer.");
+}
+
+- (NSString *)collectionID {
+ return [self.query.path lastSegment];
+}
+
+- (FIRDocumentReference *_Nullable)parent {
+ FSTResourcePath *parentPath = [self.query.path pathByRemovingLastSegment];
+ if (parentPath.isEmpty) {
+ return nil;
+ } else {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:parentPath];
+ return [FIRDocumentReference referenceWithKey:key firestore:self.firestore];
+ }
+}
+
+- (NSString *)path {
+ return [self.query.path canonicalString];
+}
+
+- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath {
+ if (!documentPath) {
+ FSTThrowInvalidArgument(@"Document path cannot be nil.");
+ }
+ FSTResourcePath *subPath = [FSTResourcePath pathWithString:documentPath];
+ FSTResourcePath *path = [self.query.path pathByAppendingPath:subPath];
+ return [FIRDocumentReference referenceWithPath:path firestore:self.firestore];
+}
+
+- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data {
+ return [self addDocumentWithData:data completion:nil];
+}
+
+- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data
+ completion:
+ (nullable void (^)(NSError *_Nullable error))completion {
+ FIRDocumentReference *docRef = [self documentWithAutoID];
+ [docRef setData:data completion:completion];
+ return docRef;
+}
+
+- (FIRDocumentReference *)documentWithAutoID {
+ NSString *autoID = [FSTUtil autoID];
+ FSTDocumentKey *key =
+ [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:autoID]];
+ return [FIRDocumentReference referenceWithKey:key firestore:self.firestore];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentChange+Internal.h b/Firestore/Source/API/FIRDocumentChange+Internal.h
new file mode 100644
index 0000000..7e2e5c6
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentChange+Internal.h
@@ -0,0 +1,32 @@
+/*
+ * 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"
+
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRDocumentChange API we don't want exposed in our public header files. */
+@interface FIRDocumentChange (Internal)
+
+/** Calculates the array of FIRDocumentChange's based on the given FSTViewSnapshot. */
++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot
+ firestore:(FIRFirestore *)firestore;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m
new file mode 100644
index 0000000..f284bfe
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentChange.m
@@ -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 "FIRDocumentSnapshot+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentSet.h"
+#import "FSTQuery.h"
+#import "FSTViewSnapshot.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<FIRDocumentChange *> *)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<FIRDocumentChange *> *changes = [NSMutableArray array];
+ for (FSTDocumentViewChange *change in snapshot.documentChanges) {
+ FIRDocumentSnapshot *document =
+ [FIRDocumentSnapshot 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<FIRDocumentChange *> *changes = [NSMutableArray array];
+ for (FSTDocumentViewChange *change in snapshot.documentChanges) {
+ FIRDocumentSnapshot *document =
+ [FIRDocumentSnapshot 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:(FIRDocumentSnapshot *)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+Internal.h b/Firestore/Source/API/FIRDocumentReference+Internal.h
new file mode 100644
index 0000000..5e12ddc
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentReference+Internal.h
@@ -0,0 +1,34 @@
+/*
+ * 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"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTDocumentKey;
+@class FSTResourcePath;
+
+/** Internal FIRDocumentReference API we don't want exposed in our public header files. */
+@interface FIRDocumentReference (Internal)
+
++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore;
++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore;
+
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m
new file mode 100644
index 0000000..5515bd6
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentReference.m
@@ -0,0 +1,285 @@
+/*
+ * 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 "FIRCollectionReference+Internal.h"
+#import "FIRDocumentReference+Internal.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRFirestoreErrors.h"
+#import "FIRListenerRegistration+Internal.h"
+#import "FIRSetOptions+Internal.h"
+#import "FIRSnapshotMetadata.h"
+#import "FSTAssert.h"
+#import "FSTAsyncQueryListener.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentSet.h"
+#import "FSTEventManager.h"
+#import "FSTFieldValue.h"
+#import "FSTFirestoreClient.h"
+#import "FSTMutation.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FIRDocumentListenOptions
+
+@interface FIRDocumentListenOptions ()
+
+- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges
+ NS_DESIGNATED_INITIALIZER;
+
+@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;
+}
+
+- (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<NSString *, id> *)documentData {
+ return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData options:(FIRSetOptions *)options {
+ return [self setData:documentData options:options completion:nil];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData
+ completion:(nullable void (^)(NSError *_Nullable error))completion {
+ return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion];
+}
+
+- (void)setData:(NSDictionary<NSString *, id> *)documentData
+ options:(FIRSetOptions *)options
+ completion:(nullable void (^)(NSError *_Nullable error))completion {
+ FSTParsedSetData *parsed =
+ [self.firestore.dataConverter parsedSetData:documentData options:options];
+ return [self.firestore.client
+ writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]]
+ completion:completion];
+}
+
+- (void)updateData:(NSDictionary<id, id> *)fields {
+ return [self updateData:fields completion:nil];
+}
+
+- (void)updateData:(NSDictionary<id, id> *)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<FIRListenerRegistration> 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<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener {
+ return [self addSnapshotListenerWithOptions:nil listener:listener];
+}
+
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:
+ (nullable FIRDocumentListenOptions *)options
+ listener:(FIRDocumentSnapshotBlock)listener {
+ return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options]
+ listener:listener];
+}
+
+- (id<FIRListenerRegistration>)
+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+Internal.h b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h
new file mode 100644
index 0000000..f2776f0
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h
@@ -0,0 +1,37 @@
+/*
+ * 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"
+
+@class FIRFirestore;
+@class FSTDocument;
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRDocumentSnapshot API we don't want exposed in our public header files. */
+@interface FIRDocumentSnapshot (Internal)
+
++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore
+ documentKey:(FSTDocumentKey *)documentKey
+ document:(nullable FSTDocument *)document
+ fromCache:(BOOL)fromCache;
+
+@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m
new file mode 100644
index 0000000..b5f61ba
--- /dev/null
+++ b/Firestore/Source/API/FIRDocumentSnapshot.m
@@ -0,0 +1,175 @@
+/*
+ * 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 "FIRDocumentReference+Internal.h"
+#import "FIRFieldPath+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRSnapshotMetadata+Internal.h"
+#import "FSTDatabaseID.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTPath.h"
+#import "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 [[FIRDocumentSnapshot 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;
+}
+
+@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;
+}
+
+- (NSDictionary<NSString *, id> *)data {
+ FSTDocument *document = self.internalDocument;
+
+ if (!document) {
+ FSTThrowInvalidUsage(
+ @"NonExistentDocumentException",
+ @"Document '%@' doesn't exist. "
+ @"Check document.exists to make sure the document exists before calling document.data.",
+ self.internalKey);
+ }
+
+ return [self convertedObject:[self.internalDocument data]];
+}
+
+- (nullable id)objectForKeyedSubscript:(id)key {
+ FIRFieldPath *fieldPath;
+
+ if ([key isKindOfClass:[NSString class]]) {
+ fieldPath = [FIRFieldPath pathWithDotSeparatedString:key];
+ } else if ([key isKindOfClass:[FIRFieldPath class]]) {
+ fieldPath = key;
+ } else {
+ FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath.");
+ }
+
+ FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue];
+ return [self convertedValue:fieldValue];
+}
+
+- (id)convertedValue:(FSTFieldValue *)value {
+ if ([value isKindOfClass:[FSTObjectValue class]]) {
+ return [self convertedObject:(FSTObjectValue *)value];
+ } else if ([value isKindOfClass:[FSTArrayValue class]]) {
+ return [self convertedArray:(FSTArrayValue *)value];
+ } 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.value firestore:self.firestore];
+ } else {
+ return value.value;
+ }
+}
+
+- (NSDictionary<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue {
+ NSMutableDictionary *result = [NSMutableDictionary dictionary];
+ [objectValue.internalValue
+ enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) {
+ result[key] = [self convertedValue:value];
+ }];
+ return result;
+}
+
+- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue {
+ NSArray<FSTFieldValue *> *internalValue = arrayValue.internalValue;
+ NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count];
+ [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) {
+ [result addObject:[self convertedValue:value]];
+ }];
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldPath+Internal.h b/Firestore/Source/API/FIRFieldPath+Internal.h
new file mode 100644
index 0000000..227cdad
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldPath+Internal.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "FIRFieldPath.h"
+
+@class FSTFieldPath;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFieldPath ()
+
+- (instancetype)initPrivate:(FSTFieldPath *)path NS_DESIGNATED_INITIALIZER;
+
+/** Internal field path representation */
+@property(nonatomic, strong, readonly) FSTFieldPath *internalValue;
+
+@end
+
+/** Internal FIRFieldPath API we don't want exposed in our public header files. */
+@interface FIRFieldPath (Internal)
+
++ (instancetype)pathWithDotSeparatedString:(NSString *)path;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m
new file mode 100644
index 0000000..b3c919c
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldPath.m
@@ -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 "FIRFieldPath+Internal.h"
+
+#import "FSTPath.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRFieldPath
+
+- (instancetype)initWithFields:(NSArray<NSString *> *)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:(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+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h
new file mode 100644
index 0000000..1b4a99c
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldValue+Internal.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "FIRFieldValue.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FIRFieldValue class for field deletes. Exposed internally so code can do isKindOfClass checks on
+ * it.
+ */
+@interface FSTDeleteFieldValue : FIRFieldValue
+- (instancetype)init NS_UNAVAILABLE;
+@end
+
+/**
+ * FIRFieldValue class for server timestamps. Exposed internally so code can do isKindOfClass checks
+ * on it.
+ */
+@interface FSTServerTimestampFieldValue : FIRFieldValue
+- (instancetype)init NS_UNAVAILABLE;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFieldValue.m b/Firestore/Source/API/FIRFieldValue.m
new file mode 100644
index 0000000..a44d8fa
--- /dev/null
+++ b/Firestore/Source/API/FIRFieldValue.m
@@ -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 "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+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h
new file mode 100644
index 0000000..08f5266
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestore+Internal.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "FIRFirestoreSwiftNameSupport.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FSTDatabaseID;
+@class FSTDispatchQueue;
+@class FSTFirestoreClient;
+@class FSTUserDataConverter;
+@protocol FSTCredentialsProvider;
+
+@interface FIRFirestore (/* Init */)
+
+/**
+ * Initializes a Firestore object with all the required parameters directly. This exists so that
+ * tests can create FIRFirestore objects without needing FIRApp.
+ */
+- (instancetype)initWithProjectID:(NSString *)projectID
+ database:(NSString *)database
+ persistenceKey:(NSString *)persistenceKey
+ credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ firebaseApp:(FIRApp *)app;
+
+@end
+
+/** Internal FIRFirestore API we don't want exposed in our public header files. */
+@interface FIRFirestore (Internal)
+
+/** Checks to see if logging is is globally enabled for the Firestore client. */
++ (BOOL)isLoggingEnabled;
+
+/**
+ * Shutdown this `FIRFirestore`, releasing all resources (abandoning any outstanding writes,
+ * removing all listens, closing all network connections, etc.).
+ *
+ * @param completion A block to execute once everything has shut down.
+ */
+- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion
+ FIR_SWIFT_NAME(shutdown(completion:));
+
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+@property(nonatomic, strong, readonly) FSTFirestoreClient *client;
+@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m
new file mode 100644
index 0000000..e8c4fa6
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestore.m
@@ -0,0 +1,284 @@
+/*
+ * 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 <FirebaseCommunity/FIRApp.h>
+#import <FirebaseCommunity/FIRLogger.h>
+#import <FirebaseCommunity/FIROptions.h>
+
+#import "FIRCollectionReference+Internal.h"
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRFirestoreSettings.h"
+#import "FIRTransaction+Internal.h"
+#import "FIRWriteBatch+Internal.h"
+#import "FSTUserDataConverter.h"
+
+#import "FSTAssert.h"
+#import "FSTCredentialsProvider.h"
+#import "FSTDatabaseID.h"
+#import "FSTDatabaseInfo.h"
+#import "FSTDispatchQueue.h"
+#import "FSTDocumentKey.h"
+#import "FSTFirestoreClient.h"
+#import "FSTLogger.h"
+#import "FSTPath.h"
+#import "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<FSTCredentialsProvider> credentialsProvider;
+@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue;
+
+@property(nonatomic, strong) FSTFirestoreClient *client;
+@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter;
+
+@end
+
+@implementation FIRFirestore {
+ FIRFirestoreSettings *_settings;
+}
+
++ (NSMutableDictionary<NSString *, FIRFirestore *> *)instances {
+ static dispatch_once_t token = 0;
+ static NSMutableDictionary<NSString *, FIRFirestore *> *instances;
+ dispatch_once(&token, ^{
+ instances = [NSMutableDictionary dictionary];
+ });
+ return instances;
+}
+
++ (instancetype)firestore {
+ FIRApp *app = [FIRApp defaultApp];
+ if (!app) {
+ FSTThrowInvalidUsage(
+ @"FIRAppNotConfiguredException",
+ @"Failed to get FIRApp instance. Please call FIRApp.configure() before using FIRFirestore");
+ }
+ 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<NSString *, FIRFirestore *> *instances = self.instances;
+ @synchronized(instances) {
+ FIRFirestore *firestore = instances[key];
+ if (!firestore) {
+ NSString *projectID = app.options.projectID;
+ FSTAssert(projectID, @"FIROptions.projectID cannot be nil.");
+
+ FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)];
+
+ id<FSTCredentialsProvider> 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<FSTCredentialsProvider>)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 {
+ // Disallow mutation of our internal settings
+ return [_settings copy];
+}
+
+- (void)setSettings:(FIRFirestoreSettings *)settings {
+ // 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.
+ * @return self
+ */
+- (instancetype)firestoreWithConfiguredClient {
+ if (!_client) {
+ // These values are validated elsewhere; this is just double-checking:
+ FSTAssert(_settings.host, @"FIRFirestoreSettings.host cannot be nil.");
+ FSTAssert(_settings.dispatchQueue, @"FIRFirestoreSettings.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];
+ }
+ return self;
+}
+
+- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath {
+ if (!collectionPath) {
+ FSTThrowInvalidArgument(@"Collection path cannot be nil.");
+ }
+ FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath];
+ return
+ [FIRCollectionReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient];
+}
+
+- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath {
+ if (!documentPath) {
+ FSTThrowInvalidArgument(@"Document path cannot be nil.");
+ }
+ FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath];
+ return [FIRDocumentReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient];
+}
+
+- (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 firestoreWithConfiguredClient];
+ [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion];
+}
+
+- (FIRWriteBatch *)batch {
+ return [FIRWriteBatch writeBatchWithFirestore:[self firestoreWithConfiguredClient]];
+}
+
+- (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 {
+ if (!self.client) {
+ completion(nil);
+ return;
+ }
+ return [self.client shutdownWithCompletion:completion];
+}
+
++ (BOOL)isLoggingEnabled {
+ return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO);
+}
+
++ (void)enableLogging:(BOOL)logging {
+ FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRFirestoreSettings.m b/Firestore/Source/API/FIRFirestoreSettings.m
new file mode 100644
index 0000000..106a0b5
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestoreSettings.m
@@ -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 "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.h b/Firestore/Source/API/FIRFirestoreVersion.h
new file mode 100644
index 0000000..6fb21eb
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestoreVersion.h
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/** Version for Firestore. */
+
+#import <Foundation/Foundation.h>
+
+/** Version string for the Firebase Firestore SDK. */
+FOUNDATION_EXPORT const unsigned char *const FirebaseFirestoreVersionString;
diff --git a/Firestore/Source/API/FIRFirestoreVersion.m b/Firestore/Source/API/FIRFirestoreVersion.m
new file mode 100644
index 0000000..4f8bb28
--- /dev/null
+++ b/Firestore/Source/API/FIRFirestoreVersion.m
@@ -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
+
+const unsigned char *const FirebaseFirestoreVersionString =
+ (const unsigned char *const)STR(FIRFirestore_VERSION);
diff --git a/Firestore/Source/API/FIRGeoPoint+Internal.h b/Firestore/Source/API/FIRGeoPoint+Internal.h
new file mode 100644
index 0000000..6eb8548
--- /dev/null
+++ b/Firestore/Source/API/FIRGeoPoint+Internal.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "FIRGeoPoint.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRGeoPoint API we don't want exposed in our public header files. */
+@interface FIRGeoPoint (Internal)
+- (NSComparisonResult)compare:(FIRGeoPoint *)other;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRGeoPoint.m b/Firestore/Source/API/FIRGeoPoint.m
new file mode 100644
index 0000000..a50cf37
--- /dev/null
+++ b/Firestore/Source/API/FIRGeoPoint.m
@@ -0,0 +1,85 @@
+/*
+ * 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 "FIRGeoPoint+Internal.h"
+
+#import "FSTComparison.h"
+#import "FSTUsageValidation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRGeoPoint
+
+- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude {
+ if (self = [super init]) {
+ if (latitude < -90 || latitude > 90 || !isfinite(latitude)) {
+ FSTThrowInvalidArgument(
+ @"GeoPoint requires a latitude value in the range of [-90, 90], "
+ "but was %f",
+ latitude);
+ }
+ if (longitude < -180 || longitude > 180 || !isfinite(longitude)) {
+ FSTThrowInvalidArgument(
+ @"GeoPoint requires a longitude value in the range of [-180, 180], "
+ "but was %f",
+ longitude);
+ }
+
+ _latitude = latitude;
+ _longitude = longitude;
+ }
+ return self;
+}
+
+- (NSComparisonResult)compare:(FIRGeoPoint *)other {
+ NSComparisonResult result = FSTCompareDoubles(self.latitude, other.latitude);
+ if (result != NSOrderedSame) {
+ return result;
+ } else {
+ return FSTCompareDoubles(self.longitude, other.longitude);
+ }
+}
+
+#pragma mark - NSObject methods
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<FIRGeoPoint: (%f, %f)>", self.latitude, self.longitude];
+}
+
+- (BOOL)isEqual:(id)other {
+ if (self == other) {
+ return YES;
+ }
+ if (![other isKindOfClass:[FIRGeoPoint class]]) {
+ return NO;
+ }
+ FIRGeoPoint *otherGeoPoint = (FIRGeoPoint *)other;
+ return FSTDoubleBitwiseEquals(self.latitude, otherGeoPoint.latitude) &&
+ FSTDoubleBitwiseEquals(self.longitude, otherGeoPoint.longitude);
+}
+
+- (NSUInteger)hash {
+ return 31 * FSTDoubleBitwiseHash(self.latitude) + FSTDoubleBitwiseHash(self.longitude);
+}
+
+/** Implements NSCopying without actually copying because geopoints are immutable. */
+- (id)copyWithZone:(NSZone *_Nullable)zone {
+ return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRListenerRegistration+Internal.h b/Firestore/Source/API/FIRListenerRegistration+Internal.h
new file mode 100644
index 0000000..4cd2d57
--- /dev/null
+++ b/Firestore/Source/API/FIRListenerRegistration+Internal.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "FIRListenerRegistration.h"
+
+@class FSTAsyncQueryListener;
+@class FSTFirestoreClient;
+@class FSTQueryListener;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Private implementation of the FIRListenerRegistration protocol. */
+@interface FSTListenerRegistration : NSObject <FIRListenerRegistration>
+
+- (instancetype)initWithClient:(FSTFirestoreClient *)client
+ asyncListener:(FSTAsyncQueryListener *)asyncListener
+ internalListener:(FSTQueryListener *)internalListener;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRListenerRegistration.m b/Firestore/Source/API/FIRListenerRegistration.m
new file mode 100644
index 0000000..9ce0127
--- /dev/null
+++ b/Firestore/Source/API/FIRListenerRegistration.m
@@ -0,0 +1,57 @@
+/*
+ * 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 "FIRListenerRegistration+Internal.h"
+
+#import "FSTAsyncQueryListener.h"
+#import "FSTFirestoreClient.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];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuery+Internal.h b/Firestore/Source/API/FIRQuery+Internal.h
new file mode 100644
index 0000000..3c2b2a7
--- /dev/null
+++ b/Firestore/Source/API/FIRQuery+Internal.h
@@ -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.
+ */
+
+#import "FIRQuery.h"
+
+@class FSTQuery;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRQuery API we don't want exposed in our public header files. */
+@interface FIRQuery (Internal)
++ (FIRQuery *)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore;
+@property(nonatomic, strong, readonly) FSTQuery *query;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m
new file mode 100644
index 0000000..63244fd
--- /dev/null
+++ b/Firestore/Source/API/FIRQuery.m
@@ -0,0 +1,520 @@
+/*
+ * 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+Internal.h"
+#import "FIRDocumentReference.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRFieldPath+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRListenerRegistration+Internal.h"
+#import "FIRQuery+Internal.h"
+#import "FIRQuerySnapshot+Internal.h"
+#import "FIRQuery_Init.h"
+#import "FIRSnapshotMetadata+Internal.h"
+#import "FSTAssert.h"
+#import "FSTAsyncQueryListener.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTEventManager.h"
+#import "FSTFieldValue.h"
+#import "FSTFirestoreClient.h"
+#import "FSTPath.h"
+#import "FSTQuery.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.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 - Public Methods
+
+- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore {
+ if (self = [super init]) {
+ _query = query;
+ _firestore = firestore;
+ }
+ return self;
+}
+
+- (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<FIRListenerRegistration> 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<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener {
+ return [self addSnapshotListenerWithOptions:nil listener:listener];
+}
+
+- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions:
+ (nullable FIRQueryListenOptions *)options
+ listener:(FIRQuerySnapshotBlock)listener {
+ return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options]
+ listener:listener];
+}
+
+- (id<FIRListenerRegistration>)
+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 *)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<FSTFilter> 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<FSTFieldValue *> *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<id> *)fieldValues isBefore:(BOOL)isBefore {
+ // Use explicit sort order because it has to match the query the user made
+ NSArray<FSTSortOrder *> *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<FSTFieldValue *> *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+Internal.h b/Firestore/Source/API/FIRQuerySnapshot+Internal.h
new file mode 100644
index 0000000..3a1e9db
--- /dev/null
+++ b/Firestore/Source/API/FIRQuerySnapshot+Internal.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "FIRQuerySnapshot.h"
+
+@class FIRFirestore;
+@class FIRSnapshotMetadata;
+@class FSTDocumentSet;
+@class FSTQuery;
+@class FSTViewSnapshot;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Internal FIRQuerySnapshot API we don't want exposed in our public header files. */
+@interface FIRQuerySnapshot (Internal)
+
++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore
+ originalQuery:(FSTQuery *)query
+ snapshot:(FSTViewSnapshot *)snapshot
+ metadata:(FIRSnapshotMetadata *)metadata;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m
new file mode 100644
index 0000000..4bf4edf
--- /dev/null
+++ b/Firestore/Source/API/FIRQuerySnapshot.m
@@ -0,0 +1,125 @@
+/*
+ * 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 "FIRQuerySnapshot+Internal.h"
+
+#import "FIRDocumentChange+Internal.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRQuery+Internal.h"
+#import "FIRSnapshotMetadata.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTDocumentSet.h"
+#import "FSTQuery.h"
+#import "FSTViewSnapshot.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<FIRDocumentSnapshot *> *_documents;
+
+ // Cached value of the documentChanges property.
+ NSArray<FIRDocumentChange *> *_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;
+}
+
+@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<FIRDocumentSnapshot *> *)documents {
+ if (!_documents) {
+ FSTDocumentSet *documentSet = self.snapshot.documents;
+ FIRFirestore *firestore = self.firestore;
+ BOOL fromCache = self.metadata.fromCache;
+
+ NSMutableArray<FIRDocumentSnapshot *> *result = [NSMutableArray array];
+ for (FSTDocument *document in documentSet.documentEnumerator) {
+ [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore
+ documentKey:document.key
+ document:document
+ fromCache:fromCache]];
+ }
+
+ _documents = result;
+ }
+ return _documents;
+}
+
+- (NSArray<FIRDocumentChange *> *)documentChanges {
+ if (!_documentChanges) {
+ _documentChanges =
+ [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore];
+ }
+ return _documentChanges;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRQuery_Init.h b/Firestore/Source/API/FIRQuery_Init.h
new file mode 100644
index 0000000..d6b0f37
--- /dev/null
+++ b/Firestore/Source/API/FIRQuery_Init.h
@@ -0,0 +1,32 @@
+/*
+ * 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"
+
+@class FSTQuery;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An Internal class extension for `FIRQuery` that exposes the init method to classes
+ * that need to derive from it.
+ */
+@interface FIRQuery (/*Init*/)
+- (instancetype)initWithQuery:(FSTQuery *)query
+ firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSetOptions+Internal.h b/Firestore/Source/API/FIRSetOptions+Internal.h
new file mode 100644
index 0000000..9118096
--- /dev/null
+++ b/Firestore/Source/API/FIRSetOptions+Internal.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "FIRSetOptions.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSetOptions ()
+
+- (instancetype)initWithMerge:(BOOL)merge NS_DESIGNATED_INITIALIZER;
+
+@end
+
+@interface FIRSetOptions (Internal)
+
++ (instancetype)overwrite;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m
new file mode 100644
index 0000000..ea68c63
--- /dev/null
+++ b/Firestore/Source/API/FIRSetOptions.m
@@ -0,0 +1,65 @@
+/*
+ * 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 "FIRSetOptions+Internal.h"
+#import "FSTMutation.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+Internal.h b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h
new file mode 100644
index 0000000..d3265cd
--- /dev/null
+++ b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h
@@ -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.
+ */
+
+#import "FIRSnapshotMetadata.h"
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRSnapshotMetadata (Internal)
+
++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m
new file mode 100644
index 0000000..ff49d8f
--- /dev/null
+++ b/Firestore/Source/API/FIRSnapshotMetadata.m
@@ -0,0 +1,49 @@
+/*
+ * 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 "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;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FIRTransaction+Internal.h b/Firestore/Source/API/FIRTransaction+Internal.h
new file mode 100644
index 0000000..8fd3f65
--- /dev/null
+++ b/Firestore/Source/API/FIRTransaction+Internal.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "FIRTransaction.h"
+
+@class FIRFirestore;
+@class FSTTransaction;
+
+@interface FIRTransaction (Internal)
+
++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction
+ firestore:(FIRFirestore *)firestore;
+
+@end
diff --git a/Firestore/Source/API/FIRTransaction.m b/Firestore/Source/API/FIRTransaction.m
new file mode 100644
index 0000000..02006bb
--- /dev/null
+++ b/Firestore/Source/API/FIRTransaction.m
@@ -0,0 +1,147 @@
+/*
+ * 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 "FIRTransaction+Internal.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRDocumentSnapshot+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRSetOptions+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDocument.h"
+#import "FSTTransaction.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.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<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document {
+ return [self setData:data forDocument:document options:[FIRSetOptions overwrite]];
+}
+
+- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ options:(FIRSetOptions *)options {
+ [self validateReference:document];
+ FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options];
+ [self.internalTransaction setData:parsed forDocument:document.key];
+ return self;
+}
+
+- (FIRTransaction *)updateData:(NSDictionary<id, id> *)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<FSTMaybeDocument *> *_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+Internal.h b/Firestore/Source/API/FIRWriteBatch+Internal.h
new file mode 100644
index 0000000..a434e02
--- /dev/null
+++ b/Firestore/Source/API/FIRWriteBatch+Internal.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "FIRWriteBatch.h"
+
+@class FIRFirestore;
+
+@interface FIRWriteBatch (Internal)
+
++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore;
+
+@end
diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m
new file mode 100644
index 0000000..32b6ce8
--- /dev/null
+++ b/Firestore/Source/API/FIRWriteBatch.m
@@ -0,0 +1,116 @@
+/*
+ * 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 "FIRWriteBatch+Internal.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRSetOptions+Internal.h"
+#import "FSTFirestoreClient.h"
+#import "FSTMutation.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.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<FSTMutation *> *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<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document {
+ return [self setData:data forDocument:document options:[FIRSetOptions overwrite]];
+}
+
+- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
+ forDocument:(FIRDocumentReference *)document
+ options:(FIRSetOptions *)options {
+ [self verifyNotCommitted];
+ [self validateReference:document];
+ FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options];
+ [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key
+ precondition:[FSTPrecondition none]]];
+ return self;
+}
+
+- (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)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)commitWithCompletion:(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.h b/Firestore/Source/API/FSTUserDataConverter.h
new file mode 100644
index 0000000..69d1fa9
--- /dev/null
+++ b/Firestore/Source/API/FSTUserDataConverter.h
@@ -0,0 +1,124 @@
+/*
+ * 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 <Foundation/Foundation.h>
+
+@class FIRSetOptions;
+@class FSTDatabaseID;
+@class FSTDocumentKey;
+@class FSTObjectValue;
+@class FSTFieldMask;
+@class FSTFieldValue;
+@class FSTFieldTransform;
+@class FSTMutation;
+@class FSTPrecondition;
+@class FSTSnapshotVersion;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** The result of parsing document data (e.g. for a setData call). */
+@interface FSTParsedSetData : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithData:(FSTObjectValue *)data
+ fieldMask:(nullable FSTFieldMask *)fieldMask
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms
+ NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTObjectValue *data;
+@property(nonatomic, strong, readonly, nullable) FSTFieldMask *fieldMask;
+@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms;
+
+/**
+ * Converts the parsed document data into 1 or 2 mutations (depending on whether there are any
+ * field transforms) using the specified document key and precondition.
+ */
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition;
+
+@end
+
+/** The result of parsing "update" data (i.e. for an updateData call). */
+@interface FSTParsedUpdateData : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithData:(FSTObjectValue *)data
+ fieldMask:(FSTFieldMask *)fieldMask
+ fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms
+ NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTObjectValue *data;
+@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask;
+@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms;
+
+/**
+ * Converts the parsed update data into 1 or 2 mutations (depending on whether there are any
+ * field transforms) using the specified document key and precondition.
+ */
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition;
+
+@end
+
+/**
+ * An internal representation of FIRDocumentReference, representing a key in a specific database.
+ * This is necessary because keys assume a database from context (usually the current one).
+ * FSTDocumentKeyReference binds a key to a specific databaseID.
+ *
+ * TODO(b/64160088): Make FSTDocumentKey aware of the specific databaseID it is tied to.
+ */
+@interface FSTDocumentKeyReference : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (instancetype)initWithKey:(FSTDocumentKey *)key
+ databaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, strong, readonly) FSTDocumentKey *key;
+@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID;
+
+@end
+
+/**
+ * An interface that allows arbitrary pre-converting of user data.
+ *
+ * Returns the converted value (can return back the input to act as a no-op).
+ */
+typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable);
+
+/**
+ * Helper for parsing raw user input (provided via the API) into internal model classes.
+ */
+@interface FSTUserDataConverter : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID
+ preConverter:(FSTPreConverterBlock)preConverter NS_DESIGNATED_INITIALIZER;
+
+/** Parse document data (e.g. from a setData call). */
+- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options;
+
+/** Parse "update" data (i.e. from an updateData call). */
+- (FSTParsedUpdateData *)parsedUpdateData:(id)input;
+
+/** Parse a "query value" (e.g. value in a where filter or a value in a cursor bound). */
+- (FSTFieldValue *)parsedQueryValue:(id)input;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Source/API/FSTUserDataConverter.m b/Firestore/Source/API/FSTUserDataConverter.m
new file mode 100644
index 0000000..7a6c950
--- /dev/null
+++ b/Firestore/Source/API/FSTUserDataConverter.m
@@ -0,0 +1,568 @@
+/*
+ * 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 "FSTUserDataConverter.h"
+
+#import "FIRDocumentReference+Internal.h"
+#import "FIRFieldPath+Internal.h"
+#import "FIRFieldValue+Internal.h"
+#import "FIRFirestore+Internal.h"
+#import "FIRGeoPoint.h"
+#import "FIRSetOptions+Internal.h"
+#import "FSTAssert.h"
+#import "FSTDatabaseID.h"
+#import "FSTDocumentKey.h"
+#import "FSTFieldValue.h"
+#import "FSTMutation.h"
+#import "FSTPath.h"
+#import "FSTTimestamp.h"
+#import "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<FSTFieldTransform *> *)fieldTransforms {
+ self = [super init];
+ if (self) {
+ _data = data;
+ _fieldMask = fieldMask;
+ _fieldTransforms = fieldTransforms;
+ }
+ return self;
+}
+
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition {
+ NSMutableArray<FSTMutation *> *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<FSTFieldTransform *> *)fieldTransforms {
+ self = [super init];
+ if (self) {
+ _data = data;
+ _fieldMask = fieldMask;
+ _fieldTransforms = fieldTransforms;
+ }
+ return self;
+}
+
+- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key
+ precondition:(FSTPrecondition *)precondition {
+ NSMutableArray<FSTMutation *> *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,
+ 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(strong, nonatomic, readonly, nullable) FSTFieldPath *path;
+
+/**
+ * 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<FSTFieldTransform *> *fieldTransforms;
+@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldPath *> *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
+ fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms
+ fieldMask:(NSMutableArray<FSTFieldPath *> *)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;
+@end
+
+@implementation FSTParseContext
+
++ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path {
+ FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource
+ path:path
+ fieldTransforms:[NSMutableArray array]
+ fieldMask:[NSMutableArray array]];
+ [context validatePath];
+ return context;
+}
+
+- (instancetype)initWithSource:(FSTUserDataSource)dataSource
+ path:(nullable FSTFieldPath *)path
+ fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms
+ fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask {
+ if (self = [super init]) {
+ _dataSource = dataSource;
+ _path = path;
+ _fieldTransforms = fieldTransforms;
+ _fieldMask = fieldMask;
+ }
+ return self;
+}
+
+- (instancetype)contextForField:(NSString *)fieldName {
+ FSTParseContext *context =
+ [[FSTParseContext alloc] initWithSource:self.dataSource
+ path:[self.path pathByAppendingSegment:fieldName]
+ 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]
+ 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
+ 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 {
+ return _dataSource == FSTUserDataSourceSet || _dataSource == FSTUserDataSourceUpdate;
+}
+
+- (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 *)parsedSetData:(id)input options:(FIRSetOptions *)options {
+ // 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]];
+
+ __block FSTObjectValue *updateData = [FSTObjectValue objectValue];
+
+ [input enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ // Treat key as a complete field name (don't split on dots, etc.)
+ FSTFieldPath *path = [[FIRFieldPath alloc] initWithFields:@[ key ]].internalValue;
+
+ value = self.preConverter(value);
+
+ FSTFieldValue *_Nullable parsedValue =
+ [self parseData:value context:[context contextForFieldPath:path]];
+ if (parsedValue) {
+ updateData = [updateData objectBySettingValue:parsedValue forPath:path];
+ }
+ }];
+
+ return [[FSTParsedSetData alloc]
+ initWithData:updateData
+ fieldMask:options.merge ? [[FSTFieldMask alloc] initWithFields:context.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<FSTFieldPath *> *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): We may need a different way to detect nested arrays once we support array
+ // paths (at which point we should include the path containing the array in the error message).
+ if (!context.path) {
+ FSTThrowInvalidArgument(@"Nested arrays are not supported");
+ }
+ NSArray *array = input;
+ NSMutableArray<FSTFieldValue *> *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];
+ }];
+ // We don't support field mask paths more granular than the top-level array.
+ [context.fieldMask addObject:context.path];
+ return [[FSTArrayValue alloc] initWithValueNoCopy:result];
+
+ } else if ([input isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *dict = input;
+ NSMutableDictionary<NSString *, FSTFieldValue *> *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]]) {
+ // We shouldn't encounter delete sentinels here. Provide a good error.
+ if (context.dataSource != FSTUserDataSourceUpdate) {
+ FSTThrowInvalidArgument(@"FieldValue.delete() can only be used with updateData().");
+ } else {
+ 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 if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) {
+ if (context.dataSource != FSTUserDataSourceSet &&
+ context.dataSource != FSTUserDataSourceUpdate) {
+ 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