From bde743ed25166a0b320ae157bfb1d68064f531c9 Mon Sep 17 00:00:00 2001 From: Gil Date: Tue, 3 Oct 2017 08:55:22 -0700 Subject: Release 4.3.0 (#327) Initial release of Firestore at 0.8.0 Bump FirebaseCommunity to 0.1.3 --- Firebase/Core/FIRLogger.m | 1 + Firebase/Core/Private/FIRLogger.h | 1 + FirebaseCommunity.podspec | 2 +- .../Example/Firestore.xcodeproj/project.pbxproj | 1700 ++++++ .../xcshareddata/xcschemes/AllTests.xcscheme | 111 + .../xcschemes/Firestore-Example.xcscheme | 113 + .../xcschemes/Firestore_IntegrationTests.xcscheme | 71 + .../xcschemes/Firestore_Tests.xcscheme | 71 + .../xcshareddata/xcschemes/SwiftBuildTest.xcscheme | 91 + .../Firestore/Base.lproj/LaunchScreen.storyboard | 27 + .../Example/Firestore/Base.lproj/Main.storyboard | 27 + Firestore/Example/Firestore/FIRAppDelegate.h | 23 + Firestore/Example/Firestore/FIRAppDelegate.m | 57 + Firestore/Example/Firestore/FIRViewController.h | 21 + Firestore/Example/Firestore/FIRViewController.m | 35 + Firestore/Example/Firestore/Firestore-Info.plist | 49 + .../AppIcon.appiconset/Contents.json | 93 + .../Example/Firestore/en.lproj/InfoPlist.strings | 2 + Firestore/Example/Firestore/main.m | 24 + Firestore/Example/Podfile | 22 + Firestore/Example/SwiftBuildTest/main.swift | 284 + Firestore/Example/Tests/API/FIRGeoPointTests.m | 67 + .../Example/Tests/Core/FSTDatabaseInfoTests.m | 59 + .../Example/Tests/Core/FSTEventManagerTests.m | 163 + .../Example/Tests/Core/FSTQueryListenerTests.m | 487 ++ Firestore/Example/Tests/Core/FSTQueryTests.m | 577 +++ .../Example/Tests/Core/FSTSyncEngine+Testing.h | 32 + .../Example/Tests/Core/FSTTargetIDGeneratorTests.m | 94 + Firestore/Example/Tests/Core/FSTTimestampTests.m | 88 + Firestore/Example/Tests/Core/FSTViewSnapshotTest.m | 141 + Firestore/Example/Tests/Core/FSTViewTests.m | 618 +++ .../Example/Tests/Integration/API/FIRCursorTests.m | 195 + .../Tests/Integration/API/FIRDatabaseTests.m | 741 +++ .../Example/Tests/Integration/API/FIRFieldsTests.m | 223 + .../Integration/API/FIRListenerRegistrationTests.m | 129 + .../Example/Tests/Integration/API/FIRQueryTests.m | 197 + .../Integration/API/FIRServerTimestampTests.m | 183 + .../Example/Tests/Integration/API/FIRTypeTests.m | 79 + .../Tests/Integration/API/FIRValidationTests.m | 560 ++ .../Tests/Integration/API/FIRWriteBatchTests.m | 313 ++ Firestore/Example/Tests/Integration/CAcert.pem | 0 .../Example/Tests/Integration/FSTDatastoreTests.m | 239 + .../Example/Tests/Integration/FSTSmokeTests.m | 129 + .../Tests/Integration/FSTTransactionTests.m | 541 ++ .../Tests/Local/FSTEagerGarbageCollectorTests.m | 111 + .../Example/Tests/Local/FSTLevelDBKeyTests.mm | 361 ++ .../Tests/Local/FSTLevelDBLocalStoreTests.m | 45 + .../Tests/Local/FSTLevelDBMutationQueueTests.mm | 158 + .../Tests/Local/FSTLevelDBQueryCacheTests.m | 54 + .../Local/FSTLevelDBRemoteDocumentCacheTests.mm | 78 + .../Example/Tests/Local/FSTLocalSerializerTests.m | 181 + Firestore/Example/Tests/Local/FSTLocalStoreTests.h | 38 + Firestore/Example/Tests/Local/FSTLocalStoreTests.m | 795 +++ .../Example/Tests/Local/FSTMemoryLocalStoreTests.m | 44 + .../Tests/Local/FSTMemoryMutationQueueTests.m | 42 + .../Example/Tests/Local/FSTMemoryQueryCacheTests.m | 54 + .../Local/FSTMemoryRemoteDocumentCacheTests.m | 49 + .../Example/Tests/Local/FSTMutationQueueTests.h | 38 + .../Example/Tests/Local/FSTMutationQueueTests.m | 511 ++ .../Tests/Local/FSTPersistenceTestHelpers.h | 40 + .../Tests/Local/FSTPersistenceTestHelpers.m | 72 + Firestore/Example/Tests/Local/FSTQueryCacheTests.h | 47 + Firestore/Example/Tests/Local/FSTQueryCacheTests.m | 375 ++ .../Example/Tests/Local/FSTReferenceSetTests.m | 84 + .../Tests/Local/FSTRemoteDocumentCacheTests.h | 39 + .../Tests/Local/FSTRemoteDocumentCacheTests.m | 151 + .../Local/FSTRemoteDocumentChangeBufferTests.m | 113 + .../Example/Tests/Local/FSTWriteGroupTests.mm | 121 + Firestore/Example/Tests/Model/FSTDatabaseIDTests.m | 45 + .../Example/Tests/Model/FSTDocumentKeyTests.m | 60 + .../Example/Tests/Model/FSTDocumentSetTests.m | 142 + Firestore/Example/Tests/Model/FSTDocumentTests.m | 101 + Firestore/Example/Tests/Model/FSTFieldValueTests.m | 576 +++ Firestore/Example/Tests/Model/FSTMutationTests.m | 216 + Firestore/Example/Tests/Model/FSTPathTests.m | 196 + Firestore/Example/Tests/Remote/FSTDatastoreTests.m | 58 + .../Example/Tests/Remote/FSTRemoteEventTests.m | 556 ++ .../Example/Tests/Remote/FSTSerializerBetaTests.m | 794 +++ Firestore/Example/Tests/Remote/FSTStreamTests.m | 139 + .../Example/Tests/Remote/FSTWatchChange+Testing.h | 40 + .../Example/Tests/Remote/FSTWatchChange+Testing.m | 54 + .../Example/Tests/Remote/FSTWatchChangeTests.m | 66 + .../Example/Tests/SpecTests/FSTLevelDBSpecTests.m | 43 + .../Example/Tests/SpecTests/FSTMemorySpecTests.m | 42 + .../Example/Tests/SpecTests/FSTMockDatastore.h | 68 + .../Example/Tests/SpecTests/FSTMockDatastore.m | 344 ++ Firestore/Example/Tests/SpecTests/FSTSpecTests.h | 46 + Firestore/Example/Tests/SpecTests/FSTSpecTests.m | 642 +++ .../Tests/SpecTests/FSTSyncEngineTestDriver.h | 248 + .../Tests/SpecTests/FSTSyncEngineTestDriver.m | 291 ++ Firestore/Example/Tests/SpecTests/json/README.md | 3 + .../Tests/SpecTests/json/collection_spec_test.json | 147 + .../SpecTests/json/existence_filter_spec_test.json | 738 +++ .../Tests/SpecTests/json/limbo_spec_test.json | 1150 +++++ .../Tests/SpecTests/json/limit_spec_test.json | 1626 ++++++ .../Tests/SpecTests/json/listen_spec_test.json | 1524 ++++++ .../Tests/SpecTests/json/offline_spec_test.json | 151 + .../Tests/SpecTests/json/orderby_spec_test.json | 155 + .../SpecTests/json/persistence_spec_test.json | 858 +++ .../SpecTests/json/remote_store_spec_test.json | 559 ++ .../SpecTests/json/resume_token_spec_test.json | 250 + .../Tests/SpecTests/json/write_spec_test.json | 5437 ++++++++++++++++++++ Firestore/Example/Tests/Tests-Info.plist | 22 + Firestore/Example/Tests/Util/FSTAssertTests.m | 105 + Firestore/Example/Tests/Util/FSTComparisonTests.m | 143 + Firestore/Example/Tests/Util/FSTEventAccumulator.h | 41 + Firestore/Example/Tests/Util/FSTEventAccumulator.m | 94 + Firestore/Example/Tests/Util/FSTHelpers.h | 258 + Firestore/Example/Tests/Util/FSTHelpers.m | 348 ++ .../Example/Tests/Util/FSTIntegrationTestCase.h | 94 + .../Example/Tests/Util/FSTIntegrationTestCase.m | 285 + Firestore/Example/Tests/Util/FSTUtilTests.m | 35 + Firestore/Example/Tests/Util/XCTestCase+Await.h | 32 + Firestore/Example/Tests/Util/XCTestCase+Await.m | 38 + Firestore/Example/Tests/en.lproj/InfoPlist.strings | 2 + Firestore/Firestore.podspec | 44 + Firestore/Port/absl/absl_attributes.h | 644 +++ Firestore/Port/absl/absl_config.h | 306 ++ Firestore/Port/absl/absl_endian.h | 342 ++ Firestore/Port/absl/absl_integral_types.h | 148 + Firestore/Port/absl/absl_port.h | 535 ++ Firestore/Port/bits.cc | 39 + Firestore/Port/bits.h | 160 + Firestore/Port/bits_test.cc | 138 + Firestore/Port/ordered_code.cc | 579 +++ Firestore/Port/ordered_code.h | 116 + Firestore/Port/ordered_code_test.cc | 528 ++ Firestore/Port/string_util.cc | 51 + Firestore/Port/string_util.h | 66 + Firestore/Port/string_util_test.cc | 39 + .../FrameworkMaker.xcodeproj/project.pbxproj | 428 ++ .../xcschemes/FrameworkMaker_iOS.xcscheme | 91 + .../xcschemes/FrameworkMaker_macOS.xcscheme | 91 + Firestore/Protos/Podfile | 11 + Firestore/Protos/README.md | 20 + Firestore/Protos/build-protos.sh | 40 + .../objc/firestore/local/MaybeDocument.pbobjc.h | 132 + .../objc/firestore/local/MaybeDocument.pbobjc.m | 192 + .../Protos/objc/firestore/local/Mutation.pbobjc.h | 138 + .../Protos/objc/firestore/local/Mutation.pbobjc.m | 190 + .../Protos/objc/firestore/local/Target.pbobjc.h | 208 + .../Protos/objc/firestore/local/Target.pbobjc.m | 247 + .../Protos/objc/google/api/Annotations.pbobjc.h | 17 + .../Protos/objc/google/api/Annotations.pbobjc.m | 17 + Firestore/Protos/objc/google/api/HTTP.pbobjc.h | 406 ++ Firestore/Protos/objc/google/api/HTTP.pbobjc.m | 306 ++ .../objc/google/firestore/v1beta1/Common.pbobjc.h | 223 + .../objc/google/firestore/v1beta1/Common.pbobjc.m | 345 ++ .../google/firestore/v1beta1/Document.pbobjc.h | 309 ++ .../google/firestore/v1beta1/Document.pbobjc.m | 412 ++ .../google/firestore/v1beta1/Firestore.pbobjc.h | 1342 +++++ .../google/firestore/v1beta1/Firestore.pbobjc.m | 2064 ++++++++ .../google/firestore/v1beta1/Firestore.pbrpc.h | 232 + .../google/firestore/v1beta1/Firestore.pbrpc.m | 281 + .../objc/google/firestore/v1beta1/Query.pbobjc.h | 579 +++ .../objc/google/firestore/v1beta1/Query.pbobjc.m | 907 ++++ .../objc/google/firestore/v1beta1/Write.pbobjc.h | 432 ++ .../objc/google/firestore/v1beta1/Write.pbobjc.m | 653 +++ Firestore/Protos/objc/google/rpc/Status.pbobjc.h | 155 + Firestore/Protos/objc/google/rpc/Status.pbobjc.m | 136 + Firestore/Protos/objc/google/type/Latlng.pbobjc.h | 127 + Firestore/Protos/objc/google/type/Latlng.pbobjc.m | 119 + .../protos/firestore/local/maybe_document.proto | 33 + .../Protos/protos/firestore/local/mutation.proto | 44 + .../Protos/protos/firestore/local/target.proto | 90 + .../Protos/protos/google/api/annotations.proto | 31 + Firestore/Protos/protos/google/api/http.proto | 291 ++ .../protos/google/firestore/v1beta1/common.proto | 82 + .../protos/google/firestore/v1beta1/document.proto | 148 + .../google/firestore/v1beta1/firestore.proto | 719 +++ .../protos/google/firestore/v1beta1/query.proto | 231 + .../protos/google/firestore/v1beta1/write.proto | 189 + Firestore/Protos/protos/google/rpc/status.proto | 92 + Firestore/Protos/protos/google/type/latlng.proto | 71 + Firestore/Protos/strip-registry.py | 36 + Firestore/README.md | 15 + .../Source/API/FIRCollectionReference+Internal.h | 28 + Firestore/Source/API/FIRCollectionReference.m | 113 + Firestore/Source/API/FIRDocumentChange+Internal.h | 32 + Firestore/Source/API/FIRDocumentChange.m | 129 + .../Source/API/FIRDocumentReference+Internal.h | 34 + Firestore/Source/API/FIRDocumentReference.m | 285 + .../Source/API/FIRDocumentSnapshot+Internal.h | 37 + Firestore/Source/API/FIRDocumentSnapshot.m | 175 + Firestore/Source/API/FIRFieldPath+Internal.h | 39 + Firestore/Source/API/FIRFieldPath.m | 101 + Firestore/Source/API/FIRFieldValue+Internal.h | 37 + Firestore/Source/API/FIRFieldValue.m | 96 + Firestore/Source/API/FIRFirestore+Internal.h | 64 + Firestore/Source/API/FIRFirestore.m | 284 + Firestore/Source/API/FIRFirestoreSettings.m | 92 + Firestore/Source/API/FIRFirestoreVersion.h | 22 + Firestore/Source/API/FIRFirestoreVersion.m | 29 + Firestore/Source/API/FIRGeoPoint+Internal.h | 26 + Firestore/Source/API/FIRGeoPoint.m | 85 + .../Source/API/FIRListenerRegistration+Internal.h | 34 + Firestore/Source/API/FIRListenerRegistration.m | 57 + Firestore/Source/API/FIRQuery+Internal.h | 29 + Firestore/Source/API/FIRQuery.m | 520 ++ Firestore/Source/API/FIRQuerySnapshot+Internal.h | 37 + Firestore/Source/API/FIRQuerySnapshot.m | 125 + Firestore/Source/API/FIRQuery_Init.h | 32 + Firestore/Source/API/FIRSetOptions+Internal.h | 33 + Firestore/Source/API/FIRSetOptions.m | 65 + .../Source/API/FIRSnapshotMetadata+Internal.h | 29 + Firestore/Source/API/FIRSnapshotMetadata.m | 49 + Firestore/Source/API/FIRTransaction+Internal.h | 27 + Firestore/Source/API/FIRTransaction.m | 147 + Firestore/Source/API/FIRWriteBatch+Internal.h | 25 + Firestore/Source/API/FIRWriteBatch.m | 116 + Firestore/Source/API/FSTUserDataConverter.h | 124 + Firestore/Source/API/FSTUserDataConverter.m | 568 ++ Firestore/Source/Auth/FSTCredentialsProvider.h | 113 + Firestore/Source/Auth/FSTCredentialsProvider.m | 161 + .../Source/Auth/FSTEmptyCredentialsProvider.h | 28 + .../Source/Auth/FSTEmptyCredentialsProvider.m | 47 + Firestore/Source/Auth/FSTUser.h | 43 + Firestore/Source/Auth/FSTUser.m | 68 + Firestore/Source/Core/FSTDatabaseInfo.h | 55 + Firestore/Source/Core/FSTDatabaseInfo.m | 70 + Firestore/Source/Core/FSTEventManager.h | 88 + Firestore/Source/Core/FSTEventManager.m | 335 ++ Firestore/Source/Core/FSTFirestoreClient.h | 87 + Firestore/Source/Core/FSTFirestoreClient.m | 271 + Firestore/Source/Core/FSTQuery.h | 269 + Firestore/Source/Core/FSTQuery.m | 759 +++ Firestore/Source/Core/FSTSnapshotVersion.h | 43 + Firestore/Source/Core/FSTSnapshotVersion.m | 80 + Firestore/Source/Core/FSTSyncEngine.h | 105 + Firestore/Source/Core/FSTSyncEngine.m | 520 ++ Firestore/Source/Core/FSTTargetIDGenerator.h | 55 + Firestore/Source/Core/FSTTargetIDGenerator.m | 105 + Firestore/Source/Core/FSTTimestamp.h | 72 + Firestore/Source/Core/FSTTimestamp.m | 122 + Firestore/Source/Core/FSTTransaction.h | 73 + Firestore/Source/Core/FSTTransaction.m | 250 + Firestore/Source/Core/FSTTypes.h | 90 + Firestore/Source/Core/FSTView.h | 143 + Firestore/Source/Core/FSTView.m | 451 ++ Firestore/Source/Core/FSTViewSnapshot.h | 117 + Firestore/Source/Core/FSTViewSnapshot.m | 231 + Firestore/Source/Local/FSTDocumentReference.h | 61 + Firestore/Source/Local/FSTDocumentReference.m | 83 + Firestore/Source/Local/FSTEagerGarbageCollector.h | 36 + Firestore/Source/Local/FSTEagerGarbageCollector.m | 89 + Firestore/Source/Local/FSTGarbageCollector.h | 95 + Firestore/Source/Local/FSTLevelDB.h | 105 + Firestore/Source/Local/FSTLevelDB.mm | 246 + Firestore/Source/Local/FSTLevelDBKey.h | 344 ++ Firestore/Source/Local/FSTLevelDBKey.mm | 757 +++ Firestore/Source/Local/FSTLevelDBMutationQueue.h | 64 + Firestore/Source/Local/FSTLevelDBMutationQueue.mm | 637 +++ Firestore/Source/Local/FSTLevelDBQueryCache.h | 54 + Firestore/Source/Local/FSTLevelDBQueryCache.mm | 340 ++ .../Source/Local/FSTLevelDBRemoteDocumentCache.h | 50 + .../Source/Local/FSTLevelDBRemoteDocumentCache.mm | 153 + Firestore/Source/Local/FSTLocalDocumentsView.h | 62 + Firestore/Source/Local/FSTLocalDocumentsView.m | 182 + Firestore/Source/Local/FSTLocalSerializer.h | 72 + Firestore/Source/Local/FSTLocalSerializer.m | 208 + Firestore/Source/Local/FSTLocalStore.h | 194 + Firestore/Source/Local/FSTLocalStore.m | 546 ++ Firestore/Source/Local/FSTLocalViewChanges.h | 51 + Firestore/Source/Local/FSTLocalViewChanges.m | 76 + Firestore/Source/Local/FSTLocalWriteResult.h | 36 + Firestore/Source/Local/FSTLocalWriteResult.m | 43 + Firestore/Source/Local/FSTMemoryMutationQueue.h | 34 + Firestore/Source/Local/FSTMemoryMutationQueue.m | 441 ++ Firestore/Source/Local/FSTMemoryPersistence.h | 33 + Firestore/Source/Local/FSTMemoryPersistence.m | 107 + Firestore/Source/Local/FSTMemoryQueryCache.h | 30 + Firestore/Source/Local/FSTMemoryQueryCache.m | 131 + .../Source/Local/FSTMemoryRemoteDocumentCache.h | 29 + .../Source/Local/FSTMemoryRemoteDocumentCache.m | 84 + Firestore/Source/Local/FSTMutationQueue.h | 159 + Firestore/Source/Local/FSTNoOpGarbageCollector.h | 32 + Firestore/Source/Local/FSTNoOpGarbageCollector.m | 45 + Firestore/Source/Local/FSTPersistence.h | 103 + Firestore/Source/Local/FSTQueryCache.h | 113 + Firestore/Source/Local/FSTQueryData.h | 82 + Firestore/Source/Local/FSTQueryData.m | 93 + Firestore/Source/Local/FSTReferenceSet.h | 71 + Firestore/Source/Local/FSTReferenceSet.m | 135 + Firestore/Source/Local/FSTRemoteDocumentCache.h | 76 + .../Source/Local/FSTRemoteDocumentChangeBuffer.h | 66 + .../Source/Local/FSTRemoteDocumentChangeBuffer.m | 88 + Firestore/Source/Local/FSTWriteGroup.h | 97 + Firestore/Source/Local/FSTWriteGroup.mm | 145 + Firestore/Source/Local/FSTWriteGroupTracker.h | 45 + Firestore/Source/Local/FSTWriteGroupTracker.m | 52 + Firestore/Source/Local/StringView.h | 85 + Firestore/Source/Model/FSTDatabaseID.h | 48 + Firestore/Source/Model/FSTDatabaseID.m | 90 + Firestore/Source/Model/FSTDocument.h | 58 + Firestore/Source/Model/FSTDocument.m | 139 + Firestore/Source/Model/FSTDocumentDictionary.h | 44 + Firestore/Source/Model/FSTDocumentDictionary.m | 42 + Firestore/Source/Model/FSTDocumentKey.h | 66 + Firestore/Source/Model/FSTDocumentKey.m | 105 + Firestore/Source/Model/FSTDocumentKeySet.h | 35 + Firestore/Source/Model/FSTDocumentKeySet.m | 31 + Firestore/Source/Model/FSTDocumentSet.h | 95 + Firestore/Source/Model/FSTDocumentSet.m | 197 + .../Source/Model/FSTDocumentVersionDictionary.h | 40 + .../Source/Model/FSTDocumentVersionDictionary.m | 37 + Firestore/Source/Model/FSTFieldValue.h | 242 + Firestore/Source/Model/FSTFieldValue.m | 837 +++ Firestore/Source/Model/FSTMutation.h | 325 ++ Firestore/Source/Model/FSTMutation.m | 575 +++ Firestore/Source/Model/FSTMutationBatch.h | 119 + Firestore/Source/Model/FSTMutationBatch.m | 176 + Firestore/Source/Model/FSTPath.h | 141 + Firestore/Source/Model/FSTPath.m | 356 ++ Firestore/Source/Public/FIRCollectionReference.h | 99 + Firestore/Source/Public/FIRDocumentChange.h | 70 + Firestore/Source/Public/FIRDocumentReference.h | 219 + Firestore/Source/Public/FIRDocumentSnapshot.h | 68 + Firestore/Source/Public/FIRFieldPath.h | 50 + Firestore/Source/Public/FIRFieldValue.h | 45 + Firestore/Source/Public/FIRFirestore.h | 145 + Firestore/Source/Public/FIRFirestoreErrors.h | 105 + Firestore/Source/Public/FIRFirestoreSettings.h | 51 + .../Source/Public/FIRFirestoreSwiftNameSupport.h | 29 + Firestore/Source/Public/FIRGeoPoint.h | 49 + Firestore/Source/Public/FIRListenerRegistration.h | 32 + Firestore/Source/Public/FIRQuery.h | 414 ++ Firestore/Source/Public/FIRQuerySnapshot.h | 65 + Firestore/Source/Public/FIRSetOptions.h | 46 + Firestore/Source/Public/FIRSnapshotMetadata.h | 44 + Firestore/Source/Public/FIRTransaction.h | 106 + Firestore/Source/Public/FIRWriteBatch.h | 107 + Firestore/Source/Remote/FSTBufferedWriter.h | 44 + Firestore/Source/Remote/FSTBufferedWriter.m | 134 + Firestore/Source/Remote/FSTDatastore.h | 365 ++ Firestore/Source/Remote/FSTDatastore.m | 1027 ++++ Firestore/Source/Remote/FSTExistenceFilter.h | 31 + Firestore/Source/Remote/FSTExistenceFilter.m | 53 + Firestore/Source/Remote/FSTExponentialBackoff.h | 79 + Firestore/Source/Remote/FSTExponentialBackoff.m | 97 + Firestore/Source/Remote/FSTRemoteEvent.h | 213 + Firestore/Source/Remote/FSTRemoteEvent.m | 516 ++ Firestore/Source/Remote/FSTRemoteStore.h | 143 + Firestore/Source/Remote/FSTRemoteStore.m | 599 +++ Firestore/Source/Remote/FSTSerializerBeta.h | 110 + Firestore/Source/Remote/FSTSerializerBeta.m | 1084 ++++ Firestore/Source/Remote/FSTWatchChange.h | 118 + Firestore/Source/Remote/FSTWatchChange.m | 150 + Firestore/Source/Util/FSTAssert.h | 77 + Firestore/Source/Util/FSTAsyncQueryListener.h | 48 + Firestore/Source/Util/FSTAsyncQueryListener.m | 50 + Firestore/Source/Util/FSTClasses.h | 40 + Firestore/Source/Util/FSTComparison.h | 66 + Firestore/Source/Util/FSTComparison.m | 175 + Firestore/Source/Util/FSTDispatchQueue.h | 58 + Firestore/Source/Util/FSTDispatchQueue.m | 75 + Firestore/Source/Util/FSTLogger.h | 34 + Firestore/Source/Util/FSTLogger.m | 40 + Firestore/Source/Util/FSTUsageValidation.h | 45 + Firestore/Source/Util/FSTUsageValidation.m | 30 + Firestore/Source/Util/FSTUtil.h | 31 + Firestore/Source/Util/FSTUtil.m | 44 + Firestore/test.sh | 49 + .../Immutable/FSTArraySortedDictionary.h | 35 + .../Immutable/FSTArraySortedDictionary.m | 242 + .../Immutable/FSTArraySortedDictionaryEnumerator.h | 26 + .../Immutable/FSTArraySortedDictionaryEnumerator.m | 54 + .../Immutable/FSTImmutableSortedDictionary.h | 120 + .../Immutable/FSTImmutableSortedDictionary.m | 143 + .../third_party/Immutable/FSTImmutableSortedSet.h | 47 + .../third_party/Immutable/FSTImmutableSortedSet.m | 144 + Firestore/third_party/Immutable/FSTLLRBEmptyNode.h | 11 + Firestore/third_party/Immutable/FSTLLRBEmptyNode.m | 102 + Firestore/third_party/Immutable/FSTLLRBNode.h | 68 + Firestore/third_party/Immutable/FSTLLRBValueNode.h | 29 + Firestore/third_party/Immutable/FSTLLRBValueNode.m | 308 ++ .../Immutable/FSTTreeSortedDictionary.h | 41 + .../Immutable/FSTTreeSortedDictionary.m | 382 ++ .../Immutable/FSTTreeSortedDictionaryEnumerator.h | 21 + .../Immutable/FSTTreeSortedDictionaryEnumerator.m | 114 + Firestore/third_party/Immutable/LICENSE | 21 + .../Tests/FSTArraySortedDictionaryTests.m | 467 ++ .../Tests/FSTImmutableSortedDictionary+Testing.h | 17 + .../Tests/FSTImmutableSortedDictionary+Testing.m | 17 + .../Tests/FSTImmutableSortedSet+Testing.h | 20 + .../Tests/FSTImmutableSortedSet+Testing.m | 17 + .../Immutable/Tests/FSTLLRBValueNode+Test.h | 10 + .../Immutable/Tests/FSTTreeSortedDictionaryTests.m | 655 +++ README.md | 8 +- scripts/style.sh | 1 + test.sh | 5 +- 390 files changed, 77624 insertions(+), 4 deletions(-) create mode 100644 Firestore/Example/Firestore.xcodeproj/project.pbxproj create mode 100644 Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme create mode 100644 Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme create mode 100644 Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme create mode 100644 Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme create mode 100644 Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme create mode 100644 Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard create mode 100644 Firestore/Example/Firestore/Base.lproj/Main.storyboard create mode 100644 Firestore/Example/Firestore/FIRAppDelegate.h create mode 100644 Firestore/Example/Firestore/FIRAppDelegate.m create mode 100644 Firestore/Example/Firestore/FIRViewController.h create mode 100644 Firestore/Example/Firestore/FIRViewController.m create mode 100644 Firestore/Example/Firestore/Firestore-Info.plist create mode 100644 Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Firestore/Example/Firestore/en.lproj/InfoPlist.strings create mode 100644 Firestore/Example/Firestore/main.m create mode 100644 Firestore/Example/Podfile create mode 100644 Firestore/Example/SwiftBuildTest/main.swift create mode 100644 Firestore/Example/Tests/API/FIRGeoPointTests.m create mode 100644 Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m create mode 100644 Firestore/Example/Tests/Core/FSTEventManagerTests.m create mode 100644 Firestore/Example/Tests/Core/FSTQueryListenerTests.m create mode 100644 Firestore/Example/Tests/Core/FSTQueryTests.m create mode 100644 Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h create mode 100644 Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m create mode 100644 Firestore/Example/Tests/Core/FSTTimestampTests.m create mode 100644 Firestore/Example/Tests/Core/FSTViewSnapshotTest.m create mode 100644 Firestore/Example/Tests/Core/FSTViewTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRCursorTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRFieldsTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRQueryTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRTypeTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRValidationTests.m create mode 100644 Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m create mode 100644 Firestore/Example/Tests/Integration/CAcert.pem create mode 100644 Firestore/Example/Tests/Integration/FSTDatastoreTests.m create mode 100644 Firestore/Example/Tests/Integration/FSTSmokeTests.m create mode 100644 Firestore/Example/Tests/Integration/FSTTransactionTests.m create mode 100644 Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm create mode 100644 Firestore/Example/Tests/Local/FSTLocalSerializerTests.m create mode 100644 Firestore/Example/Tests/Local/FSTLocalStoreTests.h create mode 100644 Firestore/Example/Tests/Local/FSTLocalStoreTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTMutationQueueTests.h create mode 100644 Firestore/Example/Tests/Local/FSTMutationQueueTests.m create mode 100644 Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h create mode 100644 Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m create mode 100644 Firestore/Example/Tests/Local/FSTQueryCacheTests.h create mode 100644 Firestore/Example/Tests/Local/FSTQueryCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTReferenceSetTests.m create mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h create mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m create mode 100644 Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m create mode 100644 Firestore/Example/Tests/Local/FSTWriteGroupTests.mm create mode 100644 Firestore/Example/Tests/Model/FSTDatabaseIDTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDocumentKeyTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDocumentSetTests.m create mode 100644 Firestore/Example/Tests/Model/FSTDocumentTests.m create mode 100644 Firestore/Example/Tests/Model/FSTFieldValueTests.m create mode 100644 Firestore/Example/Tests/Model/FSTMutationTests.m create mode 100644 Firestore/Example/Tests/Model/FSTPathTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTDatastoreTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTRemoteEventTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTStreamTests.m create mode 100644 Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h create mode 100644 Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m create mode 100644 Firestore/Example/Tests/Remote/FSTWatchChangeTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTMockDatastore.h create mode 100644 Firestore/Example/Tests/SpecTests/FSTMockDatastore.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTSpecTests.h create mode 100644 Firestore/Example/Tests/SpecTests/FSTSpecTests.m create mode 100644 Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h create mode 100644 Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m create mode 100644 Firestore/Example/Tests/SpecTests/json/README.md create mode 100644 Firestore/Example/Tests/SpecTests/json/collection_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/limit_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/listen_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/offline_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json create mode 100644 Firestore/Example/Tests/SpecTests/json/write_spec_test.json create mode 100644 Firestore/Example/Tests/Tests-Info.plist create mode 100644 Firestore/Example/Tests/Util/FSTAssertTests.m create mode 100644 Firestore/Example/Tests/Util/FSTComparisonTests.m create mode 100644 Firestore/Example/Tests/Util/FSTEventAccumulator.h create mode 100644 Firestore/Example/Tests/Util/FSTEventAccumulator.m create mode 100644 Firestore/Example/Tests/Util/FSTHelpers.h create mode 100644 Firestore/Example/Tests/Util/FSTHelpers.m create mode 100644 Firestore/Example/Tests/Util/FSTIntegrationTestCase.h create mode 100644 Firestore/Example/Tests/Util/FSTIntegrationTestCase.m create mode 100644 Firestore/Example/Tests/Util/FSTUtilTests.m create mode 100644 Firestore/Example/Tests/Util/XCTestCase+Await.h create mode 100644 Firestore/Example/Tests/Util/XCTestCase+Await.m create mode 100644 Firestore/Example/Tests/en.lproj/InfoPlist.strings create mode 100644 Firestore/Firestore.podspec create mode 100644 Firestore/Port/absl/absl_attributes.h create mode 100644 Firestore/Port/absl/absl_config.h create mode 100644 Firestore/Port/absl/absl_endian.h create mode 100644 Firestore/Port/absl/absl_integral_types.h create mode 100644 Firestore/Port/absl/absl_port.h create mode 100644 Firestore/Port/bits.cc create mode 100644 Firestore/Port/bits.h create mode 100644 Firestore/Port/bits_test.cc create mode 100644 Firestore/Port/ordered_code.cc create mode 100644 Firestore/Port/ordered_code.h create mode 100644 Firestore/Port/ordered_code_test.cc create mode 100644 Firestore/Port/string_util.cc create mode 100644 Firestore/Port/string_util.h create mode 100644 Firestore/Port/string_util_test.cc create mode 100644 Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj create mode 100644 Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme create mode 100644 Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme create mode 100644 Firestore/Protos/Podfile create mode 100644 Firestore/Protos/README.md create mode 100755 Firestore/Protos/build-protos.sh create mode 100644 Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h create mode 100644 Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m create mode 100644 Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h create mode 100644 Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m create mode 100644 Firestore/Protos/objc/firestore/local/Target.pbobjc.h create mode 100644 Firestore/Protos/objc/firestore/local/Target.pbobjc.m create mode 100644 Firestore/Protos/objc/google/api/Annotations.pbobjc.h create mode 100644 Firestore/Protos/objc/google/api/Annotations.pbobjc.m create mode 100644 Firestore/Protos/objc/google/api/HTTP.pbobjc.h create mode 100644 Firestore/Protos/objc/google/api/HTTP.pbobjc.m create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h create mode 100644 Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m create mode 100644 Firestore/Protos/objc/google/rpc/Status.pbobjc.h create mode 100644 Firestore/Protos/objc/google/rpc/Status.pbobjc.m create mode 100644 Firestore/Protos/objc/google/type/Latlng.pbobjc.h create mode 100644 Firestore/Protos/objc/google/type/Latlng.pbobjc.m create mode 100644 Firestore/Protos/protos/firestore/local/maybe_document.proto create mode 100644 Firestore/Protos/protos/firestore/local/mutation.proto create mode 100644 Firestore/Protos/protos/firestore/local/target.proto create mode 100644 Firestore/Protos/protos/google/api/annotations.proto create mode 100644 Firestore/Protos/protos/google/api/http.proto create mode 100644 Firestore/Protos/protos/google/firestore/v1beta1/common.proto create mode 100644 Firestore/Protos/protos/google/firestore/v1beta1/document.proto create mode 100644 Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto create mode 100644 Firestore/Protos/protos/google/firestore/v1beta1/query.proto create mode 100644 Firestore/Protos/protos/google/firestore/v1beta1/write.proto create mode 100644 Firestore/Protos/protos/google/rpc/status.proto create mode 100644 Firestore/Protos/protos/google/type/latlng.proto create mode 100755 Firestore/Protos/strip-registry.py create mode 100644 Firestore/README.md create mode 100644 Firestore/Source/API/FIRCollectionReference+Internal.h create mode 100644 Firestore/Source/API/FIRCollectionReference.m create mode 100644 Firestore/Source/API/FIRDocumentChange+Internal.h create mode 100644 Firestore/Source/API/FIRDocumentChange.m create mode 100644 Firestore/Source/API/FIRDocumentReference+Internal.h create mode 100644 Firestore/Source/API/FIRDocumentReference.m create mode 100644 Firestore/Source/API/FIRDocumentSnapshot+Internal.h create mode 100644 Firestore/Source/API/FIRDocumentSnapshot.m create mode 100644 Firestore/Source/API/FIRFieldPath+Internal.h create mode 100644 Firestore/Source/API/FIRFieldPath.m create mode 100644 Firestore/Source/API/FIRFieldValue+Internal.h create mode 100644 Firestore/Source/API/FIRFieldValue.m create mode 100644 Firestore/Source/API/FIRFirestore+Internal.h create mode 100644 Firestore/Source/API/FIRFirestore.m create mode 100644 Firestore/Source/API/FIRFirestoreSettings.m create mode 100644 Firestore/Source/API/FIRFirestoreVersion.h create mode 100644 Firestore/Source/API/FIRFirestoreVersion.m create mode 100644 Firestore/Source/API/FIRGeoPoint+Internal.h create mode 100644 Firestore/Source/API/FIRGeoPoint.m create mode 100644 Firestore/Source/API/FIRListenerRegistration+Internal.h create mode 100644 Firestore/Source/API/FIRListenerRegistration.m create mode 100644 Firestore/Source/API/FIRQuery+Internal.h create mode 100644 Firestore/Source/API/FIRQuery.m create mode 100644 Firestore/Source/API/FIRQuerySnapshot+Internal.h create mode 100644 Firestore/Source/API/FIRQuerySnapshot.m create mode 100644 Firestore/Source/API/FIRQuery_Init.h create mode 100644 Firestore/Source/API/FIRSetOptions+Internal.h create mode 100644 Firestore/Source/API/FIRSetOptions.m create mode 100644 Firestore/Source/API/FIRSnapshotMetadata+Internal.h create mode 100644 Firestore/Source/API/FIRSnapshotMetadata.m create mode 100644 Firestore/Source/API/FIRTransaction+Internal.h create mode 100644 Firestore/Source/API/FIRTransaction.m create mode 100644 Firestore/Source/API/FIRWriteBatch+Internal.h create mode 100644 Firestore/Source/API/FIRWriteBatch.m create mode 100644 Firestore/Source/API/FSTUserDataConverter.h create mode 100644 Firestore/Source/API/FSTUserDataConverter.m create mode 100644 Firestore/Source/Auth/FSTCredentialsProvider.h create mode 100644 Firestore/Source/Auth/FSTCredentialsProvider.m create mode 100644 Firestore/Source/Auth/FSTEmptyCredentialsProvider.h create mode 100644 Firestore/Source/Auth/FSTEmptyCredentialsProvider.m create mode 100644 Firestore/Source/Auth/FSTUser.h create mode 100644 Firestore/Source/Auth/FSTUser.m create mode 100644 Firestore/Source/Core/FSTDatabaseInfo.h create mode 100644 Firestore/Source/Core/FSTDatabaseInfo.m create mode 100644 Firestore/Source/Core/FSTEventManager.h create mode 100644 Firestore/Source/Core/FSTEventManager.m create mode 100644 Firestore/Source/Core/FSTFirestoreClient.h create mode 100644 Firestore/Source/Core/FSTFirestoreClient.m create mode 100644 Firestore/Source/Core/FSTQuery.h create mode 100644 Firestore/Source/Core/FSTQuery.m create mode 100644 Firestore/Source/Core/FSTSnapshotVersion.h create mode 100644 Firestore/Source/Core/FSTSnapshotVersion.m create mode 100644 Firestore/Source/Core/FSTSyncEngine.h create mode 100644 Firestore/Source/Core/FSTSyncEngine.m create mode 100644 Firestore/Source/Core/FSTTargetIDGenerator.h create mode 100644 Firestore/Source/Core/FSTTargetIDGenerator.m create mode 100644 Firestore/Source/Core/FSTTimestamp.h create mode 100644 Firestore/Source/Core/FSTTimestamp.m create mode 100644 Firestore/Source/Core/FSTTransaction.h create mode 100644 Firestore/Source/Core/FSTTransaction.m create mode 100644 Firestore/Source/Core/FSTTypes.h create mode 100644 Firestore/Source/Core/FSTView.h create mode 100644 Firestore/Source/Core/FSTView.m create mode 100644 Firestore/Source/Core/FSTViewSnapshot.h create mode 100644 Firestore/Source/Core/FSTViewSnapshot.m create mode 100644 Firestore/Source/Local/FSTDocumentReference.h create mode 100644 Firestore/Source/Local/FSTDocumentReference.m create mode 100644 Firestore/Source/Local/FSTEagerGarbageCollector.h create mode 100644 Firestore/Source/Local/FSTEagerGarbageCollector.m create mode 100644 Firestore/Source/Local/FSTGarbageCollector.h create mode 100644 Firestore/Source/Local/FSTLevelDB.h create mode 100644 Firestore/Source/Local/FSTLevelDB.mm create mode 100644 Firestore/Source/Local/FSTLevelDBKey.h create mode 100644 Firestore/Source/Local/FSTLevelDBKey.mm create mode 100644 Firestore/Source/Local/FSTLevelDBMutationQueue.h create mode 100644 Firestore/Source/Local/FSTLevelDBMutationQueue.mm create mode 100644 Firestore/Source/Local/FSTLevelDBQueryCache.h create mode 100644 Firestore/Source/Local/FSTLevelDBQueryCache.mm create mode 100644 Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h create mode 100644 Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm create mode 100644 Firestore/Source/Local/FSTLocalDocumentsView.h create mode 100644 Firestore/Source/Local/FSTLocalDocumentsView.m create mode 100644 Firestore/Source/Local/FSTLocalSerializer.h create mode 100644 Firestore/Source/Local/FSTLocalSerializer.m create mode 100644 Firestore/Source/Local/FSTLocalStore.h create mode 100644 Firestore/Source/Local/FSTLocalStore.m create mode 100644 Firestore/Source/Local/FSTLocalViewChanges.h create mode 100644 Firestore/Source/Local/FSTLocalViewChanges.m create mode 100644 Firestore/Source/Local/FSTLocalWriteResult.h create mode 100644 Firestore/Source/Local/FSTLocalWriteResult.m create mode 100644 Firestore/Source/Local/FSTMemoryMutationQueue.h create mode 100644 Firestore/Source/Local/FSTMemoryMutationQueue.m create mode 100644 Firestore/Source/Local/FSTMemoryPersistence.h create mode 100644 Firestore/Source/Local/FSTMemoryPersistence.m create mode 100644 Firestore/Source/Local/FSTMemoryQueryCache.h create mode 100644 Firestore/Source/Local/FSTMemoryQueryCache.m create mode 100644 Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h create mode 100644 Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m create mode 100644 Firestore/Source/Local/FSTMutationQueue.h create mode 100644 Firestore/Source/Local/FSTNoOpGarbageCollector.h create mode 100644 Firestore/Source/Local/FSTNoOpGarbageCollector.m create mode 100644 Firestore/Source/Local/FSTPersistence.h create mode 100644 Firestore/Source/Local/FSTQueryCache.h create mode 100644 Firestore/Source/Local/FSTQueryData.h create mode 100644 Firestore/Source/Local/FSTQueryData.m create mode 100644 Firestore/Source/Local/FSTReferenceSet.h create mode 100644 Firestore/Source/Local/FSTReferenceSet.m create mode 100644 Firestore/Source/Local/FSTRemoteDocumentCache.h create mode 100644 Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h create mode 100644 Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m create mode 100644 Firestore/Source/Local/FSTWriteGroup.h create mode 100644 Firestore/Source/Local/FSTWriteGroup.mm create mode 100644 Firestore/Source/Local/FSTWriteGroupTracker.h create mode 100644 Firestore/Source/Local/FSTWriteGroupTracker.m create mode 100644 Firestore/Source/Local/StringView.h create mode 100644 Firestore/Source/Model/FSTDatabaseID.h create mode 100644 Firestore/Source/Model/FSTDatabaseID.m create mode 100644 Firestore/Source/Model/FSTDocument.h create mode 100644 Firestore/Source/Model/FSTDocument.m create mode 100644 Firestore/Source/Model/FSTDocumentDictionary.h create mode 100644 Firestore/Source/Model/FSTDocumentDictionary.m create mode 100644 Firestore/Source/Model/FSTDocumentKey.h create mode 100644 Firestore/Source/Model/FSTDocumentKey.m create mode 100644 Firestore/Source/Model/FSTDocumentKeySet.h create mode 100644 Firestore/Source/Model/FSTDocumentKeySet.m create mode 100644 Firestore/Source/Model/FSTDocumentSet.h create mode 100644 Firestore/Source/Model/FSTDocumentSet.m create mode 100644 Firestore/Source/Model/FSTDocumentVersionDictionary.h create mode 100644 Firestore/Source/Model/FSTDocumentVersionDictionary.m create mode 100644 Firestore/Source/Model/FSTFieldValue.h create mode 100644 Firestore/Source/Model/FSTFieldValue.m create mode 100644 Firestore/Source/Model/FSTMutation.h create mode 100644 Firestore/Source/Model/FSTMutation.m create mode 100644 Firestore/Source/Model/FSTMutationBatch.h create mode 100644 Firestore/Source/Model/FSTMutationBatch.m create mode 100644 Firestore/Source/Model/FSTPath.h create mode 100644 Firestore/Source/Model/FSTPath.m create mode 100644 Firestore/Source/Public/FIRCollectionReference.h create mode 100644 Firestore/Source/Public/FIRDocumentChange.h create mode 100644 Firestore/Source/Public/FIRDocumentReference.h create mode 100644 Firestore/Source/Public/FIRDocumentSnapshot.h create mode 100644 Firestore/Source/Public/FIRFieldPath.h create mode 100644 Firestore/Source/Public/FIRFieldValue.h create mode 100644 Firestore/Source/Public/FIRFirestore.h create mode 100644 Firestore/Source/Public/FIRFirestoreErrors.h create mode 100644 Firestore/Source/Public/FIRFirestoreSettings.h create mode 100644 Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h create mode 100644 Firestore/Source/Public/FIRGeoPoint.h create mode 100644 Firestore/Source/Public/FIRListenerRegistration.h create mode 100644 Firestore/Source/Public/FIRQuery.h create mode 100644 Firestore/Source/Public/FIRQuerySnapshot.h create mode 100644 Firestore/Source/Public/FIRSetOptions.h create mode 100644 Firestore/Source/Public/FIRSnapshotMetadata.h create mode 100644 Firestore/Source/Public/FIRTransaction.h create mode 100644 Firestore/Source/Public/FIRWriteBatch.h create mode 100644 Firestore/Source/Remote/FSTBufferedWriter.h create mode 100644 Firestore/Source/Remote/FSTBufferedWriter.m create mode 100644 Firestore/Source/Remote/FSTDatastore.h create mode 100644 Firestore/Source/Remote/FSTDatastore.m create mode 100644 Firestore/Source/Remote/FSTExistenceFilter.h create mode 100644 Firestore/Source/Remote/FSTExistenceFilter.m create mode 100644 Firestore/Source/Remote/FSTExponentialBackoff.h create mode 100644 Firestore/Source/Remote/FSTExponentialBackoff.m create mode 100644 Firestore/Source/Remote/FSTRemoteEvent.h create mode 100644 Firestore/Source/Remote/FSTRemoteEvent.m create mode 100644 Firestore/Source/Remote/FSTRemoteStore.h create mode 100644 Firestore/Source/Remote/FSTRemoteStore.m create mode 100644 Firestore/Source/Remote/FSTSerializerBeta.h create mode 100644 Firestore/Source/Remote/FSTSerializerBeta.m create mode 100644 Firestore/Source/Remote/FSTWatchChange.h create mode 100644 Firestore/Source/Remote/FSTWatchChange.m create mode 100644 Firestore/Source/Util/FSTAssert.h create mode 100644 Firestore/Source/Util/FSTAsyncQueryListener.h create mode 100644 Firestore/Source/Util/FSTAsyncQueryListener.m create mode 100644 Firestore/Source/Util/FSTClasses.h create mode 100644 Firestore/Source/Util/FSTComparison.h create mode 100644 Firestore/Source/Util/FSTComparison.m create mode 100644 Firestore/Source/Util/FSTDispatchQueue.h create mode 100644 Firestore/Source/Util/FSTDispatchQueue.m create mode 100644 Firestore/Source/Util/FSTLogger.h create mode 100644 Firestore/Source/Util/FSTLogger.m create mode 100644 Firestore/Source/Util/FSTUsageValidation.h create mode 100644 Firestore/Source/Util/FSTUsageValidation.m create mode 100644 Firestore/Source/Util/FSTUtil.h create mode 100644 Firestore/Source/Util/FSTUtil.m create mode 100755 Firestore/test.sh create mode 100644 Firestore/third_party/Immutable/FSTArraySortedDictionary.h create mode 100644 Firestore/third_party/Immutable/FSTArraySortedDictionary.m create mode 100644 Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h create mode 100644 Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m create mode 100644 Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h create mode 100644 Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m create mode 100644 Firestore/third_party/Immutable/FSTImmutableSortedSet.h create mode 100644 Firestore/third_party/Immutable/FSTImmutableSortedSet.m create mode 100644 Firestore/third_party/Immutable/FSTLLRBEmptyNode.h create mode 100644 Firestore/third_party/Immutable/FSTLLRBEmptyNode.m create mode 100644 Firestore/third_party/Immutable/FSTLLRBNode.h create mode 100644 Firestore/third_party/Immutable/FSTLLRBValueNode.h create mode 100644 Firestore/third_party/Immutable/FSTLLRBValueNode.m create mode 100644 Firestore/third_party/Immutable/FSTTreeSortedDictionary.h create mode 100644 Firestore/third_party/Immutable/FSTTreeSortedDictionary.m create mode 100644 Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h create mode 100644 Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m create mode 100644 Firestore/third_party/Immutable/LICENSE create mode 100644 Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m create mode 100644 Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h create mode 100644 Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m create mode 100644 Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h create mode 100644 Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m create mode 100644 Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h create mode 100644 Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m diff --git a/Firebase/Core/FIRLogger.m b/Firebase/Core/FIRLogger.m index e417879..a607f58 100644 --- a/Firebase/Core/FIRLogger.m +++ b/Firebase/Core/FIRLogger.m @@ -32,6 +32,7 @@ FIRLoggerService kFIRLoggerCore = @"[Firebase/Core]"; FIRLoggerService kFIRLoggerCrash = @"[Firebase/Crash]"; FIRLoggerService kFIRLoggerDatabase = @"[Firebase/Database]"; FIRLoggerService kFIRLoggerDynamicLinks = @"[Firebase/DynamicLinks]"; +FIRLoggerService kFIRLoggerFirestore = @"[Firebase/Firestore]"; FIRLoggerService kFIRLoggerInstanceID = @"[Firebase/InstanceID]"; FIRLoggerService kFIRLoggerInvites = @"[Firebase/Invites]"; FIRLoggerService kFIRLoggerMessaging = @"[Firebase/Messaging]"; diff --git a/Firebase/Core/Private/FIRLogger.h b/Firebase/Core/Private/FIRLogger.h index 260c145..3e62fed 100644 --- a/Firebase/Core/Private/FIRLogger.h +++ b/Firebase/Core/Private/FIRLogger.h @@ -33,6 +33,7 @@ extern FIRLoggerService kFIRLoggerCore; extern FIRLoggerService kFIRLoggerCrash; extern FIRLoggerService kFIRLoggerDatabase; extern FIRLoggerService kFIRLoggerDynamicLinks; +extern FIRLoggerService kFIRLoggerFirestore; extern FIRLoggerService kFIRLoggerInstanceID; extern FIRLoggerService kFIRLoggerInvites; extern FIRLoggerService kFIRLoggerMessaging; diff --git a/FirebaseCommunity.podspec b/FirebaseCommunity.podspec index 7a8f2e4..d1c3891 100644 --- a/FirebaseCommunity.podspec +++ b/FirebaseCommunity.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCommunity' - s.version = '0.1.2' + s.version = '0.1.3' s.summary = 'Firebase Open Source Libraries for iOS.' s.description = <<-DESC diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj new file mode 100644 index 0000000..979050f --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -0,0 +1,1700 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + DE29E7F51F2174B000909613 /* AllTests */ = { + isa = PBXAggregateTarget; + buildConfigurationList = DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */; + buildPhases = ( + ); + dependencies = ( + DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */, + DE29E7FA1F2174DD00909613 /* PBXTargetDependency */, + DE29E7FC1F2174DD00909613 /* PBXTargetDependency */, + ); + name = AllTests; + productName = AllTests; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; + 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; }; + 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; }; + 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; }; + 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; }; + 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; }; + 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; }; + 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */; }; + 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; }; + 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; }; + 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; + 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */; }; + 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; }; + 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; }; + 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; }; + 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; }; + 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; }; + 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; }; + 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; + 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + 6003F598195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F596195388D20070C39A /* InfoPlist.strings */; }; + 6003F59A195388D20070C39A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F599195388D20070C39A /* main.m */; }; + 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F59D195388D20070C39A /* FIRAppDelegate.m */; }; + 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5A6195388D20070C39A /* FIRViewController.m */; }; + 6003F5A9195388D20070C39A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5A8195388D20070C39A /* Images.xcassets */; }; + 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; + 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; + 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; }; + 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; + 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; + AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; }; + C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; }; + DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */; }; + DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; + DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; + DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; + DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */; }; + DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */; }; + DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */; }; + DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */; }; + DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */; }; + DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */; }; + DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */; }; + DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */; }; + DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */; }; + DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */; }; + DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; + DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; }; + DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; }; + DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */; }; + DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; }; + DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; }; + DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; }; + DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */; }; + DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */; }; + DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */; }; + DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */; }; + DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */; }; + DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */; }; + DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */; }; + DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */; }; + DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B11F0D48AC0013853F /* FSTViewTests.m */; }; + DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */; }; + DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */; }; + DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */; }; + DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */; }; + DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */; }; + DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */; }; + DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */; }; + DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */; }; + DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */; }; + DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */; }; + DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */; }; + DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */; }; + DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */; }; + DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */; }; + DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */; }; + DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */; }; + DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */; }; + DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */; }; + DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */; }; + DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */; }; + DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */; }; + DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */; }; + DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */; }; + DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */; }; + DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1811F0D48AC0013853F /* FSTMutationTests.m */; }; + DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1821F0D48AC0013853F /* FSTPathTests.m */; }; + DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */; }; + DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */; }; + DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */; }; + DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */; }; + DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */; }; + DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */; }; + DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */; }; + DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */; }; + DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */; }; + DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1991F0D48AC0013853F /* FSTSpecTests.m */; }; + DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */; }; + DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1861F0D48AC0013853F /* FSTAssertTests.m */; }; + DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */; }; + DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; }; + DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */; }; + F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6003F5B3195388D20070C39A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F589195388D20070C39A; + remoteInfo = Firestore; + }; + DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F589195388D20070C39A; + remoteInfo = Firestore; + }; + DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE0761E31F2FE611003233AF; + remoteInfo = SwiftBuildTest; + }; + DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F5AD195388D20070C39A; + remoteInfo = Firestore_Tests; + }; + DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE03B2941F2149D600A30B9C; + remoteInfo = Firestore_IntegrationTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = ""; }; + 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; + 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = ""; }; + 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.debug.xcconfig"; sourceTree = ""; }; + 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.release.xcconfig"; sourceTree = ""; }; + 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = ""; }; + 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = existence_filter_spec_test.json; sourceTree = ""; }; + 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limbo_spec_test.json; sourceTree = ""; }; + 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limit_spec_test.json; sourceTree = ""; }; + 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = listen_spec_test.json; sourceTree = ""; }; + 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = offline_spec_test.json; sourceTree = ""; }; + 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = orderby_spec_test.json; sourceTree = ""; }; + 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = persistence_spec_test.json; sourceTree = ""; }; + 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = resume_token_spec_test.json; sourceTree = ""; }; + 54DA12A51F315EE100DD57A1 /* write_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = write_spec_test.json; sourceTree = ""; }; + 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRValidationTests.m; sourceTree = ""; }; + 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTEventAccumulator.h; sourceTree = ""; }; + 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTEventAccumulator.m; sourceTree = ""; }; + 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTIntegrationTestCase.h; sourceTree = ""; }; + 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTIntegrationTestCase.m; sourceTree = ""; }; + 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Await.h"; sourceTree = ""; }; + 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTestCase+Await.m"; sourceTree = ""; }; + 6003F58A195388D20070C39A /* Firestore_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 6003F595195388D20070C39A /* Firestore-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore-Info.plist"; sourceTree = ""; }; + 6003F597195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 6003F599195388D20070C39A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 6003F59C195388D20070C39A /* FIRAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = ""; }; + 6003F59D195388D20070C39A /* FIRAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = ""; }; + 6003F5A5195388D20070C39A /* FIRViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = ""; }; + 6003F5A6195388D20070C39A /* FIRViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = ""; }; + 6003F5A8195388D20070C39A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; + 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = ""; }; + 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.debug.xcconfig"; sourceTree = ""; }; + 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.debug.xcconfig"; sourceTree = ""; }; + B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = ""; }; + D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = ""; }; + DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = ""; }; + DE0761E41F2FE611003233AF /* SwiftBuildTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBuildTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DE0761F61F2FE68D003233AF /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTArraySortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m; sourceTree = ""; }; + DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedDictionary+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"; sourceTree = ""; }; + DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedDictionary+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m"; sourceTree = ""; }; + DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedSet+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h"; sourceTree = ""; }; + DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedSet+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m"; sourceTree = ""; }; + DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTLLRBValueNode+Test.h"; path = "../../third_party/Immutable/Tests/FSTLLRBValueNode+Test.h"; sourceTree = ""; }; + DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTTreeSortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m; sourceTree = ""; }; + DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEagerGarbageCollectorTests.m; sourceTree = ""; }; + DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBKeyTests.mm; sourceTree = ""; }; + DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBLocalStoreTests.m; sourceTree = ""; }; + DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMutationQueueTests.mm; sourceTree = ""; }; + DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBQueryCacheTests.m; sourceTree = ""; }; + DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBRemoteDocumentCacheTests.mm; sourceTree = ""; }; + DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalSerializerTests.m; sourceTree = ""; }; + DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = ""; }; + DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalStoreTests.m; sourceTree = ""; }; + DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryLocalStoreTests.m; sourceTree = ""; }; + DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryMutationQueueTests.m; sourceTree = ""; }; + DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryQueryCacheTests.m; sourceTree = ""; }; + DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryRemoteDocumentCacheTests.m; sourceTree = ""; }; + DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMutationQueueTests.h; sourceTree = ""; }; + DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationQueueTests.m; sourceTree = ""; }; + DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTPersistenceTestHelpers.h; sourceTree = ""; }; + DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPersistenceTestHelpers.m; sourceTree = ""; }; + DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTQueryCacheTests.h; sourceTree = ""; }; + DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryCacheTests.m; sourceTree = ""; }; + DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTReferenceSetTests.m; sourceTree = ""; }; + DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTRemoteDocumentCacheTests.h; sourceTree = ""; }; + DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentCacheTests.m; sourceTree = ""; }; + DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentChangeBufferTests.m; sourceTree = ""; }; + DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWriteGroupTests.mm; sourceTree = ""; }; + DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseIDTests.m; sourceTree = ""; }; + DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentKeyTests.m; sourceTree = ""; }; + DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentSetTests.m; sourceTree = ""; }; + DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentTests.m; sourceTree = ""; }; + DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTFieldValueTests.m; sourceTree = ""; }; + DE51B1811F0D48AC0013853F /* FSTMutationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationTests.m; sourceTree = ""; }; + DE51B1821F0D48AC0013853F /* FSTPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPathTests.m; sourceTree = ""; }; + DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRGeoPointTests.m; sourceTree = ""; }; + DE51B1861F0D48AC0013853F /* FSTAssertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAssertTests.m; sourceTree = ""; }; + DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTComparisonTests.m; sourceTree = ""; }; + DE51B1881F0D48AC0013853F /* FSTHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTHelpers.h; sourceTree = ""; }; + DE51B1891F0D48AC0013853F /* FSTHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTHelpers.m; sourceTree = ""; }; + DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTUtilTests.m; sourceTree = ""; }; + DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBSpecTests.m; sourceTree = ""; }; + DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemorySpecTests.m; sourceTree = ""; }; + DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMockDatastore.h; sourceTree = ""; }; + DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMockDatastore.m; sourceTree = ""; }; + DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = ""; }; + DE51B1991F0D48AC0013853F /* FSTSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSpecTests.m; sourceTree = ""; }; + DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = ""; }; + DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSyncEngineTestDriver.m; sourceTree = ""; }; + DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseInfoTests.m; sourceTree = ""; }; + DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEventManagerTests.m; sourceTree = ""; }; + DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryListenerTests.m; sourceTree = ""; }; + DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryTests.m; sourceTree = ""; }; + DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = ""; }; + DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTargetIDGeneratorTests.m; sourceTree = ""; }; + DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTimestampTests.m; sourceTree = ""; }; + DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewSnapshotTest.m; sourceTree = ""; }; + DE51B1B11F0D48AC0013853F /* FSTViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewTests.m; sourceTree = ""; }; + DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = ""; }; + DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteEventTests.m; sourceTree = ""; }; + DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSerializerBetaTests.m; sourceTree = ""; }; + DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = ""; }; + DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = ""; }; + DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FSTWatchChange+Testing.m"; sourceTree = ""; }; + DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTWatchChangeTests.m; sourceTree = ""; }; + DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCursorTests.m; sourceTree = ""; }; + DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDatabaseTests.m; sourceTree = ""; }; + DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldsTests.m; sourceTree = ""; }; + DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRListenerRegistrationTests.m; sourceTree = ""; }; + DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = ""; }; + DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRServerTimestampTests.m; sourceTree = ""; }; + DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRTypeTests.m; sourceTree = ""; }; + DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = ""; }; + DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSmokeTests.m; sourceTree = ""; }; + DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTransactionTests.m; sourceTree = ""; }; + DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRWriteBatchTests.m; sourceTree = ""; }; + F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6003F587195388D20070C39A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, + 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, + 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, + 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6003F5AB195388D20070C39A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, + 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, + 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, + F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE03B2D31F2149D600A30B9C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */, + DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */, + DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */, + DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */, + AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE0761E11F2FE611003233AF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6003F581195388D10070C39A = { + isa = PBXGroup; + children = ( + 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */, + 6003F593195388D20070C39A /* Example for Firestore */, + 6003F5B5195388D20070C39A /* Tests */, + DE0761E51F2FE611003233AF /* SwiftBuildTest */, + 6003F58C195388D20070C39A /* Frameworks */, + 6003F58B195388D20070C39A /* Products */, + A47A1BF74A48BCAEAFBCBF1E /* Pods */, + ); + sourceTree = ""; + }; + 6003F58B195388D20070C39A /* Products */ = { + isa = PBXGroup; + children = ( + 6003F58A195388D20070C39A /* Firestore_Example.app */, + 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */, + DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */, + DE0761E41F2FE611003233AF /* SwiftBuildTest.app */, + ); + name = Products; + sourceTree = ""; + }; + 6003F58C195388D20070C39A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6003F58D195388D20070C39A /* Foundation.framework */, + 6003F58F195388D20070C39A /* CoreGraphics.framework */, + 6003F591195388D20070C39A /* UIKit.framework */, + 6003F5AF195388D20070C39A /* XCTest.framework */, + 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */, + 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */, + B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */, + 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6003F593195388D20070C39A /* Example for Firestore */ = { + isa = PBXGroup; + children = ( + 6003F59C195388D20070C39A /* FIRAppDelegate.h */, + 6003F59D195388D20070C39A /* FIRAppDelegate.m */, + 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */, + 6003F5A5195388D20070C39A /* FIRViewController.h */, + 6003F5A6195388D20070C39A /* FIRViewController.m */, + 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */, + 6003F5A8195388D20070C39A /* Images.xcassets */, + 6003F594195388D20070C39A /* Supporting Files */, + ); + name = "Example for Firestore"; + path = Firestore; + sourceTree = ""; + }; + 6003F594195388D20070C39A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 6003F595195388D20070C39A /* Firestore-Info.plist */, + 6003F596195388D20070C39A /* InfoPlist.strings */, + 6003F599195388D20070C39A /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 6003F5B5195388D20070C39A /* Tests */ = { + isa = PBXGroup; + children = ( + DE51B1831F0D48AC0013853F /* API */, + DE51B1A81F0D48AC0013853F /* Core */, + DE2EF06E1F3D07D7003D0CDC /* Immutable */, + DE51B1BB1F0D48AC0013853F /* Integration */, + DE51B1621F0D48AC0013853F /* Local */, + DE51B17B1F0D48AC0013853F /* Model */, + DE51B1B21F0D48AC0013853F /* Remote */, + DE51B1931F0D48AC0013853F /* SpecTests */, + DE51B1851F0D48AC0013853F /* Util */, + 6003F5B6195388D20070C39A /* Supporting Files */, + ); + path = Tests; + sourceTree = ""; + }; + 6003F5B6195388D20070C39A /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 6003F5B7195388D20070C39A /* Tests-Info.plist */, + 6003F5B8195388D20070C39A /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = { + isa = PBXGroup; + children = ( + 8E002F4AD5D9B6197C940847 /* Firestore.podspec */, + D3CC3DC5338DCAF43A211155 /* README.md */, + 12F4357299652983A615F886 /* LICENSE */, + ); + name = "Podspec Metadata"; + sourceTree = ""; + }; + A47A1BF74A48BCAEAFBCBF1E /* Pods */ = { + isa = PBXGroup; + children = ( + 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */, + 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */, + 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */, + DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */, + CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */, + 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */, + 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */, + F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + DE0761E51F2FE611003233AF /* SwiftBuildTest */ = { + isa = PBXGroup; + children = ( + DE0761F61F2FE68D003233AF /* main.swift */, + ); + path = SwiftBuildTest; + sourceTree = ""; + }; + DE2EF06E1F3D07D7003D0CDC /* Immutable */ = { + isa = PBXGroup; + children = ( + DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */, + DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */, + DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */, + DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */, + DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */, + DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */, + DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */, + ); + name = Immutable; + sourceTree = ""; + }; + DE51B1621F0D48AC0013853F /* Local */ = { + isa = PBXGroup; + children = ( + DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */, + DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */, + DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */, + DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */, + DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */, + DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */, + DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */, + DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */, + DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */, + DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */, + DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */, + DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */, + DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */, + DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */, + DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */, + DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */, + DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */, + DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */, + DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */, + DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */, + DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */, + DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */, + DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */, + DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */, + ); + path = Local; + sourceTree = ""; + }; + DE51B17B1F0D48AC0013853F /* Model */ = { + isa = PBXGroup; + children = ( + DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */, + DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */, + DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */, + DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */, + DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */, + DE51B1811F0D48AC0013853F /* FSTMutationTests.m */, + DE51B1821F0D48AC0013853F /* FSTPathTests.m */, + ); + path = Model; + sourceTree = ""; + }; + DE51B1831F0D48AC0013853F /* API */ = { + isa = PBXGroup; + children = ( + DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */, + ); + path = API; + sourceTree = ""; + }; + DE51B1851F0D48AC0013853F /* Util */ = { + isa = PBXGroup; + children = ( + 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */, + 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */, + 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */, + 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */, + DE51B1861F0D48AC0013853F /* FSTAssertTests.m */, + DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */, + DE51B1881F0D48AC0013853F /* FSTHelpers.h */, + DE51B1891F0D48AC0013853F /* FSTHelpers.m */, + DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */, + 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */, + 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */, + ); + path = Util; + sourceTree = ""; + }; + DE51B1931F0D48AC0013853F /* SpecTests */ = { + isa = PBXGroup; + children = ( + DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */, + DE51B1981F0D48AC0013853F /* FSTSpecTests.h */, + DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */, + DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */, + DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */, + DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */, + DE51B1991F0D48AC0013853F /* FSTSpecTests.m */, + DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */, + DE51B19C1F0D48AC0013853F /* json */, + ); + path = SpecTests; + sourceTree = ""; + }; + DE51B19C1F0D48AC0013853F /* json */ = { + isa = PBXGroup; + children = ( + 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */, + 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */, + 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */, + 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */, + 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */, + 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */, + 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */, + 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */, + 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */, + 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */, + 54DA12A51F315EE100DD57A1 /* write_spec_test.json */, + DE51B1A71F0D48AC0013853F /* README.md */, + ); + path = json; + sourceTree = ""; + }; + DE51B1A81F0D48AC0013853F /* Core */ = { + isa = PBXGroup; + children = ( + DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */, + DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */, + DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */, + DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */, + DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */, + DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */, + DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */, + DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */, + DE51B1B11F0D48AC0013853F /* FSTViewTests.m */, + ); + path = Core; + sourceTree = ""; + }; + DE51B1B21F0D48AC0013853F /* Remote */ = { + isa = PBXGroup; + children = ( + DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */, + DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */, + DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */, + DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */, + DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */, + DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */, + DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */, + ); + path = Remote; + sourceTree = ""; + }; + DE51B1BB1F0D48AC0013853F /* Integration */ = { + isa = PBXGroup; + children = ( + DE03B3621F215E1600A30B9C /* CAcert.pem */, + DE51B1BC1F0D48AC0013853F /* API */, + DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */, + DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */, + DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */, + DE51B1C71F0D48AC0013853F /* Util */, + ); + path = Integration; + sourceTree = ""; + }; + DE51B1BC1F0D48AC0013853F /* API */ = { + isa = PBXGroup; + children = ( + DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */, + DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */, + DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */, + DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */, + DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */, + DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */, + DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */, + 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */, + DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */, + ); + path = API; + sourceTree = ""; + }; + DE51B1C71F0D48AC0013853F /* Util */ = { + isa = PBXGroup; + children = ( + ); + path = Util; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6003F589195388D20070C39A /* Firestore_Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */; + buildPhases = ( + FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */, + 6003F586195388D20070C39A /* Sources */, + 6003F587195388D20070C39A /* Frameworks */, + 6003F588195388D20070C39A /* Resources */, + 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */, + DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Firestore_Example; + productName = Firestore; + productReference = 6003F58A195388D20070C39A /* Firestore_Example.app */; + productType = "com.apple.product-type.application"; + }; + 6003F5AD195388D20070C39A /* Firestore_Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */; + buildPhases = ( + 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */, + 6003F5AA195388D20070C39A /* Sources */, + 6003F5AB195388D20070C39A /* Frameworks */, + 6003F5AC195388D20070C39A /* Resources */, + BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */, + AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6003F5B4195388D20070C39A /* PBXTargetDependency */, + ); + name = Firestore_Tests; + productName = FirestoreTests; + productReference = 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */; + buildPhases = ( + DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */, + DE03B2981F2149D600A30B9C /* Sources */, + DE03B2D31F2149D600A30B9C /* Frameworks */, + DE03B2D81F2149D600A30B9C /* Resources */, + DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */, + DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + DE03B2951F2149D600A30B9C /* PBXTargetDependency */, + ); + name = Firestore_IntegrationTests; + productName = FirestoreTests; + productReference = DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DE0761E31F2FE611003233AF /* SwiftBuildTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */; + buildPhases = ( + 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */, + DE0761E01F2FE611003233AF /* Sources */, + DE0761E11F2FE611003233AF /* Frameworks */, + DE0761E21F2FE611003233AF /* Resources */, + 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */, + 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftBuildTest; + productName = SwiftBuildTest; + productReference = DE0761E41F2FE611003233AF /* SwiftBuildTest.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6003F582195388D10070C39A /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = FIR; + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 6003F5AD195388D20070C39A = { + DevelopmentTeam = EQHXZ8M8AV; + TestTargetID = 6003F589195388D20070C39A; + }; + DE03B2941F2149D600A30B9C = { + DevelopmentTeam = EQHXZ8M8AV; + }; + DE0761E31F2FE611003233AF = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = EQHXZ8M8AV; + ProvisioningStyle = Automatic; + }; + DE29E7F51F2174B000909613 = { + CreatedOnToolsVersion = 9.0; + DevelopmentTeam = EQHXZ8M8AV; + }; + }; + }; + buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6003F581195388D10070C39A; + productRefGroup = 6003F58B195388D20070C39A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6003F589195388D20070C39A /* Firestore_Example */, + 6003F5AD195388D20070C39A /* Firestore_Tests */, + DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */, + DE29E7F51F2174B000909613 /* AllTests */, + DE0761E31F2FE611003233AF /* SwiftBuildTest */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6003F588195388D20070C39A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */, + 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */, + 6003F5A9195388D20070C39A /* Images.xcassets in Resources */, + 6003F598195388D20070C39A /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6003F5AC195388D20070C39A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */, + 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */, + 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */, + 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */, + 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */, + 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */, + 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */, + 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */, + 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */, + 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */, + 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */, + 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE03B2D81F2149D600A30B9C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */, + DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE0761E21F2FE611003233AF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework", + "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework", + "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SwiftBuildTest-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Firestore_IntegrationTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Firestore_Example-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6003F586195388D20070C39A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */, + 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */, + 6003F59A195388D20070C39A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6003F5AA195388D20070C39A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */, + DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */, + DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */, + DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */, + DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */, + DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */, + DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */, + DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */, + DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */, + DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */, + DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */, + DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */, + DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */, + DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */, + DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */, + DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */, + DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */, + 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */, + DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */, + DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */, + DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */, + 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */, + DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */, + DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */, + DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */, + DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */, + DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */, + DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */, + DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */, + DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */, + DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */, + DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */, + DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */, + DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */, + 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, + DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */, + DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */, + DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */, + DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */, + DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */, + DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */, + DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */, + DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */, + DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */, + DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */, + DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */, + DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */, + DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */, + DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */, + DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */, + DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */, + DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */, + DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */, + DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */, + DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */, + DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */, + DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE03B2981F2149D600A30B9C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */, + DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */, + 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */, + DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */, + DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */, + 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, + DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */, + 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */, + DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */, + DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */, + DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */, + DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */, + DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */, + DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */, + DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */, + 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE0761E01F2FE611003233AF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE0761F81F2FE68D003233AF /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 6003F5B4195388D20070C39A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F589195388D20070C39A /* Firestore_Example */; + targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */; + }; + DE03B2951F2149D600A30B9C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F589195388D20070C39A /* Firestore_Example */; + targetProxy = DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */; + }; + DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DE0761E31F2FE611003233AF /* SwiftBuildTest */; + targetProxy = DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */; + }; + DE29E7FA1F2174DD00909613 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F5AD195388D20070C39A /* Firestore_Tests */; + targetProxy = DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */; + }; + DE29E7FC1F2174DD00909613 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */; + targetProxy = DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 6003F596195388D20070C39A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 6003F597195388D20070C39A /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 6003F5B8195388D20070C39A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 6003F5B9195388D20070C39A /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 71719F9E1E33DC2100824A3D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 6003F5BD195388D20070C39A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6003F5BE195388D20070C39A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6003F5C0195388D20070C39A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"", + "\"${PODS_ROOT}/leveldb-library/\"", + "\"${PODS_ROOT}/leveldb-library/include\"", + ); + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + MODULE_NAME = ExampleApp; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 6003F5C1195388D20070C39A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"", + "\"${PODS_ROOT}/leveldb-library/\"", + "\"${PODS_ROOT}/leveldb-library/include\"", + ); + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + MODULE_NAME = ExampleApp; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 6003F5C3195388D20070C39A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../\"", + "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 6003F5C4195388D20070C39A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../Protos/objc/firebase/datastore/clients/proto\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + "\"${PODS_ROOT}/../../\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + DE03B2E71F2149D600A30B9C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + "\"${PODS_ROOT}/../../\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-l\"c++\"", + "-framework", + "\"OCMock\"", + "-framework", + "\"leveldb\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + DE03B2E81F2149D600A30B9C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"$(inherited)\"", + "\"${PODS_ROOT}/../../Source\"", + "\"${PODS_ROOT}/../../Source/API\"", + "\"${PODS_ROOT}/../../Source/Core\"", + "\"${PODS_ROOT}/../../Source/Remote\"", + "\"${PODS_ROOT}/../../Source/Model\"", + "\"${PODS_ROOT}/../../third_party\"", + "\"${PODS_ROOT}/../../third_party/Immutable\"", + "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"", + "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"", + "\"${PODS_ROOT}/../../Protos/objc/google/api\"", + "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"", + "\"${PODS_ROOT}/../../Protos/objc/google/type\"", + "\"${PODS_ROOT}/../../\"", + ); + INFOPLIST_FILE = "Tests/Tests-Info.plist"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-l\"c++\"", + "-framework", + "\"OCMock\"", + "-framework", + "\"leveldb\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + DE0761F31F2FE611003233AF /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DE0761F41F2FE611003233AF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DE29E7F61F2174B000909613 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = EQHXZ8M8AV; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + DE29E7F71F2174B000909613 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = EQHXZ8M8AV; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6003F5BD195388D20070C39A /* Debug */, + 6003F5BE195388D20070C39A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6003F5C0195388D20070C39A /* Debug */, + 6003F5C1195388D20070C39A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6003F5C3195388D20070C39A /* Debug */, + 6003F5C4195388D20070C39A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE03B2E71F2149D600A30B9C /* Debug */, + DE03B2E81F2149D600A30B9C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE0761F31F2FE611003233AF /* Debug */, + DE0761F41F2FE611003233AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE29E7F61F2174B000909613 /* Debug */, + DE29E7F71F2174B000909613 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6003F582195388D10070C39A /* Project object */; +} diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme new file mode 100644 index 0000000..aacb70e --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme new file mode 100644 index 0000000..a8538f5 --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme new file mode 100644 index 0000000..ccd099f --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme new file mode 100644 index 0000000..920e1f3 --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme new file mode 100644 index 0000000..112a0d0 --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..66a7681 --- /dev/null +++ b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore/Base.lproj/Main.storyboard b/Firestore/Example/Firestore/Base.lproj/Main.storyboard new file mode 100644 index 0000000..d164a23 --- /dev/null +++ b/Firestore/Example/Firestore/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Example/Firestore/FIRAppDelegate.h b/Firestore/Example/Firestore/FIRAppDelegate.h new file mode 100644 index 0000000..1eb5040 --- /dev/null +++ b/Firestore/Example/Firestore/FIRAppDelegate.h @@ -0,0 +1,23 @@ +/* + * 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 UIKit; + +@interface FIRAppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/Firestore/Example/Firestore/FIRAppDelegate.m b/Firestore/Example/Firestore/FIRAppDelegate.m new file mode 100644 index 0000000..12ca249 --- /dev/null +++ b/Firestore/Example/Firestore/FIRAppDelegate.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 "FIRAppDelegate.h" + +@implementation FIRAppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for + // certain types of temporary interruptions (such as an incoming phone call or SMS message) or + // when the user quits the application and it begins the transition to the background state. Use + // this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. + // Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store + // enough application state information to restore your application to its current state in case + // it is terminated later. If your application supports background execution, this method is + // called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo + // many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If + // the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. +} + +@end diff --git a/Firestore/Example/Firestore/FIRViewController.h b/Firestore/Example/Firestore/FIRViewController.h new file mode 100644 index 0000000..64b4b74 --- /dev/null +++ b/Firestore/Example/Firestore/FIRViewController.h @@ -0,0 +1,21 @@ +/* + * 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 UIKit; + +@interface FIRViewController : UIViewController + +@end diff --git a/Firestore/Example/Firestore/FIRViewController.m b/Firestore/Example/Firestore/FIRViewController.m new file mode 100644 index 0000000..cdad545 --- /dev/null +++ b/Firestore/Example/Firestore/FIRViewController.m @@ -0,0 +1,35 @@ +/* + * 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 "FIRViewController.h" + +@interface FIRViewController () + +@end + +@implementation FIRViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/Firestore/Example/Firestore/Firestore-Info.plist b/Firestore/Example/Firestore/Firestore-Info.plist new file mode 100644 index 0000000..7576a0d --- /dev/null +++ b/Firestore/Example/Firestore/Firestore-Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d7070bc --- /dev/null +++ b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Firestore/Example/Firestore/en.lproj/InfoPlist.strings b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Firestore/Example/Firestore/main.m b/Firestore/Example/Firestore/main.m new file mode 100644 index 0000000..724fccf --- /dev/null +++ b/Firestore/Example/Firestore/main.m @@ -0,0 +1,24 @@ +/* + * 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 UIKit; +#import "FIRAppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class])); + } +} diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile new file mode 100644 index 0000000..4008af7 --- /dev/null +++ b/Firestore/Example/Podfile @@ -0,0 +1,22 @@ +use_frameworks! +platform :ios, '8.0' + +target 'Firestore_Example' do + pod 'FirebaseCommunity/Core', :path => '../../' + pod 'Firestore', :path => '../' + + target 'Firestore_Tests' do + inherit! :search_paths + pod 'OCMock' + pod 'leveldb-library' + end + + target 'Firestore_IntegrationTests' do + inherit! :search_paths + pod 'OCMock' + end +end + +target 'SwiftBuildTest' do + pod 'Firestore', :path => '../' +end diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift new file mode 100644 index 0000000..ff6c0dd --- /dev/null +++ b/Firestore/Example/SwiftBuildTest/main.swift @@ -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 Foundation + +import Firestore + +func main() { + let db = initializeDb(); + + let (collectionRef, documentRef) = makeRefs(database: db); + + let query = makeQuery(collection: collectionRef); + + writeDocument(at: documentRef); + + addDocument(to: collectionRef); + + readDocument(at: documentRef); + + readDocuments(matching: query); + + listenToDocument(at: documentRef); + + listenToDocuments(matching: query); + + types(); +} + +func initializeDb() -> Firestore { + + // Initialize with ProjectID. + let firestore = Firestore.firestore() + + // Apply settings + let settings = FirestoreSettings() + settings.host = "localhost" + settings.isPersistenceEnabled = true + firestore.settings = settings + + return firestore; +} + +func makeRefs(database db: Firestore) -> (CollectionReference, DocumentReference) { + + var collectionRef = db.collection("my-collection") + + var documentRef: DocumentReference; + documentRef = collectionRef.document("my-doc") + // or + documentRef = db.document("my-collection/my-doc") + + // deeper collection (my-collection/my-doc/some/deep/collection) + collectionRef = documentRef.collection("some/deep/collection") + + // parent doc (my-collection/my-doc/some/deep) + documentRef = collectionRef.parent! + + // print paths. + print("Collection: \(collectionRef.path), document: \(documentRef.path)") + + return (collectionRef, documentRef); +} + +func makeQuery(collection collectionRef: CollectionReference) -> Query { + + let query = collectionRef.whereField(FieldPath(["name"]), isEqualTo: "Fred") + .whereField("age", isGreaterThanOrEqualTo: 24) + .whereField(FieldPath.documentID(), isEqualTo: "fred") + .order(by: FieldPath(["age"])) + .order(by: "name", descending: true) + .limit(to: 10) + + return query; +} + +func writeDocument(at docRef: DocumentReference) { + + let setData = [ + "foo": 42, + "bar": [ + "baz": "Hello world!" + ] + ] as [String : Any]; + + let updateData = [ + "bar.baz": 42, + FieldPath(["foobar"]) : 42 + ] as [AnyHashable : Any]; + + docRef.setData(setData) + + // Completion callback (via trailing closure syntax). + docRef.setData(setData) { error in + if let error = error { + print("Uh oh! \(error)") + return + } + + print("Set complete!") + } + + // SetOptions + docRef.setData(setData, options:SetOptions.merge()) + + docRef.updateData(updateData) + docRef.delete(); + + docRef.delete() { error in + if let error = error { + print("Uh oh! \(error)") + return + } + + print("Set complete!") + } +} + +func addDocument(to collectionRef: CollectionReference) { + + collectionRef.addDocument(data: ["foo": 42]); + //or + collectionRef.document().setData(["foo": 42]); +} + +func readDocument(at docRef: DocumentReference) { + + // Trailing closure syntax. + docRef.getDocument() { document, error in + if let document = document { + // NOTE that document is nullable. + let data = document.data(); + print("Read document: \(data)") + + // Fields are read via subscript notation. + if let foo = document["foo"] { + print("Field: \(foo)") + } + } else { + // TODO(mikelehen): There may be a better way to do this, but it at least demonstrates + // the swift error domain / enum codes are renamed appropriately. + if let errorCode = error.flatMap({ + ($0._domain == FirestoreErrorDomain) ? FirestoreErrorCode (rawValue: $0._code) : nil + }) { + switch errorCode { + case .unavailable: + print("Can't read document due to being offline!") + case _: + print("Failed to read.") + } + } else { + print("Unknown error!") + } + } + + } +} + +func readDocuments(matching query: Query) { + query.getDocuments() { querySnapshot, error in + // TODO(mikelehen): Figure out how to make "for..in" syntax work + // directly on documentSet. + for document in querySnapshot!.documents { + print(document.data()) + } + } +} + +func listenToDocument(at docRef: DocumentReference) { + + let listener = docRef.addSnapshotListener() { document, error in + if let error = error { + print("Uh oh! Listen canceled: \(error)") + return + } + + if let document = document { + print("Current document: \(document.data())"); + if (document.metadata.isFromCache) { + print("From Cache") + } else { + print("From Server") + } + } + } + + // Unsubscribe. + listener.remove(); +} + +func listenToDocuments(matching query: Query) { + + let listener = query.addSnapshotListener() { snap, error in + if let error = error { + print("Uh oh! Listen canceled: \(error)") + return + } + + if let snap = snap { + print("NEW SNAPSHOT (empty=\(snap.isEmpty) count=\(snap.count)") + + // TODO(mikelehen): Figure out how to make "for..in" syntax work + // directly on documentSet. + for document in snap.documents { + print("Doc: ", document.data()) + } + } + } + + // Unsubscribe + listener.remove(); +} + +func listenToQueryDiffs(onQuery query: Query) { + + let listener = query.addSnapshotListener() { snap, error in + if let snap = snap { + for change in snap.documentChanges { + switch (change.type) { + case .added: + print("New document: \(change.document.data())") + case .modified: + print("Modified document: \(change.document.data())") + case .removed: + print("Removed document: \(change.document.data())") + } + } + } + } + + // Unsubscribe + listener.remove(); +} + +func transactions() { + let db = Firestore.firestore() + + let collectionRef = db.collection("cities") + let accA = collectionRef.document("accountA") + let accB = collectionRef.document("accountB") + let amount = 20.0 + + db.runTransaction({ (transaction, errorPointer) -> Any? in + do { + let balanceA = try transaction.getDocument(accA)["balance"] as! Double + let balanceB = try transaction.getDocument(accB)["balance"] as! Double + + if (balanceA < amount) { + errorPointer?.pointee = NSError(domain: "Foo", code: 123, userInfo: nil) + return nil + } + transaction.updateData(["balance": balanceA - amount], forDocument:accA) + transaction.updateData(["balance": balanceB + amount], forDocument:accB) + } catch let error as NSError { + print("Uh oh! \(error)") + } + return 0 + }) { (result, error) in + // handle result. + } +} + +func types() { + // Just highlighting the types of everything, though devs can/will often omit them. + let _: Firestore; + let _: CollectionReference; + let _: DocumentReference; + let _: Query; + let _: DocumentSnapshot; + let _: QuerySnapshot; +} diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.m b/Firestore/Example/Tests/API/FIRGeoPointTests.m new file mode 100644 index 0000000..ca7d24c --- /dev/null +++ b/Firestore/Example/Tests/API/FIRGeoPointTests.m @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/FIRGeoPoint.h" + +#import + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRGeoPointTests : XCTestCase +@end + +@implementation FIRGeoPointTests + +- (void)testEquals { + XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0]); + XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56], + [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]); + XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[FIRGeoPoint alloc] initWithLatitude:1 longitude:0]); + XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[FIRGeoPoint alloc] initWithLatitude:0 longitude:1]); + XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], + [[NSObject alloc] init]); +} + +- (void)testComparison { + NSArray *values = @[ + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:-180] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:0] ], + @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:180] ], + ]; + + FSTAssertComparisons(values); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m new file mode 100644 index 0000000..931a59f --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Core/FSTDatabaseInfo.h" + +#import + +#import "Model/FSTDatabaseID.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDatabaseInfoTests : XCTestCase +@end + +@implementation FSTDatabaseInfoTests + +- (void)testConstructor { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"pk" + host:@"h" + sslEnabled:YES]; + XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"d"); + XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); + XCTAssertEqualObjects(databaseInfo.host, @"h"); + XCTAssertEqual(databaseInfo.sslEnabled, YES); +} + +- (void)testDefaultDatabase { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"pk" + host:@"h" + sslEnabled:YES]; + XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"(default)"); + XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk"); + XCTAssertEqualObjects(databaseInfo.host, @"h"); + XCTAssertEqual(databaseInfo.sslEnabled, YES); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.m b/Firestore/Example/Tests/Core/FSTEventManagerTests.m new file mode 100644 index 0000000..85afe58 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.m @@ -0,0 +1,163 @@ +/* + * 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 "Core/FSTEventManager.h" + +#import +#import + +#import "Core/FSTQuery.h" +#import "Core/FSTSyncEngine.h" +#import "Model/FSTDocumentSet.h" +#import "Util/FSTDispatchQueue.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +// FSTEventManager implements this delegate privately +@interface FSTEventManager () +@end + +@interface FSTEventManagerTests : XCTestCase +@end + +@implementation FSTEventManagerTests { + FSTDispatchQueue *_testUserQueue; +} + +- (void)setUp { + _testUserQueue = [FSTDispatchQueue queueWith:dispatch_get_main_queue()]; +} + +- (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query { + return [[FSTQueryListener alloc] + initWithQuery:query + options:[FSTListenOptions defaultOptions] + viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; +} + +- (void)testHandlesManyListenersPerQuery { + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQueryListener *listener1 = [self noopListenerForQuery:query]; + FSTQueryListener *listener2 = [self noopListenerForQuery:query]; + + FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + OCMExpect([syncEngineMock listenToQuery:query]); + [eventManager addListener:listener1]; + OCMVerifyAll((id)syncEngineMock); + + [eventManager addListener:listener2]; + [eventManager removeListener:listener2]; + + OCMExpect([syncEngineMock stopListeningToQuery:query]); + [eventManager removeListener:listener1]; + OCMVerifyAll((id)syncEngineMock); +} + +- (void)testHandlesUnlistenOnUnknownListenerGracefully { + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQueryListener *listener = [self noopListenerForQuery:query]; + + FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager removeListener:listener]; + OCMVerifyAll((id)syncEngineMock); +} + +- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query + viewSnapshotHandler:(void (^)())handler { + FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]); + OCMStub([listener query]).andReturn(query); + OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + handler(); + }); + return listener; +} + +- (void)testNotifiesListenersInTheRightOrder { + FSTQuery *query1 = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQuery *query2 = [FSTQuery queryWithPath:FSTTestPath(@"bar/baz")]; + NSMutableArray *eventOrder = [NSMutableArray array]; + + FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener1"]; + }]; + + FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener2"]; + }]; + + FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1 + viewSnapshotHandler:^{ + [eventOrder addObject:@"listener3"]; + }]; + + FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager addListener:listener1]; + [eventManager addListener:listener2]; + [eventManager addListener:listener3]; + OCMVerify([syncEngineMock listenToQuery:query1]); + OCMVerify([syncEngineMock listenToQuery:query2]); + + FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]); + OCMStub([snapshot1 query]).andReturn(query1); + FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]); + OCMStub([snapshot2 query]).andReturn(query2); + + [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]]; + + NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ]; + XCTAssertEqualObjects(eventOrder, expected); +} + +- (void)testWillForwardOnlineStateChanges { + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]); + NSMutableArray *events = [NSMutableArray array]; + OCMStub([fakeListener query]).andReturn(query); + OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateUnknown]) + .andDo(^(NSInvocation *invocation) { + [events addObject:@(FSTOnlineStateUnknown)]; + }); + OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateHealthy]) + .andDo(^(NSInvocation *invocation) { + [events addObject:@(FSTOnlineStateHealthy)]; + }); + + FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); + OCMExpect([syncEngineMock setDelegate:[OCMArg any]]); + FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; + + [eventManager addListener:fakeListener]; + XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]); + [eventManager watchStreamDidChangeOnlineState:FSTOnlineStateHealthy]; + XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m new file mode 100644 index 0000000..155eb00 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m @@ -0,0 +1,487 @@ +/* + * 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 "Core/FSTEventManager.h" + +#import + +#import "Core/FSTQuery.h" +#import "Core/FSTView.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentSet.h" +#import "Remote/FSTRemoteEvent.h" +#import "Util/FSTAsyncQueryListener.h" +#import "Util/FSTDispatchQueue.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTQueryListenerTests : XCTestCase +@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue; +@end + +@implementation FSTQueryListenerTests + +- (void)setUp { + _asyncQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)]; +} + +- (void)testRaisesCollectionEvents { + NSMutableArray *accum = [NSMutableArray array]; + NSMutableArray *otherAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc2prime = + FSTTestDoc(@"rooms/Hades", 3, @{@"name" : @"Hades", @"owner" : @"Jonny"}, NO); + + FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; + FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeModified]; + FSTDocumentViewChange *change4 = + [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeAdded]; + + [listener queryDidChangeViewSnapshot:snap1]; + [listener queryDidChangeViewSnapshot:snap2]; + [otherListener queryDidChangeViewSnapshot:snap2]; + + XCTAssertEqualObjects(accum, (@[ snap1, snap2 ])); + XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ])); + + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] + initWithQuery:snap2.query + documents:snap2.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator] + documentChanges:@[ change1, change4 ] + fromCache:snap2.fromCache + hasPendingWrites:snap2.hasPendingWrites + syncStateChanged:YES]; + XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ])); +} + +- (void)testRaisesErrorEvent { + NSMutableArray *accum = [NSMutableArray array]; + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")]; + + FSTQueryListener *listener = [self listenToQuery:query + handler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:error]; + }]; + + NSError *testError = + [NSError errorWithDomain:@"com.google.firestore.test" code:42 userInfo:@{@"some" : @"info"}]; + [listener queryDidError:testError]; + + XCTAssertEqualObjects(accum, @[ testError ]); +} + +- (void)testRaisesEventForEmptyCollectionAfterSync { + NSMutableArray *accum = [NSMutableArray array]; + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + + FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + FSTTargetChange *ackTarget = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + + [listener queryDidChangeViewSnapshot:snap1]; + XCTAssertEqualObjects(accum, @[]); + + [listener queryDidChangeViewSnapshot:snap2]; + XCTAssertEqualObjects(accum, @[ snap2 ]); +} + +- (void)testMutingAsyncListenerPreventsAllSubsequentEvents { + NSMutableArray *accum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO); + + __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc] + initWithDispatchQueue:self.asyncQueue + snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:snapshot]; + [listener mute]; + }]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler; + handler(viewSnapshot1, nil); + handler(viewSnapshot2, nil); + + // Drain queue + XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; + [self.asyncQueue dispatchAsync:^{ + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:4.0 + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; + + // We should get the first snapshot but not the second. + XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]); +} + +- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { + NSMutableArray *filteredAccum = [NSMutableArray array]; + NSMutableArray *fullAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + + FSTTargetChange *ackTarget = + [FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + [filteredListener queryDidChangeViewSnapshot:snap1]; // local event + [filteredListener queryDidChangeViewSnapshot:snap2]; // no event + [filteredListener queryDidChangeViewSnapshot:snap3]; // doc2 update + + [fullListener queryDidChangeViewSnapshot:snap1]; // local event + [fullListener queryDidChangeViewSnapshot:snap2]; // state change event + [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update + + XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); +} + +- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { + NSMutableArray *filteredAccum = [NSMutableArray array]; + NSMutableArray *fullAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:NO]; + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc1Prime type:FSTDocumentViewChangeTypeMetadata]; + FSTDocumentViewChange *change4 = + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; + + [filteredListener queryDidChangeViewSnapshot:snap1]; + [filteredListener queryDidChangeViewSnapshot:snap2]; + [filteredListener queryDidChangeViewSnapshot:snap3]; + [fullListener queryDidChangeViewSnapshot:snap1]; + [fullListener queryDidChangeViewSnapshot:snap2]; + [fullListener queryDidChangeViewSnapshot:snap3]; + + XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ])); + XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ])); + + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); + XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ])); + XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ])); + XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ])); +} + +- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { + NSMutableArray *fullAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + FSTQueryListener *fullListener = + [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); + FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); + FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil); + + [fullListener queryDidChangeViewSnapshot:snap1]; + [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events. + [fullListener queryDidChangeViewSnapshot:snap3]; + [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event. + + FSTViewSnapshot *expectedSnap4 = [[FSTViewSnapshot alloc] initWithQuery:snap4.query + documents:snap4.documents + oldDocuments:snap3.documents + documentChanges:@[] + fromCache:snap4.fromCache + hasPendingWrites:NO + syncStateChanged:snap4.syncStateChanged]; + XCTAssertEqualObjects(fullAccum, (@[ snap1, snap3, expectedSnap4 ])); +} + +- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { + NSMutableArray *filteredAccum = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO); + + FSTQueryListener *filteredListener = + [self listenToQuery:query accumulatingSnapshots:filteredAccum]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); + + FSTDocumentViewChange *change3 = + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]; + + [filteredListener queryDidChangeViewSnapshot:snap1]; + [filteredListener queryDidChangeViewSnapshot:snap2]; + + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query + documents:snap2.documents + oldDocuments:snap1.documents + documentChanges:@[ change3 ] + fromCache:snap2.isFromCache + hasPendingWrites:snap2.hasPendingWrites + syncStateChanged:snap2.syncStateChanged]; + XCTAssertEqualObjects(filteredAccum, (@[ snap1, expectedSnap2 ])); +} + +- (void)testWillWaitForSyncIfOnline { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTQueryListener *listener = + [self listenToQuery:query + options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:YES] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + FSTViewSnapshot *snap3 = + FSTTestApplyChanges(view, @[], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; + [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; + [listener queryDidChangeViewSnapshot:snap2]; + [listener queryDidChangeViewSnapshot:snap3]; + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:snap3.query + documents:snap3.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator] + documentChanges:@[ change1, change2 ] + fromCache:NO + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (void)testWillRaiseInitialEventWhenGoingOffline { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); + FSTQueryListener *listener = + [self listenToQuery:query + options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:YES] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event + [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; // no event + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap2]; // another event + + FSTDocumentViewChange *change1 = + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; + FSTDocumentViewChange *change2 = + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]; + FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[ change1 ] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query + documents:snap2.documents + oldDocuments:snap1.documents + documentChanges:@[ change2 ] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:NO]; + XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ])); +} + +- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQueryListener *listener = [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event + + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { + NSMutableArray *events = [NSMutableArray array]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQueryListener *listener = [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:events]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + + [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // event + + FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] + initWithQuery:query + documents:snap1.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] + documentChanges:@[] + fromCache:YES + hasPendingWrites:NO + syncStateChanged:YES]; + XCTAssertEqualObjects(events, (@[ expectedSnap ])); +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler { + return [[FSTQueryListener alloc] initWithQuery:query + options:[FSTListenOptions defaultOptions] + viewSnapshotHandler:handler]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + accumulatingSnapshots:(NSMutableArray *)values { + return [[FSTQueryListener alloc] initWithQuery:query + options:options + viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [values addObject:snapshot]; + }]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + accumulatingSnapshots:(NSMutableArray *)values { + return [self listenToQuery:query + options:[FSTListenOptions defaultOptions] + accumulatingSnapshots:values]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.m b/Firestore/Example/Tests/Core/FSTQueryTests.m new file mode 100644 index 0000000..051f10d --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTQueryTests.m @@ -0,0 +1,577 @@ +/* + * 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 "Core/FSTQuery.h" + +#import + +#import "API/FIRFirestore+Internal.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience methods for building test queries. */ +@interface FSTQuery (Tests) +- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending; +@end + +@implementation FSTQuery (Tests) + +- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending { + return [self queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(key) + ascending:ascending]]; +} + +@end + +@interface FSTQueryTests : XCTestCase +@end + +@implementation FSTQueryTests + +- (void)testConstructor { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages", @"0001" ]]; + FSTQuery *query = [FSTQuery queryWithPath:path]; + XCTAssertNotNil(query); + + XCTAssertEqual(query.sortOrders.count, 1); + XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, kDocumentKeyPath); + XCTAssertEqual(query.sortOrders[0].ascending, YES); + + XCTAssertEqual(query.explicitSortOrders.count, 0); +} + +- (void)testOrderBy { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages" ]]; + FSTQuery *query = [FSTQuery queryWithPath:path]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length") + ascending:NO]]; + + XCTAssertEqual(query.sortOrders.count, 2); + XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, @"length"); + XCTAssertEqual(query.sortOrders[0].ascending, NO); + XCTAssertEqualObjects(query.sortOrders[1].field.canonicalString, kDocumentKeyPath); + XCTAssertEqual(query.sortOrders[1].ascending, NO); + + XCTAssertEqual(query.explicitSortOrders.count, 1); + XCTAssertEqualObjects(query.explicitSortOrders[0].field.canonicalString, @"length"); + XCTAssertEqual(query.explicitSortOrders[0].ascending, NO); +} + +- (void)testMatchesBasedOnDocumentKey { + FSTResourcePath *queryKey = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages", @"1" ]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + // document query + FSTQuery *query = [FSTQuery queryWithPath:queryKey]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); +} + +- (void)testMatchesCorrectlyForShallowAncestorQuery { + FSTResourcePath *queryPath = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + // shallow ancestor query + FSTQuery *query = [FSTQuery queryWithPath:queryPath]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc1Meta]); + XCTAssertTrue([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); +} + +- (void)testEmptyFieldsAreAllowedForQueries { + FSTResourcePath *queryPath = [FSTResourcePath pathWithString:@"rooms/eros/messages"]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTQuery *query = [[FSTQuery queryWithPath:queryPath] + queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")]; + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); +} + +- (void)testMatchesPrimitiveValuesForFilters { + FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); + + XCTAssertFalse([query1 matchesDocument:doc1]); + XCTAssertTrue([query1 matchesDocument:doc2]); + XCTAssertTrue([query1 matchesDocument:doc3]); + XCTAssertFalse([query1 matchesDocument:doc4]); + XCTAssertFalse([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); + + XCTAssertTrue([query2 matchesDocument:doc1]); + XCTAssertTrue([query2 matchesDocument:doc2]); + XCTAssertFalse([query2 matchesDocument:doc3]); + XCTAssertFalse([query2 matchesDocument:doc4]); + XCTAssertFalse([query2 matchesDocument:doc5]); + XCTAssertFalse([query2 matchesDocument:doc6]); +} + +- (void)testNullFilter { + FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])]; + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); + XCTAssertFalse([query matchesDocument:doc4]); + XCTAssertFalse([query matchesDocument:doc5]); +} + +- (void)testNanFilter { + FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))]; + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO); + + XCTAssertTrue([query matchesDocument:doc1]); + XCTAssertFalse([query matchesDocument:doc2]); + XCTAssertFalse([query matchesDocument:doc3]); + XCTAssertFalse([query matchesDocument:doc4]); + XCTAssertFalse([query matchesDocument:doc5]); +} + +- (void)testDoesNotMatchComplexObjectsForFilters { + FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{ @"sort" : @{} }, NO); // no sort field + FSTDocument *doc7 = FSTTestDoc(@"collection/7", 0, @{ @"sort" : @[ @3, @1 ] }, NO); + + XCTAssertTrue([query1 matchesDocument:doc1]); + XCTAssertFalse([query1 matchesDocument:doc2]); + XCTAssertFalse([query1 matchesDocument:doc3]); + XCTAssertFalse([query1 matchesDocument:doc4]); + XCTAssertFalse([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); + XCTAssertFalse([query1 matchesDocument:doc7]); + + XCTAssertTrue([query2 matchesDocument:doc1]); + XCTAssertFalse([query2 matchesDocument:doc2]); + XCTAssertFalse([query2 matchesDocument:doc3]); + XCTAssertFalse([query2 matchesDocument:doc4]); + XCTAssertFalse([query2 matchesDocument:doc5]); + XCTAssertFalse([query2 matchesDocument:doc6]); + XCTAssertFalse([query2 matchesDocument:doc7]); +} + +- (void)testDoesntRemoveComplexObjectsWithOrderBy { + FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") + ascending:YES]]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); + FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO); + FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO); + FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO); + FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO); + + XCTAssertTrue([query1 matchesDocument:doc1]); + XCTAssertTrue([query1 matchesDocument:doc2]); + XCTAssertTrue([query1 matchesDocument:doc3]); + XCTAssertTrue([query1 matchesDocument:doc4]); + XCTAssertTrue([query1 matchesDocument:doc5]); + XCTAssertFalse([query1 matchesDocument:doc6]); +} + +- (void)testFiltersBasedOnArrayValue { + FSTQuery *baseQuery = + [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + + FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO); + + NSArray> *matchingFilters = + @[ FSTTestFilter(@"tags", @"==", @[ @"foo", @1, @YES ]) ]; + + NSArray> *nonMatchingFilters = @[ + FSTTestFilter(@"tags", @"==", @"foo"), + FSTTestFilter(@"tags", @"==", @[ @"foo", @1 ]), + FSTTestFilter(@"tags", @"==", @[ @"foo", @YES, @1 ]), + ]; + + for (id filter in matchingFilters) { + XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } + + for (id filter in nonMatchingFilters) { + XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } +} + +- (void)testFiltersBasedOnObjectValue { + FSTQuery *baseQuery = + [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + + FSTDocument *doc1 = + FSTTestDoc(@"collection/doc", 0, + @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO); + + NSArray> *matchingFilters = @[ + FSTTestFilter(@"tags", @"==", + @{ @"foo" : @"foo", + @"a" : @0, + @"b" : @YES, + @"c" : @(NAN) }), + FSTTestFilter(@"tags", @"==", + @{ @"b" : @YES, + @"a" : @0, + @"foo" : @"foo", + @"c" : @(NAN) }), + FSTTestFilter(@"tags.foo", @"==", @"foo") + ]; + + NSArray> *nonMatchingFilters = @[ + FSTTestFilter(@"tags", @"==", @"foo"), FSTTestFilter(@"tags", @"==", @{ + @"foo" : @"foo", + @"a" : @0, + @"b" : @YES, + }) + ]; + + for (id filter in matchingFilters) { + XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } + + for (id filter in nonMatchingFilters) { + XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]); + } +} + +/** + * Checks that an ordered array of elements yields the correct pair-wise comparison result for the + * supplied comparator. + */ +- (void)assertCorrectComparisonsWithArray:(NSArray *)array comparator:(NSComparator)comp { + [array enumerateObjectsUsingBlock:^(id iObj, NSUInteger i, BOOL *outerStop) { + [array enumerateObjectsUsingBlock:^(id _Nonnull jObj, NSUInteger j, BOOL *innerStop) { + NSComparisonResult expected = [@(i) compare:@(j)]; + NSComparisonResult actual = comp(iObj, jObj); + XCTAssertEqual(actual, expected, @"Compared %@ to %@ at (%lu, %lu).", iObj, jObj, + (unsigned long)i, (unsigned long)j); + }]; + }]; +} + +- (void)testSortsDocumentsInTheCorrectOrder { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") + ascending:YES]]; + + // clang-format off + NSArray *docs = @[ + FSTTestDoc(@"collection/1", 0, @{@"sort": [NSNull null]}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @NO}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @YES}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @1}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort": @1}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort": @1}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort": @1.9}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @2}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @2.1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @""}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"a"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"ab"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": @"b"}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort": + FSTTestRef(@"project", kDefaultDatabaseID, @"collection/id1")}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testSortsDocumentsUsingMultipleFields { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") + ascending:YES]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") + ascending:YES]]; + + // clang-format off + NSArray *docs = + @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testSortsDocumentsWithDescendingToo { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") + ascending:NO]]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2") + ascending:NO]]; + + // clang-format off + NSArray *docs = + @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO), + FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO), + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO), + FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), + FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key + FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO), + ]; + // clang-format on + + [self assertCorrectComparisonsWithArray:docs comparator:query.comparator]; +} + +- (void)testEquality { + FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + + FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + + FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + + FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; + q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; + q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; + q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; + + FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; + q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; + FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; + q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; + + FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q61 = [q61 queryBySettingLimit:10]; + + // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet + XCTAssertNotEqualObjects(q11, q21); + XCTAssertNotEqualObjects(q11, q31); + XCTAssertNotEqualObjects(q11, q41); + XCTAssertNotEqualObjects(q11, q51); + XCTAssertNotEqualObjects(q11, q61); + + XCTAssertEqualObjects(q21, q22); + XCTAssertNotEqualObjects(q21, q31); + XCTAssertNotEqualObjects(q21, q41); + XCTAssertNotEqualObjects(q21, q51); + XCTAssertNotEqualObjects(q21, q61); + + XCTAssertEqualObjects(q31, q32); + XCTAssertNotEqualObjects(q31, q41); + XCTAssertNotEqualObjects(q31, q51); + XCTAssertNotEqualObjects(q31, q61); + + XCTAssertEqualObjects(q41, q42); + XCTAssertNotEqualObjects(q41, q43Diff); + XCTAssertNotEqualObjects(q41, q51); + XCTAssertNotEqualObjects(q41, q61); + + XCTAssertEqualObjects(q51, q52); + XCTAssertNotEqualObjects(q51, q53Diff); + XCTAssertNotEqualObjects(q51, q61); +} + +- (void)testUniqueIds { + FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; + q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; + + FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + + FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + + FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; + q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; + q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; + FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; + q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; + + FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; + q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; + q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; + FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; + q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; + + FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + q61 = [q61 queryBySettingLimit:10]; + + // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet + XCTAssertNotEqual(q11.hash, q21.hash); + XCTAssertNotEqual(q11.hash, q31.hash); + XCTAssertNotEqual(q11.hash, q41.hash); + XCTAssertNotEqual(q11.hash, q51.hash); + XCTAssertNotEqual(q11.hash, q61.hash); + + XCTAssertEqual(q21.hash, q22.hash); + XCTAssertNotEqual(q21.hash, q31.hash); + XCTAssertNotEqual(q21.hash, q41.hash); + XCTAssertNotEqual(q21.hash, q51.hash); + XCTAssertNotEqual(q21.hash, q61.hash); + + XCTAssertEqual(q31.hash, q32.hash); + XCTAssertNotEqual(q31.hash, q41.hash); + XCTAssertNotEqual(q31.hash, q51.hash); + XCTAssertNotEqual(q31.hash, q61.hash); + + XCTAssertEqual(q41.hash, q42.hash); + XCTAssertNotEqual(q41.hash, q43Diff.hash); + XCTAssertNotEqual(q41.hash, q51.hash); + XCTAssertNotEqual(q41.hash, q61.hash); + + XCTAssertEqual(q51.hash, q52.hash); + XCTAssertNotEqual(q51.hash, q53Diff.hash); + XCTAssertNotEqual(q51.hash, q61.hash); +} + +- (void)testImplicitOrderBy { + FSTQuery *baseQuery = FSTTestQuery(@"foo"); + // Default is ascending + XCTAssertEqualObjects(baseQuery.sortOrders, @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); + + // Explicit key ordering is respected + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")].sortOrders, + @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]); + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")].sortOrders, + @[ FSTTestOrderBy(kDocumentKeyPath, @"desc") ]); + + XCTAssertEqualObjects( + [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")] + .sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); + + XCTAssertEqualObjects( + [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")] + .sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); + + // Inequality filters add order bys + XCTAssertEqualObjects( + [baseQuery queryByAddingFilter:FSTTestFilter(@"foo", @"<", @5)].sortOrders, + (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ])); + + // Descending order by applies to implicit key ordering + XCTAssertEqualObjects( + [baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")].sortOrders, + (@[ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ])); + XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")] + queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"desc")] + .sortOrders, + (@[ + FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(@"bar", @"desc"), + FSTTestOrderBy(kDocumentKeyPath, @"desc") + ])); + XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")] + queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"asc")] + .sortOrders, + (@[ + FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(@"bar", @"asc"), + FSTTestOrderBy(kDocumentKeyPath, @"asc") + ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h new file mode 100644 index 0000000..ff98e74 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.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 + +#import "Core/FSTSyncEngine.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSyncEngine (Testing) + +/** Returns the current set of limbo document keys and their associated target IDs. */ +- (NSDictionary *)currentLimboDocuments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m new file mode 100644 index 0000000..11a7f46 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m @@ -0,0 +1,94 @@ +/* + * 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 "Core/FSTTargetIDGenerator.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTargetIDGenerator () +- (instancetype)initWithGeneratorID:(NSInteger)generatorID startingAfterID:(FSTTargetID)after; +@end + +@interface FSTTargetIDGeneratorTests : XCTestCase +@end + +@implementation FSTTargetIDGeneratorTests + +- (void)testConstructor { + XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0] nextID], + 2); + XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0] nextID], + 1); + + XCTAssertEqual([[FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:0] nextID], 2); + XCTAssertEqual([[FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0] nextID], 1); +} + +- (void)testSkipPast { + FSTTargetIDGenerator *gen = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:-1]; + XCTAssertEqual([gen nextID], 1); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:2]; + XCTAssertEqual([gen nextID], 3); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:4]; + XCTAssertEqual([gen nextID], 5); + + for (int i = 4; i < 12; ++i) { + FSTTargetIDGenerator *gen0 = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:i]; + FSTTargetIDGenerator *gen1 = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:i]; + XCTAssertEqual([gen0 nextID], i + 2 & ~1, @"Skip failed for index %d", i); + XCTAssertEqual([gen1 nextID], i + 1 | 1, @"Skip failed for index %d", i); + } + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:12]; + XCTAssertEqual([gen nextID], 13); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:22]; + XCTAssertEqual([gen nextID], 24); +} + +- (void)testIncrement { + FSTTargetIDGenerator *gen = + [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0]; + XCTAssertEqual([gen nextID], 2); + XCTAssertEqual([gen nextID], 4); + XCTAssertEqual([gen nextID], 6); + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:46]; + XCTAssertEqual([gen nextID], 48); + XCTAssertEqual([gen nextID], 50); + XCTAssertEqual([gen nextID], 52); + XCTAssertEqual([gen nextID], 54); + + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0]; + XCTAssertEqual([gen nextID], 1); + XCTAssertEqual([gen nextID], 3); + XCTAssertEqual([gen nextID], 5); + gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:46]; + XCTAssertEqual([gen nextID], 47); + XCTAssertEqual([gen nextID], 49); + XCTAssertEqual([gen nextID], 51); + XCTAssertEqual([gen nextID], 53); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTTimestampTests.m b/Firestore/Example/Tests/Core/FSTTimestampTests.m new file mode 100644 index 0000000..8538078 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTTimestampTests.m @@ -0,0 +1,88 @@ +/* + * 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 "Core/FSTTimestamp.h" + +#import + +#import "Util/FSTAssert.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTimestampTests : XCTestCase +@end + +@implementation FSTTimestampTests + +- (void)testFromDate { + // Very carefully construct an NSDate that won't lose precision with its milliseconds. + NSDate *input = [NSDate dateWithTimeIntervalSinceReferenceDate:1.5]; + + FSTTimestamp *actual = [FSTTimestamp timestampWithDate:input]; + static const int64_t kSecondsFromEpochToReferenceDate = 978307200; + XCTAssertEqual(kSecondsFromEpochToReferenceDate + 1, actual.seconds); + XCTAssertEqual(500000000, actual.nanos); + + FSTTimestamp *expected = + [[FSTTimestamp alloc] initWithSeconds:(kSecondsFromEpochToReferenceDate + 1) nanos:500000000]; + XCTAssertEqualObjects(expected, actual); +} + +- (void)testSO8601String { + NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); + FSTTimestamp *timestamp = + [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:543000000]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.543000000Z"); +} + +- (void)testISO8601String_withLowMilliseconds { + NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0); + FSTTimestamp *timestamp = + [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:7000000]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.007000000Z"); +} + +- (void)testISO8601String_withLowNanos { + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:1]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1970-01-01T00:00:00.000000001Z"); +} + +- (void)testISO8601String_withNegativeSeconds { + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:-1 nanos:999999999]; + XCTAssertEqualObjects(timestamp.ISO8601String, @"1969-12-31T23:59:59.999999999Z"); +} + +- (void)testCompare { + NSArray *timestamps = @[ + [[FSTTimestamp alloc] initWithSeconds:12344 nanos:999999999], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:0], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:000000001], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:99999999], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000000], + [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000001], + [[FSTTimestamp alloc] initWithSeconds:12346 nanos:0], + ]; + for (int i = 0; i < timestamps.count - 1; ++i) { + XCTAssertEqual(NSOrderedAscending, [timestamps[i] compare:timestamps[i + 1]]); + XCTAssertEqual(NSOrderedDescending, [timestamps[i + 1] compare:timestamps[i]]); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m new file mode 100644 index 0000000..202fae8 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m @@ -0,0 +1,141 @@ +/* + * 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 "Core/FSTViewSnapshot.h" + +#import + +#import "Core/FSTQuery.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewSnapshotTests : XCTestCase +@end + +@implementation FSTViewSnapshotTests + +- (void)testDocumentChangeConstructor { + FSTDocument *doc = FSTTestDoc(@"a/b", 0, @{}, NO); + FSTDocumentViewChangeType type = FSTDocumentViewChangeTypeModified; + FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type]; + XCTAssertEqual(change.document, doc); + XCTAssertEqual(change.type, type); +} + +- (void)testTrack { + FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet]; + + FSTDocument *docAdded = FSTTestDoc(@"a/1", 0, @{}, NO); + FSTDocument *docRemoved = FSTTestDoc(@"a/2", 0, @{}, NO); + FSTDocument *docModified = FSTTestDoc(@"a/3", 0, @{}, NO); + + FSTDocument *docAddedThenModified = FSTTestDoc(@"b/1", 0, @{}, NO); + FSTDocument *docAddedThenRemoved = FSTTestDoc(@"b/2", 0, @{}, NO); + FSTDocument *docRemovedThenAdded = FSTTestDoc(@"b/3", 0, @{}, NO); + FSTDocument *docModifiedThenRemoved = FSTTestDoc(@"b/4", 0, @{}, NO); + FSTDocument *docModifiedThenModified = FSTTestDoc(@"b/5", 0, @{}, NO); + + [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModified + type:FSTDocumentViewChangeTypeModified]]; + + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded + type:FSTDocumentViewChangeTypeAdded]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved + type:FSTDocumentViewChangeTypeRemoved]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified + type:FSTDocumentViewChangeTypeModified]]; + [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified + type:FSTDocumentViewChangeTypeModified]]; + + NSArray *changes = [set changes]; + XCTAssertEqual(changes.count, 7); + + XCTAssertEqual(changes[0].document, docAdded); + XCTAssertEqual(changes[0].type, FSTDocumentViewChangeTypeAdded); + + XCTAssertEqual(changes[1].document, docRemoved); + XCTAssertEqual(changes[1].type, FSTDocumentViewChangeTypeRemoved); + + XCTAssertEqual(changes[2].document, docModified); + XCTAssertEqual(changes[2].type, FSTDocumentViewChangeTypeModified); + + XCTAssertEqual(changes[3].document, docAddedThenModified); + XCTAssertEqual(changes[3].type, FSTDocumentViewChangeTypeAdded); + + XCTAssertEqual(changes[4].document, docRemovedThenAdded); + XCTAssertEqual(changes[4].type, FSTDocumentViewChangeTypeModified); + + XCTAssertEqual(changes[5].document, docModifiedThenRemoved); + XCTAssertEqual(changes[5].type, FSTDocumentViewChangeTypeRemoved); + + XCTAssertEqual(changes[6].document, docModifiedThenModified); + XCTAssertEqual(changes[6].type, FSTDocumentViewChangeTypeModified); +} + +- (void)testViewSnapshotConstructor { + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"a" ]]]; + FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey]; + FSTDocumentSet *oldDocuments = documents; + documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)]; + NSArray *documentChanges = + @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc(@"c/a", 1, @{}, NO) + type:FSTDocumentViewChangeTypeAdded] ]; + + BOOL fromCache = YES; + BOOL hasPendingWrites = NO; + BOOL syncStateChanged = YES; + + FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query + documents:documents + oldDocuments:oldDocuments + documentChanges:documentChanges + fromCache:fromCache + hasPendingWrites:hasPendingWrites + syncStateChanged:syncStateChanged]; + + XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.documents, documents); + XCTAssertEqual(snapshot.oldDocuments, oldDocuments); + XCTAssertEqual(snapshot.documentChanges, documentChanges); + XCTAssertEqual(snapshot.fromCache, fromCache); + XCTAssertEqual(snapshot.hasPendingWrites, hasPendingWrites); + XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewTests.m b/Firestore/Example/Tests/Core/FSTViewTests.m new file mode 100644 index 0000000..d939d62 --- /dev/null +++ b/Firestore/Example/Tests/Core/FSTViewTests.m @@ -0,0 +1,618 @@ +/* + * 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 "Core/FSTView.h" + +#import + +#import "API/FIRFirestore+Internal.h" +#import "Core/FSTQuery.h" +#import "Core/FSTViewSnapshot.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" +#import "Remote/FSTRemoteEvent.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTViewTests : XCTestCase +@end + +@implementation FSTViewTests + +/** Returns a new empty query to use for testing. */ +- (FSTQuery *)queryForMessages { + return [FSTQuery + queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]]; +} + +- (void)testAddsDocumentsBasedOnQuery { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); + + FSTViewSnapshot *_Nullable snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertFalse(snapshot.hasPendingWrites); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testRemovesDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // delete doc2, add doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testReturnsNilIfThereAreNoChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // reapply same docs, no changes + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + XCTAssertNil(snapshot); +} + +- (void)testDoesNotReturnNilForFirstChanges { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); + XCTAssertNotNil(snapshot); +} + +- (void)testFiltersDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testUpdatesDocumentsBasedOnQueryWithFilter { + FSTQuery *query = [self queryForMessages]; + FSTRelationFilter *filter = + [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort") + filterOperator:FSTRelationFilterOperatorLessThanOrEqual + value:[FSTDoubleValue doubleValue:2]]; + query = [query queryByAddingFilter:filter]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); + + FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO); + FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO); + FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO); + + snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded], + [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertTrue(snapshot.isFromCache); + XCTAssertFalse(snapshot.syncStateChanged); +} + +- (void)testRemovesDocumentsForQueryWithLimit { + FSTQuery *query = [self queryForMessages]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); + + // add doc2, which should push out doc3 + FSTViewSnapshot *snapshot = + FSTTestApplyChanges(view, @[ doc2 ], + [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO); + + // initial state + FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + + // change doc2 to 5, and add doc3 and doc4. + // doc2 will be modified + removed = removed + // doc3 will be added + // doc4 will be added + removed = nothing + doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO); + FSTViewDocumentChanges *viewDocChanges = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])]; + XCTAssertTrue(viewDocChanges.needsRefill); + // Verify that all the docs still match. + viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ]) + previousChanges:viewDocChanges]; + FSTViewSnapshot *snapshot = + [view applyChangesToDocuments:viewDocChanges + targetChange:[FSTTargetChange + changeWithDocuments:@[ doc1, doc2, doc3, doc4 ] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]] + .snapshot; + + XCTAssertEqual(snapshot.query, query); + + XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + + XCTAssertEqualObjects( + snapshot.documentChanges, (@[ + [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved], + [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded] + ])); + + XCTAssertFalse(snapshot.isFromCache); + XCTAssertTrue(snapshot.syncStateChanged); +} + +- (void)testKeepsTrackOfLimboDocuments { + FSTQuery *query = [self queryForMessages]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + + FSTViewChange *change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = + [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange + changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] + targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ] + currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + XCTAssertEqualObjects(change.limboChanges, @[]); + + change = [view + applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]]; + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]); + + change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ + FSTTestDeletedDoc(@"rooms/eros/messages/2", + 1) + ])]]; // remove + XCTAssertEqualObjects( + change.limboChanges, + @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]); +} + +- (void)testResumingQueryCreatesNoLimbos { + FSTQuery *query = [self queryForMessages]; + + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + + // Unlike other cases, here the view is initialized with a set of previously synced documents + // which happens when listening to a previously listened-to query. + FSTView *view = [[FSTView alloc] initWithQuery:query + remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])]; + + FSTTargetChange *markCurrent = + [FSTTargetChange changeWithDocuments:@[] + currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])]; + FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent]; + XCTAssertEqualObjects(change.limboChanges, @[]); +} + +- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray *)docs { + XCTAssertEqual(docs.count, docSet.count); + for (FSTDocument *doc in docs) { + XCTAssertTrue([docSet containsKey:doc.key]); + } +} + +- (void)testReturnsNeedsRefillOnDeleteInLimitQuery { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/0", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with just the one doc remaining. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testReturnsNeedsRefillOnReorderInLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertTrue(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + // Refill it with all three current docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ]) + previousChanges:changes]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderWithinLimit { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillOnReorderAfterLimitQuery { + FSTQuery *query = [self queryForMessages]; + query = + [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order") + ascending:YES]]; + query = [query queryBySettingLimit:3]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); + FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); + FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(3, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Move one of the docs. + doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForAdditionAfterTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Add a doc that is past the limit. + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove one of the docs. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/1", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(1, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testHandlesApplyingIrrelevantDocs { + FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(2, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; + + // Remove a doc that isn't even in the results. + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( + @"rooms/eros/messages/2", 0) ])]; + [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XCTAssertFalse(changes.needsRefill); + XCTAssertEqual(0, [changes.changeSet changes].count); + [view applyChangesToDocuments:changes]; +} + +- (void)testComputesMutatedKeys { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); +} + +- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); +} + +- (void)testRemembersLocalMutationsFromPreviousSnapshot { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; + [view applyChangesToDocuments:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { + FSTQuery *query = [self queryForMessages]; + FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO); + FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES); + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + + // Start with a full view. + FSTViewDocumentChanges *changes = + [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + + FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); + changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; + XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m new file mode 100644 index 0000000..2f8babd --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m @@ -0,0 +1,195 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "FSTIntegrationTestCase.h" + +@interface FIRCursorTests : FSTIntegrationTestCase +@end + +@implementation FIRCursorTests + +- (void)testCanPageThroughItems { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a"}, + @"b" : @{@"v" : @"b"}, + @"c" : @{@"v" : @"c"}, + @"d" : @{@"v" : @"d"}, + @"e" : @{@"v" : @"e"}, + @"f" : @{@"v" : @"f"} + }]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ])); + + FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), + (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ])); + + lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]); + + lastDoc = snapshot.documents.lastObject; + snapshot = [self + readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]); +} + +- (void)testCanBeCreatedFromDocuments { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @2.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]]; + + XCTAssertTrue(snapshot.exists); + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"d", + @"sort" : @2.0 } + ])); + + querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"e", + @"sort" : @0.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 }, + @{ @"v" : @"b", + @"sort" : @2.0 } + ])); +} + +- (void)testCanBeCreatedFromValues { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @2.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"b", + @"sort" : @2.0 }, + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"d", + @"sort" : @2.0 } + ])); + + querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[ + @{ @"v" : @"e", + @"sort" : @0.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 } + ])); +} + +- (void)testCanBeCreatedUsingDocumentId { + NSDictionary *testDocs = @{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"}, + @"d" : @{@"k" : @"d"}, + @"e" : @{@"k" : @"e"} + }; + FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"] + documentWithAutoID] collectionWithPath:@"sub-collection"]; + [self writeAllDocuments:testDocs toCollection:writer]; + + FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]] + queryStartingAtValues:@[ @"b" ]] + queryEndingBeforeValues:@[ @"d" ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), + (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ])); +} + +- (void)testCanBeUsedWithReferenceValues { + FIRFirestore *db = [self firestore]; + + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]}, + @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]}, + @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]}, + @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]}, + @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]}, + }]; + FIRQuery *query = [testCollection queryOrderedByField:@"ref"]; + FIRQuerySnapshot *querySnapshot = [self + readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]] + queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]]; + NSMutableArray *actual = [NSMutableArray array]; + [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc, + NSUInteger idx, BOOL *_Nonnull stop) { + [actual addObject:doc.data[@"k"]]; + }]; + XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ])); +} + +- (void)testCanBeUsedInDescendingQueries { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"a" : @{@"v" : @"a", @"sort" : @1.0}, + @"b" : @{@"v" : @"b", @"sort" : @2.0}, + @"c" : @{@"v" : @"c", @"sort" : @2.0}, + @"d" : @{@"v" : @"d", @"sort" : @3.0}, + @"e" : @{@"v" : @"e", @"sort" : @0.0}, + @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up + }]; + FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES] + queryOrderedByFieldPath:[FIRFieldPath documentID] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ + @{ @"v" : @"c", + @"sort" : @2.0 }, + @{ @"v" : @"b", + @"sort" : @2.0 }, + @{ @"v" : @"a", + @"sort" : @1.0 }, + @{ @"v" : @"e", + @"sort" : @0.0 } + ])); + + snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ])); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m new file mode 100644 index 0000000..d5558cc --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m @@ -0,0 +1,741 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "FSTIntegrationTestCase.h" + +@interface FIRDatabaseTests : FSTIntegrationTestCase +@end + +@implementation FIRDatabaseTests + +- (void)testCanUpdateAnExistingDocument { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary *updateData = + @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"}; + NSDictionary *finalData = + @{ @"desc" : @"NewDescription", + @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"]; + [doc updateData:updateData + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [updateCompletion fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertEqualObjects(result.data, finalData); +} + +- (void)testCanDeleteAFieldWithAnUpdate { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary *updateData = + @{@"owner.email" : [FIRFieldValue fieldValueForDelete]}; + NSDictionary *finalData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny"} }; + + [self writeDocumentRef:doc data:initialData]; + [self updateDocumentRef:doc data:updateData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertTrue(result.exists); + XCTAssertEqualObjects(result.data, finalData); +} + +- (void)testDeleteDocument { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + NSDictionary *data = @{@"value" : @"foo"}; + [self writeDocumentRef:doc data:data]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, data); + [self deleteDocumentRef:doc]; + result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testCannotUpdateNonexistentDocument { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"]; + [doc updateData:@{@"owner" : @"abc"} + completion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [setCompletion fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testCanOverwriteDataAnExistingDocumentUsingSet { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + NSDictionary *udpateData = @{@"desc" : @"NewDescription"}; + + [self writeDocumentRef:doc data:initialData]; + [self writeDocumentRef:doc data:udpateData]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, udpateData); +} + +- (void)testCanMergeDataWithAnExistingDocumentUsingSet { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ + @"desc" : @"Description", + @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} + }; + NSDictionary *updateData = + @{ @"updated" : @YES, + @"owner.data" : @{@"name" : @"Sebastian"} }; + NSDictionary *finalData = @{ + @"desc" : @"Description", + @"updated" : @YES, + @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:updateData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testMergeReplacesArrays { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ + @"untouched" : @YES, + @"data" : @"old", + @"topLevel" : @[ @"old", @"old" ], + @"mapInArray" : @[ @{@"data" : @"old"} ] + }; + NSDictionary *updateData = + @{ @"data" : @"new", + @"topLevel" : @[ @"new" ], + @"mapInArray" : @[ @{@"data" : @"new"} ] }; + NSDictionary *finalData = @{ + @"untouched" : @YES, + @"data" : @"new", + @"topLevel" : @[ @"new" ], + @"mapInArray" : @[ @{@"data" : @"new"} ] + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:updateData + options:[FIRSetOptions merge] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testAddingToACollectionYieldsTheCorrectDocumentReference { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }]; + + XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"]; + [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 })); + + [getCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testListenCanBeCalledMultipleTimes { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *doc = [coll documentWithAutoID]; + + XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"]; + + __block NSDictionary *resultingData; + + // Shut the compiler up about strong references to doc. + FIRDocumentReference *__weak weakDoc = doc; + + [doc setData:@{@"foo" : @"bar"} + completion:^(NSError *error1) { + XCTAssertNil(error1); + FIRDocumentReference *strongDoc = weakDoc; + + [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) { + XCTAssertNil(error2); + + FIRDocumentReference *strongDoc2 = weakDoc; + [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) { + XCTAssertNil(error3); + resultingData = snapshot3.data; + [completed fulfill]; + }]; + }]; + }]; + + [self awaitExpectations]; + XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"}); +} + +- (void)testDocumentSnapshotEvents_nonExistent { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"]; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [snapshotCompletion fulfill]; + + } else if (callbacks == 2) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forAdd { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *dataCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + [dataCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + dataCompletion = [self expectationWithDescription:@"data snapshot"]; + + [docRef setData:@{ @"a" : @1 }]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forAddIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *dataCompletion; + __block int callbacks = 0; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + id listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertNotNil(doc); + XCTAssertFalse(doc.exists); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + + } else if (callbacks == 3) { + XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 })); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + [dataCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + dataCompletion = [self expectationWithDescription:@"data snapshot"]; + + [docRef setData:@{ @"a" : @1 }]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forChange { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + NSDictionary *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + NSDictionary *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + id listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 3) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, YES); + XCTAssertEqual(doc.metadata.isFromCache, NO); + + } else if (callbacks == 4) { + XCTAssertEqualObjects(doc.data, changedData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [changeCompletion fulfill]; + + } else if (callbacks == 5) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forDelete { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertFalse(doc.exists); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata { + FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + FIRDocumentListenOptions *options = + [[FIRDocumentListenOptions options] includeMetadataChanges:YES]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [docRef addSnapshotListenerWithOptions:options + listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, YES); + + } else if (callbacks == 2) { + XCTAssertEqualObjects(doc.data, initialData); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 3) { + XCTAssertFalse(doc.exists); + XCTAssertEqual(doc.metadata.hasPendingWrites, NO); + XCTAssertEqual(doc.metadata.isFromCache, NO); + [changeCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received this callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forAdd { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary *newData = @{ @"a" : @1 }; + + XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 0); + [emptyCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, newData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"changed snapshot"]; + + [docRef setData:newData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forChange { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + NSDictionary *changedData = @{ @"b" : @2 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, initialData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, changedData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES); + [changeCompletion fulfill]; + + } else if (callbacks == 3) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef setData:changedData]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testQuerySnapshotEvents_forDelete { + FIRCollectionReference *roomsRef = [self collectionRef]; + FIRDocumentReference *docRef = [roomsRef documentWithAutoID]; + + NSDictionary *initialData = @{ @"a" : @1 }; + + [self writeDocumentRef:docRef data:initialData]; + + XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"]; + __block XCTestExpectation *changeCompletion; + __block int callbacks = 0; + + id listenerRegistration = + [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) { + callbacks++; + + if (callbacks == 1) { + XCTAssertEqual(docSet.count, 1); + XCTAssertEqualObjects(docSet.documents[0].data, initialData); + XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO); + [initialCompletion fulfill]; + + } else if (callbacks == 2) { + XCTAssertEqual(docSet.count, 0); + [changeCompletion fulfill]; + + } else if (callbacks == 4) { + XCTFail("Should not have received a third callback"); + } + }]; + + [self awaitExpectations]; + changeCompletion = [self expectationWithDescription:@"listen for changed data"]; + + [docRef deleteDocument]; + [self awaitExpectations]; + + [listenerRegistration remove]; +} + +- (void)testExposesFirestoreOnDocumentReferences { + FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"]; + XCTAssertEqual(doc.firestore, self.db); +} + +- (void)testExposesFirestoreOnQueries { + FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5]; + XCTAssertEqual(q.firestore, self.db); +} + +- (void)testCanTraverseCollectionsAndDocuments { + NSString *expected = @"a/b/c/d"; + // doc path from root Firestore. + XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected); + // collection path from root Firestore. + XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path, + expected); + // doc path from CollectionReference. + XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path, + expected); + // collection path from DocumentReference. + XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path, + @"a/b/c/d/e"); +} + +- (void)testCanTraverseCollectionAndDocumentParents { + FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"]; + XCTAssertEqualObjects(collection.path, @"a/b/c"); + + FIRDocumentReference *doc = collection.parent; + XCTAssertEqualObjects(doc.path, @"a/b"); + + collection = doc.parent; + XCTAssertEqualObjects(collection.path, @"a"); + + FIRDocumentReference *nilDoc = collection.parent; + XCTAssertNil(nilDoc); +} + +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}]; + + [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + [self writeDocumentRef:doc + data:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + }]; + + [self updateDocumentRef:doc + data:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testCollectionID { + XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo"); + XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz"); +} + +- (void)testDocumentID { + XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar"); + XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux"); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m new file mode 100644 index 0000000..d851556 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m @@ -0,0 +1,223 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "Core/FSTFirestoreClient.h" + +#import "FSTIntegrationTestCase.h" + +@interface FIRFieldsTests : FSTIntegrationTestCase +@end + +@implementation FIRFieldsTests + +- (NSDictionary *)testNestedDataNumbered:(int)number { + return @{ + @"name" : [NSString stringWithFormat:@"room %d", number], + @"metadata" : @{ + @"createdAt" : @(number), + @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]} + } + }; +} + +- (void)testNestedFieldsCanBeWrittenWithSet { + NSDictionary *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, testData); +} + +- (void)testNestedFieldsCanBeReadDirectly { + NSDictionary *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result[@"name"], testData[@"name"]); + XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]); + XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]); + XCTAssertNil(result[@"metadata.nofield"]); + XCTAssertNil(result[@"nometadata.nofield"]); +} + +- (void)testNestedFieldsCanBeUpdated { + NSDictionary *testData = [self testNestedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects( + result.data, (@{ + @"name" : @"room 1", + @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200} + })); +} + +- (void)testNestedFieldsCanBeUsedInQueryFilters { + NSDictionary *> *testDocs = @{ + @"1" : [self testNestedDataNumbered:300], + @"2" : [self testNestedDataNumbered:100], + @"3" : [self testNestedDataNumbered:200] + }; + + // inequality adds implicit sort on field + NSArray *> *expected = + @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200]; + FIRQuerySnapshot *results = [self readDocumentSetForRef:q]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected)); +} + +- (void)testNestedFieldsCanBeUsedInOrderBy { + NSDictionary *> *testDocs = @{ + @"1" : [self testNestedDataNumbered:300], + @"2" : [self testNestedDataNumbered:100], + @"3" : [self testNestedDataNumbered:200] + }; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; + FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ + [self testNestedDataNumbered:100], [self testNestedDataNumbered:200], + [self testNestedDataNumbered:300] + ])); + [queryCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +/** + * Creates test data with special characters in field names. Datastore currently prohibits mixing + * nested data with special characters so tests that use this data must be separate. + */ +- (NSDictionary *)testDottedDataNumbered:(int)number { + return @{ + @"a" : [NSString stringWithFormat:@"field %d", number], + @"b.dot" : @(number), + @"c\\slash" : @(number) + }; +} + +- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet { + NSDictionary *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, testData); +} + +- (void)testFieldsWithSpecialCharsCanBeReadDirectly { + NSDictionary *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result[@"a"], testData[@"a"]); + XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]], + testData[@"b.dot"]); + XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]); +} + +- (void)testFieldsWithSpecialCharsCanBeUpdated { + NSDictionary *testData = [self testDottedDataNumbered:1]; + + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:testData]; + [self updateDocumentRef:doc + data:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100, + @"c\\slash" : @200 + }]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 })); +} + +- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters { + NSDictionary *> *testDocs = @{ + @"1" : [self testDottedDataNumbered:300], + @"2" : [self testDottedDataNumbered:100], + @"3" : [self testDottedDataNumbered:200] + }; + + // inequality adds implicit sort on field + NSArray *> *expected = + @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"]; + FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] + isGreaterThanOrEqualTo:@200]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [queryCompletion fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy { + NSDictionary *> *testDocs = @{ + @"1" : [self testDottedDataNumbered:300], + @"2" : [self testDottedDataNumbered:100], + @"3" : [self testDottedDataNumbered:200] + }; + + NSArray *> *expected = @[ + [self testDottedDataNumbered:100], [self testDottedDataNumbered:200], + [self testDottedDataNumbered:300] + ]; + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]]; + XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [queryDot fulfill]; + }]; + [self awaitExpectations]; + + XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"]; + q = [coll queryOrderedByField:@"c\\slash"]; + [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected); + [querySlash fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m new file mode 100644 index 0000000..19771ff --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.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 Firestore; + +#import + +#import "FSTIntegrationTestCase.h" + +@interface FIRListenerRegistrationTests : FSTIntegrationTestCase +@end + +@implementation FIRListenerRegistrationTests + +- (void)testCanBeRemoved { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + __block int callbacks = 0; + id one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacks++; + }]; + + id two = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacks++; + }]; + + // Wait for initial events + [self waitUntil:^BOOL { + return callbacks == 2; + }]; + + // Trigger new events + [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; + + // Write events should have triggered + XCTAssertEqual(4, callbacks); + + // No more events should occur + [one remove]; + [two remove]; + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // Assert no further events occurred + XCTAssertEqual(4, callbacks); +} + +- (void)testCanBeRemovedTwice { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + id one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; + id two = [docRef + addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){ + }]; + + [one remove]; + [one remove]; + + [two remove]; + [two remove]; +} + +- (void)testCanBeRemovedIndependently { + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + __block int callbacksOne = 0; + __block int callbacksTwo = 0; + id one = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacksOne++; + }]; + + id two = [collectionRef + addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { + XCTAssertNil(error); + callbacksTwo++; + }]; + + // Wait for initial events + [self waitUntil:^BOOL { + return callbacksOne == 1 && callbacksTwo == 1; + }]; + + // Trigger new events + [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}]; + + // Write events should have triggered + XCTAssertEqual(2, callbacksOne); + XCTAssertEqual(2, callbacksTwo); + + // Should leave "two" unaffected + [one remove]; + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // Assert only events for "two" actually occurred + XCTAssertEqual(2, callbacksOne); + XCTAssertEqual(3, callbacksTwo); + + [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}]; + + // No more events should occur + [two remove]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m new file mode 100644 index 0000000..f08df33 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m @@ -0,0 +1,197 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "Core/FSTFirestoreClient.h" + +#import "FSTIntegrationTestCase.h" + +@interface FIRQueryTests : FSTIntegrationTestCase +@end + +@implementation FIRQueryTests + +- (void)testLimitQueries { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a"}, + @"b" : @{@"k" : @"b"}, + @"c" : @{@"k" : @"c"} + + }]; + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ])); +} + +- (void)testLimitQueriesWithDescendingSortOrder { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"k" : @"a", @"sort" : @0}, + @"b" : @{@"k" : @"b", @"sort" : @1}, + @"c" : @{@"k" : @"c", @"sort" : @1}, + @"d" : @{@"k" : @"d", @"sort" : @2}, + + }]; + FIRQuerySnapshot *snapshot = + [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES] + queryLimitedTo:2]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ + @{ @"k" : @"d", + @"sort" : @2 }, + @{ @"k" : @"c", + @"sort" : @1 } + ])); +} + +- (void)testKeyOrderIsDescendingForDescendingInequality { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"foo" : @42}, + @"b" : @{@"foo" : @42.0}, + @"c" : @{@"foo" : @42}, + @"d" : @{@"foo" : @21}, + @"e" : @{@"foo" : @21.0}, + @"f" : @{@"foo" : @66}, + @"g" : @{@"foo" : @66.0}, + }]; + FIRQuerySnapshot *snapshot = + [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21] + queryOrderedByField:@"foo" + descending:YES]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ])); +} + +- (void)testUnaryFilterQueries { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)}, + @"b" : @{@"null" : [NSNull null], @"nan" : @0}, + @"c" : @{@"null" : @NO, @"nan" : @(NAN)} + }]; + + FIRQuerySnapshot *results = + [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]] + queryWhereField:@"nan" + isEqualTo:@(NAN)]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ + @{ @"null" : [NSNull null], + @"nan" : @(NAN) } + ])); +} + +- (void)testQueryWithFieldPaths { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"a" : @1}, + @"b" : @{@"a" : @2}, + @"c" : @{@"a" : @3} + }]; + + FIRQuery *query = + [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3]; + query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); +} + +- (void)testFilterOnInfinity { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"inf" : @(INFINITY)}, + @"b" : @{@"inf" : @(-INFINITY)} + }]; + + FIRQuerySnapshot *results = + [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ])); +} + +- (void)testCanExplicitlySortByDocumentID { + NSDictionary *testDocs = @{ + @"a" : @{@"key" : @"a"}, + @"b" : @{@"key" : @"b"}, + @"c" : @{@"key" : @"c"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + + // Ideally this would be descending to validate it's different than + // the default, but that requires an extra index + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), + (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ])); +} + +- (void)testCanQueryByDocumentID { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"ab"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); +} + +- (void)testCanQueryByDocumentIDs { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = + [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"ab"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); + + docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThan:@"aa"] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThanOrEqualTo:@"ba"]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); +} + +- (void)testCanQueryByDocumentIDsUsingRefs { + NSDictionary *testDocs = @{ + @"aa" : @{@"key" : @"aa"}, + @"ab" : @{@"key" : @"ab"}, + @"ba" : @{@"key" : @"ba"}, + @"bb" : @{@"key" : @"bb"}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *docs = [self + readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:[collection documentWithPath:@"ab"]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ])); + + docs = [self + readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThan:[collection documentWithPath:@"aa"]] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m new file mode 100644 index 0000000..1d77e16 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m @@ -0,0 +1,183 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "Core/FSTFirestoreClient.h" + +#import "FSTEventAccumulator.h" +#import "FSTIntegrationTestCase.h" + +@interface FIRServerTimestampTests : FSTIntegrationTestCase +@end + +@implementation FIRServerTimestampTests { + // Data written in tests via set. + NSDictionary *_setData; + + // Base and update data used for update tests. + NSDictionary *_initialData; + NSDictionary *_updateData; + + // A document reference to read and write to. + FIRDocumentReference *_docRef; + + // Accumulator used to capture events during the test. + FSTEventAccumulator *_accumulator; + + // Listener registration for a listener maintained during the course of the test. + id _listenerRegistration; +} + +- (void)setUp { + [super setUp]; + + // Data written in tests via set. + _setData = @{ + @"a" : @42, + @"when" : [FIRFieldValue fieldValueForServerTimestamp], + @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + // Base and update data used for update tests. + _initialData = @{ @"a" : @42 }; + _updateData = @{ + @"when" : [FIRFieldValue fieldValueForServerTimestamp], + @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + _docRef = [self documentRef]; + _accumulator = [FSTEventAccumulator accumulatorForTest:self]; + _listenerRegistration = [_docRef addSnapshotListener:_accumulator.handler]; + + // Wait for initial nil snapshot to avoid potential races. + FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnapshot.exists); +} + +- (void)tearDown { + [_listenerRegistration remove]; + + [super tearDown]; +} + +// Returns the expected data, with an arbitrary timestamp substituted in. +- (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp { + return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} }; +} + +/** Writes _initialData and waits for the corresponding snapshot. */ +- (void)writeInitialData { + [self writeDocumentRef:_docRef data:_initialData]; + FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."]; + XCTAssertEqualObjects(initialDataSnap.data, _initialData); +} + +/** Waits for a snapshot containing _setData but with NSNull for the timestamps. */ +- (void)waitForLocalEvent { + FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."]; + XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]); +} + +/** Waits for a snapshot containing _setData but with resolved server timestamps. */ +- (void)waitForRemoteEvent { + // server event should have a resolved timestamp; verify it. + FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"]; + XCTAssertTrue(remoteSnap.exists); + NSDate *when = remoteSnap[@"when"]; + XCTAssertTrue([when isKindOfClass:[NSDate class]]); + // Tolerate up to 10 seconds of clock skew between client and server. + XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10); + + // Validate the rest of the document. + XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]); +} + +- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; + [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + transactionBlock(transaction); + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testServerTimestampsWorkViaSet { + [self writeDocumentRef:_docRef data:_setData]; + [self waitForLocalEvent]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWorkViaUpdate { + [self writeInitialData]; + [self updateDocumentRef:_docRef data:_updateData]; + [self waitForLocalEvent]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWorkViaTransactionSet { + [self runTransactionBlock:^(FIRTransaction *transaction) { + [transaction setData:_setData forDocument:_docRef]; + }]; + + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWorkViaTransactionUpdate { + [self writeInitialData]; + [self runTransactionBlock:^(FIRTransaction *transaction) { + [transaction updateData:_updateData forDocument:_docRef]; + }]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument { + XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"]; + [_docRef updateData:_updateData + completion:^(NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; + [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + [transaction updateData:_updateData forDocument:_docRef]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any + // error and so this turns into Aborted instead. + // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want??? + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m new file mode 100644 index 0000000..f3021dd --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "FSTIntegrationTestCase.h" + +@interface FIRTypeTests : FSTIntegrationTestCase +@end + +@implementation FIRTypeTests + +- (void)assertSuccessfulRoundtrip:(NSDictionary *)data { + FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"]; + + [self writeDocumentRef:doc data:data]; + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertTrue(document.exists); + XCTAssertEqualObjects(document.data, data); +} + +- (void)testCanReadAndWriteNullFields { + [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }]; +} + +- (void)testCanReadAndWriteArrayFields { + [self assertSuccessfulRoundtrip:@{ + @"array" : @[ @1, @"foo", @{@"deep" : @YES}, [NSNull null] ] + }]; +} + +- (void)testCanReadAndWriteBlobFields { + NSData *data = [NSData dataWithBytes:"\0\1\2" length:3]; + [self assertSuccessfulRoundtrip:@{@"blob" : data}]; +} + +- (void)testCanReadAndWriteGeoPointFields { + [self assertSuccessfulRoundtrip:@{ + @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56] + }]; +} + +- (void)testCanReadAndWriteTimestampFields { + // Choose a value that can be converted losslessly between fixed point and double + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125]; + [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}]; +} + +- (void)testCanReadAndWriteDocumentReferences { + // We can't use assertSuccessfulRoundtrip since FIRDocumentReference doesn't implement isEqual. + FIRDocumentReference *docRef = [self.db documentWithPath:@"rooms/eros"]; + id data = @{ @"a" : @42, @"ref" : docRef }; + [self writeDocumentRef:docRef data:data]; + + FIRDocumentSnapshot *readDoc = [self readDocumentForRef:docRef]; + XCTAssertTrue(readDoc.exists); + + XCTAssertEqualObjects(readDoc[@"a"], data[@"a"]); + FIRDocumentReference *readDocRef = readDoc[@"ref"]; + XCTAssertTrue([readDocRef isKindOfClass:[FIRDocumentReference class]]); + XCTAssertEqualObjects(readDocRef.path, docRef.path); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m new file mode 100644 index 0000000..1ba1d7a --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m @@ -0,0 +1,560 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "FSTHelpers.h" +#import "FSTIntegrationTestCase.h" + +// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings. +#pragma clang diagnostic ignored "-Wnonnull" + +@interface FIRValidationTests : FSTIntegrationTestCase +@end + +@implementation FIRValidationTests + +#pragma mark - FIRFirestoreSettings Validation + +- (void)testNilHostFails { + FIRFirestoreSettings *settings = self.db.settings; + FSTAssertThrows(settings.host = nil, + @"host setting may not be nil. You should generally just use the default value " + "(which is firestore.googleapis.com)"); +} + +- (void)testNilDispatchQueueFails { + FIRFirestoreSettings *settings = self.db.settings; + FSTAssertThrows(settings.dispatchQueue = nil, + @"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())"); +} + +- (void)testChangingSettingsAfterUseFails { + FIRFirestoreSettings *settings = self.db.settings; + [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }]; + settings.host = @"example.com"; + FSTAssertThrows(self.db.settings = settings, + @"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."); +} + +#pragma mark - FIRFirestore Validation + +- (void)testNilFIRAppFails { + FSTAssertThrows( + [FIRFirestore firestoreForApp:nil], + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the " + "default FirebaseApp instance."); +} + +// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID. + +- (void)testNilTransactionBlocksFail { + FSTAssertThrows([self.db runTransactionWithBlock:nil + completion:^(id result, NSError *error) { + XCTFail(@"Completion shouldn't run."); + }], + @"Transaction block cannot be nil."); + + FSTAssertThrows( + [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + XCTFail(@"Transaction block shouldn't run."); + return nil; + } + completion:nil], + @"Transaction completion block cannot be nil."); +} + +#pragma mark - Collection and Document Path Validation + +- (void)testNilCollectionPathsFail { + FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; + NSString *nilError = @"Collection path cannot be nil."; + FSTAssertThrows([self.db collectionWithPath:nil], nilError); + FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError); +} + +- (void)testWrongLengthCollectionPathsFail { + FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"]; + NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ]; + NSArray *badRelativePaths = @[ @"", @"baz/quu" ]; + NSArray *badPathLengths = @[ @2, @4 ]; + NSString *errorFormat = + @"Invalid collection reference. Collection references must have an odd " + @"number of segments, but %@ has %@"; + for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { + NSString *error = + [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; + FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error); + FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error); + } +} + +- (void)testNilDocumentPathsFail { + FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; + NSString *nilError = @"Document path cannot be nil."; + FSTAssertThrows([self.db documentWithPath:nil], nilError); + FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError); +} + +- (void)testWrongLengthDocumentPathsFail { + FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"]; + NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ]; + NSArray *badRelativePaths = @[ @"", @"bar/baz" ]; + NSArray *badPathLengths = @[ @1, @3 ]; + NSString *errorFormat = + @"Invalid document reference. Document references must have an even " + @"number of segments, but %@ has %@"; + for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) { + NSString *error = + [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]]; + FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error); + FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error); + } +} + +- (void)testPathsWithEmptySegmentsFail { + // We're only testing using collectionWithPath since the validation happens in FSTPath which is + // shared by all methods that accept paths. + + // leading / trailing slashes are okay. + [self.db collectionWithPath:@"/foo/"]; + [self.db collectionWithPath:@"/foo"]; + [self.db collectionWithPath:@"foo/"]; + + FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"], + @"Invalid path (foo//bar/baz). Paths must not contain // in them."); + FSTAssertThrows([self.db collectionWithPath:@"//foo"], + @"Invalid path (//foo). Paths must not contain // in them."); + FSTAssertThrows([self.db collectionWithPath:@"foo//"], + @"Invalid path (foo//). Paths must not contain // in them."); +} + +#pragma mark - Write Validation + +- (void)testWritesWithNonDictionaryValuesFail { + NSArray *badData = @[ + @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete], + [FIRFieldValue fieldValueForServerTimestamp] + ]; + + for (id data in badData) { + [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."]; + } +} + +- (void)testWritesWithNestedArraysFail { + [self expectWrite:@{ + @"nested-array" : @[ @1, @[ @2 ] ] + } + toFailWithReason:@"Nested arrays are not supported"]; +} + +- (void)testWritesWithInvalidTypesFail { + [self expectWrite:@{ + @"foo" : @{@"bar" : self} + } + toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"]; +} + +- (void)testWritesWithLargeNumbersFail { + NSNumber *num = @((unsigned long long)LONG_MAX + 1); + NSString *reason = + [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num]; + [self expectWrite:@{@"num" : num} toFailWithReason:reason]; +} + +- (void)testWritesWithReferencesToADifferentDatabaseFail { + FIRDocumentReference *ref = + [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"]; + id data = @{@"foo" : ref}; + [self expectWrite:data + toFailWithReason: + [NSString + stringWithFormat:@"Document Reference is for database different-db/(default) but " + "should be for database %@/(default) (found in field foo)", + [FSTIntegrationTestCase projectID]]]; +} + +- (void)testWritesWithReservedFieldsFail { + [self expectWrite:@{ + @"__baz__" : @1 + } + toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; + [self expectWrite:@{ + @"foo" : @{@"__baz__" : @1} + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; + [self expectWrite:@{ + @"__baz__" : @{@"foo" : @1} + } + toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"]; + + [self expectUpdate:@{ + @"foo.__baz__" : @1 + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field foo.__baz__)"]; + [self expectUpdate:@{ + @"__baz__.foo" : @1 + } + toFailWithReason: + @"Document fields cannot begin and end with __ (found in field __baz__.foo)"]; + [self expectUpdate:@{ + @1 : @1 + } + toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."]; +} + +- (void)testSetsWithFieldValueDeleteFail { + [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]} + toFailWithReason:@"FieldValue.delete() can only be used with updateData()."]; +} + +- (void)testUpdatesWithNestedFieldValueDeleteFail { + [self expectUpdate:@{ + @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]} + } + toFailWithReason: + @"FieldValue.delete() can only appear at the top level of your update data " + "(found in field foo.bar)"]; +} + +- (void)testBatchWritesWithIncorrectReferencesFail { + FIRFirestore *db1 = [self firestore]; + FIRFirestore *db2 = [self firestore]; + XCTAssertNotEqual(db1, db2); + + NSString *reason = @"Provided document reference is from a different Firestore instance."; + id data = @{ @"foo" : @1 }; + FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; + FIRWriteBatch *batch = [db1 batch]; + FSTAssertThrows([batch setData:data forDocument:badRef], reason); + FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); + FSTAssertThrows([batch updateData:data forDocument:badRef], reason); + FSTAssertThrows([batch deleteDocument:badRef], reason); +} + +- (void)testTransactionWritesWithIncorrectReferencesFail { + FIRFirestore *db1 = [self firestore]; + FIRFirestore *db2 = [self firestore]; + XCTAssertNotEqual(db1, db2); + + NSString *reason = @"Provided document reference is from a different Firestore instance."; + id data = @{ @"foo" : @1 }; + FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"]; + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) { + FSTAssertThrows([txn getDocument:badRef error:nil], reason); + FSTAssertThrows([txn setData:data forDocument:badRef], reason); + FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason); + FSTAssertThrows([txn updateData:data forDocument:badRef], reason); + FSTAssertThrows([txn deleteDocument:badRef], reason); + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +#pragma mark - Field Path validation +// TODO(b/37244157): More validation for invalid field paths. + +- (void)testFieldPathsWithEmptySegmentsFail { + NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ]; + + for (NSString *fieldPath in badFieldPaths) { + NSString *reason = + [NSString stringWithFormat: + @"Invalid field path (%@). Paths must not be empty, begin with " + @"'.', end with '.', or contain '..'", + fieldPath]; + [self expectFieldPath:fieldPath toFailWithReason:reason]; + } +} + +- (void)testFieldPathsWithInvalidSegmentsFail { + NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ]; + + for (NSString *fieldPath in badFieldPaths) { + NSString *reason = + [NSString stringWithFormat: + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", + fieldPath]; + [self expectFieldPath:fieldPath toFailWithReason:reason]; + } +} + +#pragma mark - Query Validation + +- (void)testQueryWithNonPositiveLimitFails { + FSTAssertThrows([[self collectionRef] queryLimitedTo:0], + @"Invalid Query. Query limit (0) is invalid. Limit must be positive."); + FSTAssertThrows([[self collectionRef] queryLimitedTo:-1], + @"Invalid Query. Query limit (-1) is invalid. Limit must be positive."); +} + +- (void)testQueryInequalityOnNullOrNaNFails { + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil], + @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]], + @"Invalid Query. You can only perform equality comparisons on nil / NSNull."); + + FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)], + @"Invalid Query. You can only perform equality comparisons on NaN."); +} + +- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues { + FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ + @"f" : @{@"v" : @"f", @"nosort" : @1.0} + }]; + + FIRQuery *query = [testCollection queryOrderedByField:@"sort"]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]]; + XCTAssertTrue(snapshot.exists); + + NSString *reason = + @"Invalid query. You are trying to start or end a query using a document for " + "which the field 'sort' (used as the order by) does not exist."; + FSTAssertThrows([query queryStartingAtDocument:snapshot], reason); + FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason); + FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason); + FSTAssertThrows([query queryEndingAtDocument:snapshot], reason); +} + +- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; + + NSString *reason = + @"Invalid query. You are trying to start or end a query using more values " + "than were specified in the order by."; + // More elements than order by + FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason); + FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]), + reason); +} + +- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]]; + FSTAssertThrows([query queryStartingAtValues:@[ @1 ]], + @"Invalid query. Expected a string for the document ID."); + FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]], + @"Invalid query. Document ID 'foo/bar' contains a slash."); +} + +- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder { + FIRCollectionReference *testCollection = [self collectionRef]; + FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; + NSString *reason = + @"Invalid query. You must not specify a starting point before specifying the order by."; + FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + reason = @"Invalid query. You must not specify an ending point before specifying the order by."; + FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); + FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason); +} + +- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences { + FIRCollectionReference *collection = [self collectionRef]; + NSString *reason = + @"Invalid query. When querying by document ID you must provide a valid " + "document ID, but it was an empty string."; + FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason); + + reason = + @"Invalid query. When querying by document ID you must provide a valid document ID, " + "but 'foo/bar/baz' contains a '/' character."; + FSTAssertThrows( + [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason); + + reason = + @"Invalid query. When querying by document ID you must provide a valid string or " + "DocumentReference, but it was of type: __NSCFNumber"; + FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason); +} + +- (void)testQueryInequalityFieldMustMatchFirstOrderByField { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32]; + + FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"], + @"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 'x' and 'y'"); + + NSString *reason = + @"Invalid query. You have a where filter with " + "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) " + "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, " + "but your first queryOrderedBy is currently on field 'y' instead."; + FSTAssertThrows([base queryOrderedByField:@"y"], reason); + FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason); + FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason); + FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x" + isGreaterThan:@32], + reason); + + XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"], + @"Same inequality fields work"); + + XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"], + @"Inequality and equality on different fields works"); + + XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works"); + XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32], + @"inequality same as order by works"); + XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"], + @"inequality same as first order by works."); + XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x" + isGreaterThan:@32], + @"inequality same as first order by works."); +} + +#pragma mark - GeoPoint Validation + +- (void)testInvalidGeoPointParameters { + [self verifyExceptionForInvalidLatitude:NAN]; + [self verifyExceptionForInvalidLatitude:-INFINITY]; + [self verifyExceptionForInvalidLatitude:INFINITY]; + [self verifyExceptionForInvalidLatitude:-90.1]; + [self verifyExceptionForInvalidLatitude:90.1]; + + [self verifyExceptionForInvalidLongitude:NAN]; + [self verifyExceptionForInvalidLongitude:-INFINITY]; + [self verifyExceptionForInvalidLongitude:INFINITY]; + [self verifyExceptionForInvalidLongitude:-180.1]; + [self verifyExceptionForInvalidLongitude:180.1]; +} + +#pragma mark - Helpers + +/** Performs a write using each write API and makes sure it fails with the expected reason. */ +- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES]; +} + +/** Performs a write using each set API and makes sure it fails with the expected reason. */ +- (void)expectSet:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO]; +} + +/** Performs a write using each update API and makes sure it fails with the expected reason. */ +- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason { + [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES]; +} + +/** + * Performs a write using each set and/or update API and makes sure it fails with the expected + * reason. + */ +- (void)expectWrite:(id)data + toFailWithReason:(NSString *)reason + includeSets:(BOOL)includeSets + includeUpdates:(BOOL)includeUpdates { + FIRDocumentReference *ref = [self documentRef]; + if (includeSets) { + FSTAssertThrows([ref setData:data], reason, @"for %@", data); + FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data); + } + + if (includeUpdates) { + FSTAssertThrows([ref updateData:data], reason, @"for %@", data); + FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@", + data); + } + + XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"]; + [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { + if (includeSets) { + FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data); + } + if (includeUpdates) { + FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data); + } + return nil; + } + completion:^(id result, NSError *error) { + // ends up being a no-op transaction. + XCTAssertNil(error); + [transactionDone fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testFieldNamesMustNotBeEmpty { + NSString *reason = @"Invalid field path. Provided names must not be empty."; + FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason); + + reason = @"Invalid field name at index 0. Field names must not be empty."; + FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason); + + reason = @"Invalid field name at index 1. Field names must not be empty."; + FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason); +} + +/** + * Tests a field path with all of our APIs that accept field paths and ensures they fail with the + * specified reason. + */ +- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason { + // Get an arbitrary snapshot we can use for testing. + FIRDocumentReference *docRef = [self documentRef]; + [self writeDocumentRef:docRef data:@{ @"test" : @1 }]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef]; + + // Update paths. + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + dict[fieldPath] = @1; + [self expectUpdate:dict toFailWithReason:reason]; + + // Snapshot fields. + FSTAssertThrows(snapshot[fieldPath], reason); + + // Query filter / order fields. + FIRCollectionReference *collection = [self collectionRef]; + FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason); + // isLessThan, etc. omitted for brevity since the code path is trivially shared. + FSTAssertThrows([collection queryOrderedByField:fieldPath], reason); +} + +- (void)verifyExceptionForInvalidLatitude:(double)latitude { + NSString *reason = [NSString + stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f", + latitude]; + FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason); +} + +- (void)verifyExceptionForInvalidLongitude:(double)longitude { + NSString *reason = + [NSString stringWithFormat: + @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f", + longitude]; + FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m new file mode 100644 index 0000000..159cbd7 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m @@ -0,0 +1,313 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import + +#import "FSTEventAccumulator.h" +#import "FSTIntegrationTestCase.h" + +@interface FIRWriteBatchTests : FSTIntegrationTestCase +@end + +@implementation FIRWriteBatchTests + +- (void)testSupportEmptyBatches { + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + [[[self firestore] batch] commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testSetDocuments { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a" : @"b"} forDocument:doc]; + [batch setData:@{@"c" : @"d"} forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); +} + +- (void)testSetDocumentWithMerge { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; + [batch setData:@{ + @"c" : @"d", + @"nested" : @{@"c" : @"d"} + } + forDocument:doc + options:[FIRSetOptions merge]]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects( + snapshot.data, ( + @{ @"a" : @"b", + @"c" : @"d", + @"nested" : @{@"a" : @"b", @"c" : @"d"} })); +} + +- (void)testUpdateDocuments { + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch updateData:@{ @"baz" : @42 } forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 })); +} + +- (void)testCannotUpdateNonexistentDocuments { + FIRDocumentReference *doc = [self documentRef]; + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch updateData:@{ @"baz" : @42 } forDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNotNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testDeleteDocuments { + FIRDocumentReference *doc = [self documentRef]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + + XCTAssertTrue(snapshot.exists); + XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch deleteDocument:doc]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [batchExpectation fulfill]; + }]; + [self awaitExpectations]; + snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testBatchesCommitAtomicallyRaisingCorrectEvents { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.handler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write two documents. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{ @"a" : @1 } forDocument:docA]; + [batch setData:@{ @"b" : @2 } forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); + + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ])); +} + +- (void)testBatchesFailAtomicallyRaisingCorrectEvents { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.handler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write 1 document and update a nonexistent document. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{ @"a" : @1 } forDocument:docA]; + [batch updateData:@{ @"b" : @2 } forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); + [expectation fulfill]; + }]; + + // Local event with the set document. + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ])); + + // Server event with the set reverted. + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqual(serverSnap.count, 0); +} + +- (void)testWriteTheSameServerTimestampAcrossWrites { + FIRCollectionReference *collection = [self collectionRef]; + FIRDocumentReference *docA = [collection documentWithPath:@"a"]; + FIRDocumentReference *docB = [collection documentWithPath:@"b"]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] + includeQueryMetadataChanges:YES] + listener:accumulator.handler]; + FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertEqual(initialSnap.count, 0); + + // Atomically write 2 documents with server timestamps. + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [collection.firestore batch]; + [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA]; + [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), + (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ])); + + FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + XCTAssertEqual(serverSnap.count, 2); + NSDate *when = serverSnap.documents[0][@"when"]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), + (@[ @{@"when" : when}, @{@"when" : when} ])); +} + +- (void)testCanWriteTheSameDocumentMultipleTimes { + FIRDocumentReference *doc = [self documentRef]; + FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; + [doc + addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES] + listener:accumulator.handler]; + FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnap.exists); + + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch deleteDocument:doc]; + [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc]; + [batch updateData:@{ + @"b" : @2, + @"when" : [FIRFieldValue fieldValueForServerTimestamp] + } + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; + XCTAssertTrue(localSnap.metadata.hasPendingWrites); + XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] })); + + FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"]; + XCTAssertFalse(serverSnap.metadata.hasPendingWrites); + NSDate *when = serverSnap[@"when"]; + XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when })); +} + +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [batch updateData:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" + } + forDocument:doc]; + + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + } + forDocument:doc]; + [batch updateData:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + } + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Integration/CAcert.pem b/Firestore/Example/Tests/Integration/CAcert.pem new file mode 100644 index 0000000..e69de29 diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.m b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m new file mode 100644 index 0000000..bab8f44 --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m @@ -0,0 +1,239 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import +#import +#import + +#import "API/FIRDocumentReference+Internal.h" +#import "API/FSTUserDataConverter.h" +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Core/FSTFirestoreClient.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Remote/FSTDatastore.h" +#import "Remote/FSTRemoteEvent.h" +#import "Remote/FSTRemoteStore.h" +#import "Util/FSTAssert.h" +#import "Util/FSTDispatchQueue.h" +#import "Util/FSTUtil.h" + +#import "FSTIntegrationTestCase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteStore (Tests) +- (void)commitBatch:(FSTMutationBatch *)batch; +@end + +#pragma mark - FSTRemoteStoreEventCapture + +@interface FSTRemoteStoreEventCapture : NSObject + +- (instancetype)init __attribute__((unavailable("Use initWithTestCase:"))); + +- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase NS_DESIGNATED_INITIALIZER; + +- (void)expectWriteEventWithDescription:(NSString *)description; +- (void)expectListenEventWithDescription:(NSString *)description; + +@property(nonatomic, weak, nullable) XCTestCase *testCase; +@property(nonatomic, strong) NSMutableArray *writeEvents; +@property(nonatomic, strong) NSMutableArray *listenEvents; +@property(nonatomic, strong) NSMutableArray *writeEventExpectations; +@property(nonatomic, strong) NSMutableArray *listenEventExpectations; +@end + +@implementation FSTRemoteStoreEventCapture + +- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { + if (self = [super init]) { + _writeEvents = [NSMutableArray array]; + _listenEvents = [NSMutableArray array]; + _testCase = testCase; + _writeEventExpectations = [NSMutableArray array]; + _listenEventExpectations = [NSMutableArray array]; + } + return self; +} + +- (void)expectWriteEventWithDescription:(NSString *)description { + [self.writeEventExpectations + addObject:[self.testCase + expectationWithDescription:[NSString + stringWithFormat:@"write event %lu: %@", + (unsigned long) + self.writeEventExpectations + .count, + description]]]; +} + +- (void)expectListenEventWithDescription:(NSString *)description { + [self.listenEventExpectations + addObject:[self.testCase + expectationWithDescription:[NSString + stringWithFormat:@"listen event %lu: %@", + (unsigned long) + self.listenEventExpectations + .count, + description]]]; +} + +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { + [self.writeEvents addObject:batchResult]; + XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0]; + [self.writeEventExpectations removeObjectAtIndex:0]; + [expectation fulfill]; +} + +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error { + FSTFail(@"Not implemented"); +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + [self.listenEvents addObject:remoteEvent]; + XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; + [self.listenEventExpectations removeObjectAtIndex:0]; + [expectation fulfill]; +} + +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { + FSTFail(@"Not implemented"); +} + +@end + +#pragma mark - FSTDatastoreTests + +@interface FSTDatastoreTests : XCTestCase + +@end + +@implementation FSTDatastoreTests { + FSTDispatchQueue *_testWorkerQueue; + FSTLocalStore *_localStore; + id _credentials; + + FSTDatastore *_datastore; + FSTRemoteStore *_remoteStore; +} + +- (void)setUp { + [super setUp]; + + NSString *projectID = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; + if (!projectID) { + projectID = @"test-db"; + } + + FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings]; + if (!settings.sslEnabled) { + [GRPCCall useInsecureConnectionsForHost:settings.host]; + } + + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:projectID database:kDefaultDatabaseID]; + + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test-key" + host:settings.host + sslEnabled:settings.sslEnabled]; + + _testWorkerQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firestore.FSTDatastoreTestsWorkerQueue", + DISPATCH_QUEUE_SERIAL)]; + + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + _datastore = [FSTDatastore datastoreWithDatabase:databaseInfo + workerDispatchQueue:_testWorkerQueue + credentials:_credentials]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + [_remoteStore start]; +} + +- (void)tearDown { + XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"]; + [_testWorkerQueue dispatchAsync:^{ + [_remoteStore shutdown]; + [completion fulfill]; + }]; + [self awaitExpectations]; + + [super tearDown]; +} + +- (void)testCommit { + XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"]; + + [_datastore commitMutations:@[] + completion:^(NSError *_Nullable error) { + XCTAssertNil(error, @"Failed to commit"); + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testStreamingWrite { + FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; + [capture expectWriteEventWithDescription:@"write mutations"]; + + _remoteStore.syncEngine = capture; + + FSTSetMutation *mutation = [self setMutation]; + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 + localWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ]]; + [_testWorkerQueue dispatchAsync:^{ + [_remoteStore commitBatch:batch]; + }]; + + [self awaitExpectations]; +} + +- (void)awaitExpectations { + [self waitForExpectationsWithTimeout:4.0 + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; +} + +- (FSTSetMutation *)setMutation { + return [[FSTSetMutation alloc] + initWithKey:[FSTDocumentKey keyWithPathString:@"rooms/eros"] + value:[[FSTObjectValue alloc] + initWithDictionary:@{@"name" : [FSTStringValue stringValue:@"Eros"]}] + precondition:[FSTPrecondition none]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.m b/Firestore/Example/Tests/Integration/FSTSmokeTests.m new file mode 100644 index 0000000..e0ad06c --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTSmokeTests.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 Firestore; + +#import + +#import "FSTEventAccumulator.h" +#import "FSTIntegrationTestCase.h" + +@interface FSTSmokeTests : FSTIntegrationTestCase +@end + +@implementation FSTSmokeTests + +- (void)testCanWriteASingleDocument { + FIRDocumentReference *ref = [self documentRef]; + [self writeDocumentRef:ref data:[self chatMessage]]; +} + +- (void)testCanReadAWrittenDocument { + NSDictionary *data = [self chatMessage]; + + FIRDocumentReference *ref = [self documentRef]; + [self writeDocumentRef:ref data:data]; + + FIRDocumentSnapshot *doc = [self readDocumentForRef:ref]; + XCTAssertEqualObjects(doc.data, data); +} + +- (void)testObservesExistingDocument { + [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef) { + NSDictionary *data = [self chatMessage]; + [self writeDocumentRef:writerRef data:data]; + + id listenerRegistration = + [readerRef addSnapshotListener:self.eventAccumulator.handler]; + + FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"]; + XCTAssertEqual([doc class], [FIRDocumentSnapshot class]); + XCTAssertEqualObjects(doc.data, data); + + [listenerRegistration remove]; + }]; +} + +- (void)testObservesNewDocument { + [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef) { + id listenerRegistration = + [readerRef addSnapshotListener:self.eventAccumulator.handler]; + + FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"]; + XCTAssertFalse(doc1.exists); + // TODO(b/36366944): add tests for doc1.path) + + NSDictionary *data = [self chatMessage]; + [self writeDocumentRef:writerRef data:data]; + + FIRDocumentSnapshot *doc2 = [self.eventAccumulator awaitEventWithName:@"full snapshot"]; + XCTAssertEqual([doc2 class], [FIRDocumentSnapshot class]); + XCTAssertEqualObjects(doc2.data, data); + + [listenerRegistration remove]; + }]; +} + +- (void)testWillFireValueEventsForEmptyCollections { + FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"]; + id listenerRegistration = + [collection addSnapshotListener:self.eventAccumulator.handler]; + + FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"]; + XCTAssertEqual([snap class], [FIRQuerySnapshot class]); + XCTAssertEqual(snap.count, 0); + + [listenerRegistration remove]; +} + +- (void)testGetCollectionQuery { + NSDictionary *testDocs = @{ + @"1" : @{@"name" : @"Patryk", @"message" : @"Real data, yo!"}, + @"2" : @{@"name" : @"Gil", @"message" : @"Yep!"}, + @"3" : @{@"name" : @"Jonny", @"message" : @"Back to work!"}, + }; + + FIRCollectionReference *docs = [self collectionRefWithDocuments:testDocs]; + FIRQuerySnapshot *result = [self readDocumentSetForRef:docs]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), + (@[ testDocs[@"1"], testDocs[@"2"], testDocs[@"3"] ])); +} + +// TODO(klimt): This test is disabled because we can't create compound indexes programmatically. +- (void)xtestQueryByFieldAndUseOrderBy { + NSDictionary *testDocs = @{ + @"1" : @{@"sort" : @1, @"filter" : @YES, @"key" : @"1"}, + @"2" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"2"}, + @"3" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"3"}, + @"4" : @{@"sort" : @3, @"filter" : @NO, @"key" : @"4"} + }; + + FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs]; + + FIRQuery *query = + [[coll queryWhereField:@"filter" isEqualTo:@YES] queryOrderedByField:@"sort" descending:YES]; + FIRQuerySnapshot *result = [self readDocumentSetForRef:query]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(result), + (@[ testDocs[@"2"], testDocs[@"3"], testDocs[@"1"] ])); +} + +- (NSDictionary *)chatMessage { + return @{@"name" : @"Patryk", @"message" : @"We are actually writing data!"}; +} + +@end diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.m b/Firestore/Example/Tests/Integration/FSTTransactionTests.m new file mode 100644 index 0000000..a0b5bbe --- /dev/null +++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.m @@ -0,0 +1,541 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import Firestore; + +#import +#include + +#import "FSTIntegrationTestCase.h" + +@interface FSTTransactionTests : FSTIntegrationTestCase +@end + +@implementation FSTTransactionTests + +// We currently require every document read to also be written. +// TODO(b/34879758): Re-enable this test once we fix it. +- (void)xtestGetDocuments { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny" }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + // We currently require every document read to also be written. + // TODO(b/34879758): Fix this check once we drop that requirement. + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocument { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction deleteDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testGetNonexistentDocumentThenCreate { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertFalse(snapshot.exists); + [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot.exists); + XCTAssertEqualObjects(@"bar", snapshot[@"foo"]); +} + +- (void)testGetNonexistentDocumentThenFailPatch { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertFalse(snapshot.exists); + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocumentAndPatch { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertTrue(snapshot.exists); + [transaction deleteDocument:doc]; + // Since we deleted the doc, the update will fail + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testDeleteDocumentAndSet { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertTrue(snapshot.exists); + [transaction deleteDocument:doc]; + // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we + // just let the transaction fail and verify it's unsupported for now + [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + // TODO(dimond): This is probably the wrong error code, but it's what we use today. We + // should update the code once the underlying error was fixed. + XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testWriteDocumentTwice { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) { + [transaction setData:@{@"a" : @"b"} forDocument:doc]; + [transaction setData:@{@"c" : @"d"} forDocument:doc]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"}); +} + +- (void)testSetDocumentWithMerge { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc]; + [transaction setData:@{ + @"c" : @"d", + @"nested" : @{@"c" : @"d"} + } + forDocument:doc + options:[FIRSetOptions merge]]; + return @YES; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@YES, result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects( + snapshot.data, ( + @{ @"a" : @"b", + @"c" : @"d", + @"nested" : @{@"a" : @"b", @"c" : @"d"} })); +} + +- (void)testCannotUpdateNonExistentDocument { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + [transaction updateData:@{@"foo" : @"bar"} forDocument:doc]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertFalse(result.exists); +} + +- (void)testIncrementTransactionally { + // A barrier to make sure every transaction reaches the same spot. + dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); + __block volatile int32_t started = 0; + + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(5.0) }]; + + // Make 3 transactions that will all increment. + int total = 3; + for (int i = 0; i < total; i++) { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + int32_t nowStarted = OSAtomicIncrement32(&started); + // Once all of the transactions have read, allow the first write. + if (nowStarted == total) { + dispatch_semaphore_signal(writeBarrier); + } + + dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); + // Refill the barrier so that the other transactions and retries succeed. + dispatch_semaphore_signal(writeBarrier); + + double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; + [transaction setData:@{ @"count" : @(newCount) } forDocument:doc]; + return @YES; + + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + } + + [self awaitExpectations]; + // Now all transaction should be completed, so check the result. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); +} + +- (void)testUpdateTransactionally { + // A barrier to make sure every transaction reaches the same spot. + dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); + __block volatile int32_t started = 0; + + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(5.0), @"other" : @"yes" }]; + + // Make 3 transactions that will all increment. + int total = 3; + for (int i = 0; i < total; i++) { + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + int32_t nowStarted = OSAtomicIncrement32(&started); + // Once all of the transactions have read, allow the first write. + if (nowStarted == total) { + dispatch_semaphore_signal(writeBarrier); + } + + dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER); + // Refill the barrier so that the other transactions and retries succeed. + dispatch_semaphore_signal(writeBarrier); + + double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0; + [transaction updateData:@{ @"count" : @(newCount) } forDocument:doc]; + return @YES; + + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + } + + [self awaitExpectations]; + // Now all transaction should be completed, so check the result. + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); + XCTAssertEqualObjects(@"yes", snapshot[@"other"]); +} + +// We currently require every document read to also be written. +// TODO(b/34879758): Re-enable this test once we fix it. +- (void)xtestHandleReadingOneDocAndWritingAnother { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + + [self writeDocumentRef:doc1 data:@{ @"count" : @(15.0) }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + // Get the first doc. + [transaction getDocument:doc1 error:error]; + XCTAssertNil(*error); + // Do a write outside of the transaction. The first time the + // transaction is tried, this will bump the version, which + // will cause the write to doc2 to fail. The second time, it + // will be a no-op and not bump the version. + dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); + [doc1 setData:@{ + @"count" : @(1234) + } + completion:^(NSError *_Nullable error) { + dispatch_semaphore_signal(writeSemaphore); + }]; + // We can block on it, because transactions run on a background queue. + dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); + // Now try to update the other doc from within the transaction. + // This should fail once, because we read 15 earlier. + [transaction setData:@{ @"count" : @(16) } forDocument:doc2]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + // We currently require every document read to also be written. + // TODO(b/34879758): Add this check back once we drop that. + // NSError *error = nil; + // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error]; + // XCTAssertNil(error); + // XCTAssertEquals(0, tries); + // XCTAssertEqualObjects(@(1234), snapshot[@"count"]); + // snapshot = [transaction getDocument:doc2 error:&error]; + // XCTAssertNil(error); + // XCTAssertEqualObjects(@(16), snapshot[@"count"]); + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testReadingADocTwiceWithDifferentVersions { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{ @"count" : @(15.0) }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + // Get the doc once. + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertNil(*error); + XCTAssertEqualObjects(@(15), snapshot[@"count"]); + // Do a write outside of the transaction. + dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); + [doc setData:@{ + @"count" : @(1234) + } + completion:^(NSError *_Nullable error) { + dispatch_semaphore_signal(writeSemaphore); + }]; + // We can block on it, because transactions run on a background queue. + dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER); + // Get the doc again in the transaction with the new version. + snapshot = [transaction getDocument:doc error:error]; + // The get itself will fail, because we already read an earlier version of this document. + // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the + // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same + // do multiple times. But they need to handle read errors anyway. + XCTAssertNotNil(*error); + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]); +} + +// We currently require every document read to also be written. +// TODO(b/34879758): Add this test back once we fix that. +- (void)xtestCannotHaveAGetWithoutMutations { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID]; + [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; + XCTAssertTrue(snapshot.exists); + XCTAssertNil(*error); + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testSuccessWithNoTransactionOperations { + FIRFirestore *firestore = [self firestore]; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + return @"yes"; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(@"yes", result); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testCancellationOnError { + FIRFirestore *firestore = [self firestore]; + FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; + __block volatile int32_t count = 0; + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; + [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + OSAtomicIncrement32(&count); + [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}]; + return nil; + } + completion:^(id _Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(35, error.code); + [expectation fulfill]; + }]; + [self awaitExpectations]; + XCTAssertEqual(1, (int)count); + FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; + XCTAssertFalse(snapshot.exists); +} + +- (void)testUpdateFieldsWithDotsTransactionally { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"]; + + [doc.firestore + runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + XCTAssertNil(*error); + [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [transaction updateData:@{ + [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" + } + forDocument:doc]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)testUpdateNestedFieldsTransactionally { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"]; + + [doc.firestore + runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { + XCTAssertNil(*error); + [transaction setData:@{ + @"a" : @{@"b" : @"old"}, + @"c" : @{@"d" : @"old"}, + @"e" : @{@"f" : @"old"} + } + forDocument:doc]; + [transaction updateData:@{ + @"a.b" : @"new", + [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + } + forDocument:doc]; + return nil; + } + completion:^(id result, NSError *error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m new file mode 100644 index 0000000..34c0685 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m @@ -0,0 +1,111 @@ +/* + * 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 "Local/FSTEagerGarbageCollector.h" + +#import + +#import "Local/FSTReferenceSet.h" +#import "Model/FSTDocumentKey.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTEagerGarbageCollectorTests : XCTestCase +@end + +@implementation FSTEagerGarbageCollectorTests + +- (void)testAddOrRemoveReferences { + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + [gc addGarbageSource:referenceSet]; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + [referenceSet addReferenceToKey:key forID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferenceToKey:key forID:1]; + FSTAssertEqualSets([gc collectGarbage], @[ key ]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testRemoveAllReferencesForID { + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + [gc addGarbageSource:referenceSet]; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + [referenceSet addReferenceToKey:key1 forID:1]; + [referenceSet addReferenceToKey:key2 forID:1]; + [referenceSet addReferenceToKey:key3 forID:2]; + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], (@[ key1, key2 ])); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet removeReferencesForID:2]; + FSTAssertEqualSets([gc collectGarbage], @[ key3 ]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testTwoReferenceSetsAtTheSameTime { + FSTReferenceSet *remoteTargets = [[FSTReferenceSet alloc] init]; + FSTReferenceSet *localViews = [[FSTReferenceSet alloc] init]; + FSTReferenceSet *mutations = [[FSTReferenceSet alloc] init]; + + FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init]; + [gc addGarbageSource:remoteTargets]; + [gc addGarbageSource:localViews]; + [gc addGarbageSource:mutations]; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + [remoteTargets addReferenceToKey:key1 forID:1]; + [localViews addReferenceToKey:key1 forID:1]; + [mutations addReferenceToKey:key1 forID:10]; + + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + [mutations addReferenceToKey:key2 forID:10]; + + XCTAssertFalse([remoteTargets isEmpty]); + XCTAssertFalse([localViews isEmpty]); + XCTAssertFalse([mutations isEmpty]); + + [localViews removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + + [remoteTargets removeReferencesForID:1]; + FSTAssertEqualSets([gc collectGarbage], @[]); + + [mutations removeReferenceToKey:key1 forID:10]; + FSTAssertEqualSets([gc collectGarbage], @[ key1 ]); + + [mutations removeReferenceToKey:key2 forID:10]; + FSTAssertEqualSets([gc collectGarbage], @[ key2 ]); + + XCTAssertTrue([remoteTargets isEmpty]); + XCTAssertTrue([localViews isEmpty]); + XCTAssertTrue([mutations isEmpty]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm new file mode 100644 index 0000000..3374fcf --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm @@ -0,0 +1,361 @@ +/* + * 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 "Local/FSTLevelDBKey.h" + +#import + +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLevelDBKeyTests : XCTestCase +@end + +// I can't believe I have to write this... +bool StartsWith(const std::string &value, const std::string &prefix) { + return prefix.size() <= value.size() && std::equal(prefix.begin(), prefix.end(), value.begin()); +} + +static std::string RemoteDocKey(NSString *pathString) { + return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(pathString)]; +} + +static std::string RemoteDocKeyPrefix(NSString *pathString) { + return [FSTLevelDBRemoteDocumentKey keyPrefixWithResourcePath:FSTTestPath(pathString)]; +} + +static std::string DocMutationKey(NSString *userID, NSString *key, FSTBatchID batchID) { + return [FSTLevelDBDocumentMutationKey keyWithUserID:userID + documentKey:FSTTestDocKey(key) + batchID:batchID]; +} + +static std::string TargetDocKey(FSTTargetID targetID, NSString *key) { + return [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:FSTTestDocKey(key)]; +} + +static std::string DocTargetKey(NSString *key, FSTTargetID targetID) { + return [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(key) targetID:targetID]; +} + +/** + * Asserts that the description for given key is equal to the expected description. + * + * @param key A StringView of a textual key + * @param key An NSString that [FSTLevelDBKey descriptionForKey:] is expected to produce. + */ +#define FSTAssertExpectedKeyDescription(key, expectedDescription) \ + XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:(key)], (expectedDescription)) + +#define FSTAssertKeyLessThan(left, right) \ + do { \ + std::string leftKey = (left); \ + std::string rightKey = (right); \ + XCTAssertLessThan(leftKey.compare(right), 0, @"Expected %@ to be less than %@", \ + [FSTLevelDBKey descriptionForKey:leftKey], \ + [FSTLevelDBKey descriptionForKey:rightKey]); \ + } while (0) + +@implementation FSTLevelDBKeyTests + +- (void)testMutationKeyPrefixing { + auto tableKey = [FSTLevelDBMutationKey keyPrefix]; + auto emptyUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:""]; + auto fooUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:"foo"]; + + auto foo2Key = [FSTLevelDBMutationKey keyWithUserID:"foo" batchID:2]; + + XCTAssertTrue(StartsWith(emptyUserKey, tableKey)); + + // This is critical: prefixes of the a value don't convert into prefixes of the key. + XCTAssertTrue(StartsWith(fooUserKey, tableKey)); + XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey)); + + // However whole segments in common are prefixes. + XCTAssertTrue(StartsWith(foo2Key, tableKey)); + XCTAssertTrue(StartsWith(foo2Key, fooUserKey)); +} + +- (void)testMutationKeyEncodeDecodeCycle { + FSTLevelDBMutationKey *key = [[FSTLevelDBMutationKey alloc] init]; + std::string user("foo"); + + NSArray *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ]; + for (NSNumber *batchIDNumber in batchIds) { + FSTBatchID batchID = [batchIDNumber intValue]; + auto encoded = [FSTLevelDBMutationKey keyWithUserID:user batchID:batchID]; + + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.userID, user); + XCTAssertEqual(key.batchID, batchID); + } +} + +- (void)testMutationKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefix], @"[mutation: incomplete key]"); + + FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefixWithUserID:@"user1"], + @"[mutation: userID=user1 incomplete key]"); + + auto key = [FSTLevelDBMutationKey keyWithUserID:@"user1" batchID:42]; + FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42]"); + + FSTAssertExpectedKeyDescription(key + " extra", + @"[mutation: userID=user1 batchID=42 invalid " + @"key=]"); + + // Truncate the key so that it's missing its terminator. + key.resize(key.size() - 1); + FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42 incomplete key]"); +} + +- (void)testDocumentMutationKeyPrefixing { + auto tableKey = [FSTLevelDBDocumentMutationKey keyPrefix]; + auto emptyUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:""]; + auto fooUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:"foo"]; + + FSTDocumentKey *documentKey = FSTTestDocKey(@"foo/bar"); + auto foo2Key = + [FSTLevelDBDocumentMutationKey keyWithUserID:"foo" documentKey:documentKey batchID:2]; + + XCTAssertTrue(StartsWith(emptyUserKey, tableKey)); + + // While we want a key with whole segments in common be considered a prefix it's vital that + // partial segments in common not be prefixes. + XCTAssertTrue(StartsWith(fooUserKey, tableKey)); + + // Here even though "" is a prefix of "foo" that prefix is within a segment so keys derived from + // those segments cannot be prefixes of each other. + XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey)); + XCTAssertFalse(StartsWith(emptyUserKey, fooUserKey)); + + // However whole segments in common are prefixes. + XCTAssertTrue(StartsWith(foo2Key, tableKey)); + XCTAssertTrue(StartsWith(foo2Key, fooUserKey)); +} + +- (void)testDocumentMutationKeyEncodeDecodeCycle { + FSTLevelDBDocumentMutationKey *key = [[FSTLevelDBDocumentMutationKey alloc] init]; + std::string user("foo"); + + NSArray *documentKeys = @[ FSTTestDocKey(@"a/b"), FSTTestDocKey(@"a/b/c/d") ]; + + NSArray *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ]; + for (NSNumber *batchIDNumber in batchIds) { + for (FSTDocumentKey *documentKey in documentKeys) { + FSTBatchID batchID = [batchIDNumber intValue]; + auto encoded = [FSTLevelDBDocumentMutationKey keyWithUserID:user + documentKey:documentKey + batchID:batchID]; + + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.userID, user); + XCTAssertEqualObjects(key.documentKey, documentKey); + XCTAssertEqual(key.batchID, batchID); + } + } +} + +- (void)testDocumentMutationKeyOrdering { + // Different user: + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"10", @"foo/bar", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"2", @"foo/bar", 0)); + + // Different paths: + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/baz", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar2", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), + DocMutationKey(@"1", @"foo/bar/suffix/key", 0)); + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar/suffix/key", 0), + DocMutationKey(@"1", @"foo/bar2", 0)); + + // Different batchID: + FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar", 1)); +} + +- (void)testDocumentMutationKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefix], + @"[document_mutation: incomplete key]"); + + FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1"], + @"[document_mutation: userID=user1 incomplete key]"); + + auto key = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1" + resourcePath:FSTTestPath(@"foo/bar")]; + FSTAssertExpectedKeyDescription(key, + @"[document_mutation: userID=user1 key=foo/bar incomplete key]"); + + key = [FSTLevelDBDocumentMutationKey keyWithUserID:@"user1" + documentKey:FSTTestDocKey(@"foo/bar") + batchID:42]; + FSTAssertExpectedKeyDescription(key, @"[document_mutation: userID=user1 key=foo/bar batchID=42]"); +} + +- (void)testTargetGlobalKeyEncodeDecodeCycle { + FSTLevelDBTargetGlobalKey *key = [[FSTLevelDBTargetGlobalKey alloc] init]; + + auto encoded = [FSTLevelDBTargetGlobalKey key]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); +} + +- (void)testTargetGlobalKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBTargetGlobalKey key], @"[target_global:]"); +} + +- (void)testTargetKeyEncodeDecodeCycle { + FSTLevelDBTargetKey *key = [[FSTLevelDBTargetKey alloc] init]; + FSTTargetID targetID = 42; + + auto encoded = [FSTLevelDBTargetKey keyWithTargetID:42]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.targetID, targetID); +} + +- (void)testTargetKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBTargetKey keyWithTargetID:42], + @"[target: targetID=42]"); +} + +- (void)testQueryTargetKeyEncodeDecodeCycle { + FSTLevelDBQueryTargetKey *key = [[FSTLevelDBQueryTargetKey alloc] init]; + std::string canonicalID("foo"); + FSTTargetID targetID = 42; + + auto encoded = [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:42]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.canonicalID, canonicalID); + XCTAssertEqual(key.targetID, targetID); +} + +- (void)testQueryKeyDescription { + FSTAssertExpectedKeyDescription([FSTLevelDBQueryTargetKey keyWithCanonicalID:"foo" targetID:42], + @"[query_target: canonicalID=foo targetID=42]"); +} + +- (void)testTargetDocumentKeyEncodeDecodeCycle { + FSTLevelDBTargetDocumentKey *key = [[FSTLevelDBTargetDocumentKey alloc] init]; + + auto encoded = + [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqual(key.targetID, 42); + XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar")); +} + +- (void)testTargetDocumentKeyOrdering { + // Different targetID: + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(2, @"foo/bar")); + FSTAssertKeyLessThan(TargetDocKey(2, @"foo/bar"), TargetDocKey(10, @"foo/bar")); + FSTAssertKeyLessThan(TargetDocKey(10, @"foo/bar"), TargetDocKey(100, @"foo/bar")); + FSTAssertKeyLessThan(TargetDocKey(42, @"foo/bar"), TargetDocKey(100, @"foo/bar")); + + // Different paths: + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/baz")); + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar2")); + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar/suffix/key")); + FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar/suffix/key"), TargetDocKey(1, @"foo/bar2")); +} + +- (void)testTargetDocumentKeyDescription { + auto key = [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")]; + XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key], + @"[target_document: targetID=42 key=foo/bar]"); +} + +- (void)testDocumentTargetKeyEncodeDecodeCycle { + FSTLevelDBDocumentTargetKey *key = [[FSTLevelDBDocumentTargetKey alloc] init]; + + auto encoded = + [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42]; + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar")); + XCTAssertEqual(key.targetID, 42); +} + +- (void)testDocumentTargetKeyDescription { + auto key = [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42]; + XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key], + @"[document_target: key=foo/bar targetID=42]"); +} + +- (void)testDocumentTargetKeyOrdering { + // Different paths: + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/baz", 1)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar2", 1)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar/suffix/key", 1)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar/suffix/key", 1), DocTargetKey(@"foo/bar2", 1)); + + // Different targetID: + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar", 2)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 2), DocTargetKey(@"foo/bar", 10)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 10), DocTargetKey(@"foo/bar", 100)); + FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 42), DocTargetKey(@"foo/bar", 100)); +} + +- (void)testRemoteDocumentKeyPrefixing { + auto tableKey = [FSTLevelDBRemoteDocumentKey keyPrefix]; + + XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar"), tableKey)); + + // This is critical: foo/bar2 should not contain foo/bar. + XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar2"), RemoteDocKey(@"foo/bar"))); + + // Prefixes must be encoded specially + XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKey(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz"), RemoteDocKeyPrefix(@"foo/bar"))); + XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar"), RemoteDocKeyPrefix(@"foo"))); +} + +- (void)testRemoteDocumentKeyOrdering { + FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar2")); + FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar/suffix/key")); +} + +- (void)testRemoteDocumentKeyEncodeDecodeCycle { + FSTLevelDBRemoteDocumentKey *key = [[FSTLevelDBRemoteDocumentKey alloc] init]; + + NSArray *paths = @[ @"foo/bar", @"foo/bar2", @"foo/bar/baz/quux" ]; + for (NSString *path in paths) { + auto encoded = RemoteDocKey(path); + BOOL ok = [key decodeKey:encoded]; + XCTAssertTrue(ok); + XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(path)); + } +} + +- (void)testRemoteDocumentKeyDescription { + FSTAssertExpectedKeyDescription( + [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar/baz/quux")], + @"[remote_document: key=foo/bar/baz/quux]"); +} + +@end + +#undef FSTAssertExpectedKeyDescription + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m new file mode 100644 index 0000000..d426149 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m @@ -0,0 +1,45 @@ +/* + * 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 "Local/FSTLocalStore.h" + +#import + +#import "Auth/FSTUser.h" +#import "Local/FSTLevelDB.h" + +#import "FSTLocalStoreTests.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in + * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence + * implementation on demand. + */ +@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests +@end + +@implementation FSTLevelDBLocalStoreTests + +- (id)persistence { + return [FSTPersistenceTestHelpers levelDBPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm new file mode 100644 index 0000000..1e66aac --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -0,0 +1,158 @@ +/* + * 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 "Local/FSTLevelDBMutationQueue.h" + +#import +#include + +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Auth/FSTUser.h" +#import "Local/FSTLevelDB.h" +#import "Local/FSTLevelDBKey.h" +#import "Local/FSTWriteGroup.h" +#include "Port/ordered_code.h" + +#import "FSTMutationQueueTests.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +using leveldb::DB; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteOptions; +using Firestore::StringView; +using Firestore::OrderedCode; + +// A dummy mutation value, useful for testing code that's known to examine only mutation keys. +static const char *kDummy = "1"; + +/** + * Most of the tests for FSTLevelDBMutationQueue are performed on the FSTMutationQueue protocol in + * FSTMutationQueueTests. This class is responsible for setting up the @a mutationQueue plus any + * additional LevelDB-specific tests. + */ +@interface FSTLevelDBMutationQueueTests : FSTMutationQueueTests +@end + +/** + * Creates a key that's structurally the same as FSTLevelDBMutationKey except it allows for + * nonstandard table names. + */ +std::string MutationLikeKey(StringView table, StringView userID, FSTBatchID batchID) { + std::string key; + OrderedCode::WriteString(&key, table); + OrderedCode::WriteString(&key, userID); + OrderedCode::WriteSignedNumIncreasing(&key, batchID); + return key; +} + +@implementation FSTLevelDBMutationQueueTests { + FSTLevelDB *_db; +} + +- (void)setUp { + [super setUp]; + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + self.mutationQueue = [_db mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; + self.persistence = _db; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; +} + +- (void)testLoadNextBatchID_zeroWhenTotallyEmpty { + // Initial seek is invalid + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0); +} + +- (void)testLoadNextBatchID_zeroWhenNoMutations { + // Initial seek finds no mutations + [self setDummyValueForKey:MutationLikeKey("mutationr", "foo", 20)]; + [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0); +} + +- (void)testLoadNextBatchID_findsSingleRow { + // Seeks off the end of the table altogether + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]]; + + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); +} + +- (void)testLoadNextBatchID_findsSingleRowAmongNonMutations { + // Seeks into table following mutations. + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]]; + [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; + + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); +} + +- (void)testLoadNextBatchID_findsMaxAcrossUsers { + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"fo" batchID:5]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"food" batchID:3]]; + + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:2]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]]; + + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7); +} + +- (void)testLoadNextBatchID_onlyFindsMutations { + // Write higher-valued batchIDs in nearby "tables" + auto tables = @[ @"mutatio", @"mutationsa", @"bears", @"zombies" ]; + FSTBatchID highBatchID = 5; + for (NSString *table in tables) { + [self setDummyValueForKey:MutationLikeKey(table, "", highBatchID++)]; + } + + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:3]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:2]]; + [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]]; + + // None of the higher tables should match -- this is the only entry that's in the mutations + // table + XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 4); +} + +- (void)testEmptyProtoCanBeUpgraded { + // An empty protocol buffer serializes to a zero-length byte buffer. + GPBEmpty *empty = [GPBEmpty message]; + NSData *emptyData = [empty data]; + XCTAssertEqual(emptyData.length, 0); + + // Choose some other (arbitrary) proto and parse it from the empty message and it should all be + // defaults. This shows that empty proto values within the index row value don't pose any future + // liability. + NSError *error; + FSTPBMutationQueue *parsedMessage = [FSTPBMutationQueue parseFromData:emptyData error:&error]; + XCTAssertNil(error); + + FSTPBMutationQueue *defaultMessage = [FSTPBMutationQueue message]; + XCTAssertEqual(parsedMessage.lastAcknowledgedBatchId, defaultMessage.lastAcknowledgedBatchId); + XCTAssertEqualObjects(parsedMessage.lastStreamToken, defaultMessage.lastStreamToken); +} + +- (void)setDummyValueForKey:(const std::string &)key { + _db.ptr->Put(WriteOptions(), key, kDummy); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m new file mode 100644 index 0000000..8149397 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m @@ -0,0 +1,54 @@ +/* + * 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 "Local/FSTLevelDBQueryCache.h" + +#import "Local/FSTLevelDB.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTQueryCacheTests.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests +@end + +/** + * The tests for FSTLevelDBQueryCache are performed on the FSTQueryCache protocol in + * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the + * @a queryCache. + */ +@implementation FSTLevelDBQueryCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers levelDBPersistence]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; +} + +- (void)tearDown { + [self.queryCache shutdown]; + self.persistence = nil; + self.queryCache = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm new file mode 100644 index 0000000..a6af103 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm @@ -0,0 +1,78 @@ +/* + * 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 "FSTRemoteDocumentCacheTests.h" + +#include + +#import "Local/FSTLevelDB.h" +#import "Local/FSTLevelDBKey.h" +#include "Port/ordered_code.h" + +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +using leveldb::WriteOptions; +using Firestore::OrderedCode; + +// A dummy document value, useful for testing code that's known to examine only document keys. +static const char *kDummy = "1"; + +/** + * The tests for FSTLevelDBRemoteDocumentCache are performed on the FSTRemoteDocumentCache + * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and + * tearing down the @a remoteDocumentCache. + */ +@interface FSTLevelDBRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests +@end + +@implementation FSTLevelDBRemoteDocumentCacheTests { + FSTLevelDB *_db; +} + +- (void)setUp { + [super setUp]; + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + self.persistence = _db; + self.remoteDocumentCache = [self.persistence remoteDocumentCache]; + + // Write a couple dummy rows that should appear before/after the remote_documents table to make + // sure the tests are unaffected. + [self writeDummyRowWithSegments:@[ @"remote_documentr", @"foo", @"bar" ]]; + [self writeDummyRowWithSegments:@[ @"remote_documentsa", @"foo", @"bar" ]]; +} + +- (void)tearDown { + [self.remoteDocumentCache shutdown]; + self.remoteDocumentCache = nil; + self.persistence = nil; + _db = nil; + [super tearDown]; +} + +- (void)writeDummyRowWithSegments:(NSArray *)segments { + std::string key; + for (NSString *segment in segments) { + OrderedCode::WriteString(&key, segment.UTF8String); + } + + _db.ptr->Put(WriteOptions(), key, kDummy); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m new file mode 100644 index 0000000..0080f89 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m @@ -0,0 +1,181 @@ +/* + * 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 "Local/FSTLocalSerializer.h" + +#import + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Protos/objc/firestore/local/Target.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Protos/objc/google/type/Latlng.pbobjc.h" +#import "Remote/FSTSerializerBeta.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +@end + +@interface FSTLocalSerializerTests : XCTestCase + +@property(nonatomic, strong) FSTLocalSerializer *serializer; +@property(nonatomic, strong) FSTSerializerBeta *remoteSerializer; + +@end + +@implementation FSTLocalSerializerTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; + self.serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:self.remoteSerializer]; +} + +- (void)testEncodesMutationBatch { + FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 }); + FSTMutation *patch = [[FSTPatchMutation alloc] + initWithKey:[FSTDocumentKey keyWithPathString:@"bar/baz"] + fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]] + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithExists:YES]]; + FSTMutation *del = FSTTestDeleteMutation(@"baz/quux"); + FSTTimestamp *writeTime = [FSTTimestamp timestamp]; + FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 + localWriteTime:writeTime + mutations:@[ set, patch, del ]]; + + GCFSWrite *setProto = [GCFSWrite message]; + setProto.update.name = @"projects/p/databases/d/documents/foo/bar"; + [setProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + @"num" : [self.remoteSerializer encodedInteger:1] + }]; + + GCFSWrite *patchProto = [GCFSWrite message]; + patchProto.update.name = @"projects/p/databases/d/documents/bar/baz"; + [patchProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + @"num" : [self.remoteSerializer encodedInteger:1] + }]; + [patchProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]]; + patchProto.currentDocument.exists = YES; + + GCFSWrite *delProto = [GCFSWrite message]; + delProto.delete_p = @"projects/p/databases/d/documents/baz/quux"; + + GPBTimestamp *writeTimeProto = [GPBTimestamp message]; + writeTimeProto.seconds = writeTime.seconds; + writeTimeProto.nanos = writeTime.nanos; + + FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message]; + batchProto.batchId = 42; + [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]]; + batchProto.localWriteTime = writeTimeProto; + + XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto); + FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; + XCTAssertEqual(decoded.batchID, model.batchID); + XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); + XCTAssertEqualObjects(decoded.mutations, model.mutations); + XCTAssertEqualObjects([decoded keys], [model keys]); +} + +- (void)testEncodesDocumentAsMaybeDocument { + FSTDocument *doc = FSTTestDoc(@"some/path", 42, @{@"foo" : @"bar"}, NO); + + FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; + maybeDocProto.document = [GCFSDocument message]; + maybeDocProto.document.name = @"projects/p/databases/d/documents/some/path"; + [maybeDocProto.document.fields addEntriesFromDictionary:@{ + @"foo" : [self.remoteSerializer encodedString:@"bar"], + }]; + maybeDocProto.document.updateTime.seconds = 0; + maybeDocProto.document.updateTime.nanos = 42000; + + XCTAssertEqualObjects([self.serializer encodedMaybeDocument:doc], maybeDocProto); + FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; + XCTAssertEqualObjects(decoded, doc); +} + +- (void)testEncodesDeletedDocumentAsMaybeDocument { + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(@"some/path", 42); + + FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message]; + maybeDocProto.noDocument = [FSTPBNoDocument message]; + maybeDocProto.noDocument.name = @"projects/p/databases/d/documents/some/path"; + maybeDocProto.noDocument.readTime.seconds = 0; + maybeDocProto.noDocument.readTime.nanos = 42000; + + XCTAssertEqualObjects([self.serializer encodedMaybeDocument:deletedDoc], maybeDocProto); + FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto]; + XCTAssertEqualObjects(decoded, deletedDoc); +} + +- (void)testEncodesQueryData { + FSTQuery *query = FSTTestQuery(@"room"); + FSTTargetID targetID = 42; + FSTSnapshotVersion *version = FSTTestVersion(1039); + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); + + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:version + resumeToken:resumeToken]; + + // Let the RPC serializer test various permutations of query serialization. + GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query]; + + FSTPBTarget *expected = [FSTPBTarget message]; + expected.targetId = targetID; + expected.snapshotVersion.nanos = 1039000; + expected.resumeToken = [resumeToken copy]; + expected.query.parent = queryTarget.parent; + expected.query.structuredQuery = queryTarget.structuredQuery; + + XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected); + FSTQueryData *decoded = [self.serializer decodedQueryData:expected]; + XCTAssertEqualObjects(decoded, queryData); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.h b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h new file mode 100644 index 0000000..8e06d82 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h @@ -0,0 +1,38 @@ +/* + * 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 + +@class FSTLocalStore; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTLocalStore protocol. + * + * To test a specific implementation of FSTLocalStore: + * + * + Subclass FSTLocalStoreTests + * + override -persistence, creating a new instance of FSTPersistence. + */ +@interface FSTLocalStoreTests : XCTestCase + +/** Creates and returns an appropriate id implementation. */ +- (id)persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m new file mode 100644 index 0000000..ab492a7 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m @@ -0,0 +1,795 @@ +/* + * 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 "Local/FSTLocalStore.h" + +#import + +#import "Auth/FSTUser.h" +#import "Core/FSTQuery.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTLocalWriteResult.h" +#import "Local/FSTNoOpGarbageCollector.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Remote/FSTRemoteEvent.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTClasses.h" + +#import "FSTHelpers.h" +#import "FSTImmutableSortedDictionary+Testing.h" +#import "FSTImmutableSortedSet+Testing.h" +#import "FSTLocalStoreTests.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Creates a document version dictionary mapping the document in @a mutation to @a version. */ +FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, + FSTTestSnapshotVersion version) { + FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary]; + result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key]; + return result; +} + +@interface FSTLocalStoreTests () + +@property(nonatomic, strong, readwrite) id localStorePersistence; +@property(nonatomic, strong, readwrite) FSTLocalStore *localStore; + +@property(nonatomic, strong, readonly) NSMutableArray *batches; +@property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges; +@property(nonatomic, assign, readwrite) FSTTargetID lastTargetID; + +@end + +@implementation FSTLocalStoreTests + +- (void)setUp { + [super setUp]; + + if ([self isTestBaseClass]) { + return; + } + + id persistence = [self persistence]; + self.localStorePersistence = persistence; + id garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser]]; + [self.localStore start]; + + _batches = [NSMutableArray array]; + _lastChanges = nil; + _lastTargetID = 0; +} + +- (void)tearDown { + [self.localStore shutdown]; + [self.localStorePersistence shutdown]; + + [super tearDown]; +} + +- (id)persistence { + @throw FSTAbstractMethodException(); // NOLINT +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTLocalStoreTests since it is incomplete without the implementations supplied by its + * subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTLocalStoreTests class]; +} + +/** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */ +- (void)restartWithNoopGarbageCollector { + [self.localStore shutdown]; + + id garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; + self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser]]; + [self.localStore start]; +} + +- (void)writeMutation:(FSTMutation *)mutation { + [self writeMutations:@[ mutation ]]; +} + +- (void)writeMutations:(NSArray *)mutations { + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + XCTAssertNotNil(result); + [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID + localWriteTime:[FSTTimestamp timestamp] + mutations:mutations]]; + self.lastChanges = result.changes; +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)event { + self.lastChanges = [self.localStore applyRemoteEvent:event]; +} + +- (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes { + [self.localStore notifyLocalViewChanges:@[ changes ]]; +} + +- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { + FSTMutationBatch *batch = [self.batches firstObject]; + [self.batches removeObjectAtIndex:0]; + XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); + FSTSnapshotVersion *version = FSTTestVersion(documentVersion); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; + FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch + commitVersion:version + mutationResults:@[ mutationResult ] + streamToken:nil]; + self.lastChanges = [self.localStore acknowledgeBatchWithResult:result]; +} + +- (void)rejectMutation { + FSTMutationBatch *batch = [self.batches firstObject]; + [self.batches removeObjectAtIndex:0]; + self.lastChanges = [self.localStore rejectBatchID:batch.batchID]; +} + +- (void)allocateQuery:(FSTQuery *)query { + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + self.lastTargetID = queryData.targetID; +} + +- (void)collectGarbage { + [self.localStore collectGarbage]; +} + +/** Asserts that the last target ID is the given number. */ +#define FSTAssertTargetID(targetID) \ + do { \ + XCTAssertEqual(self.lastTargetID, targetID); \ + } while (0) + +/** Asserts that a the lastChanges contain the docs in the given array. */ +#define FSTAssertChanged(documents) \ + XCTAssertNotNil(self.lastChanges); \ + do { \ + FSTMaybeDocumentDictionary *actual = self.lastChanges; \ + NSArray *expected = (documents); \ + XCTAssertEqual(actual.count, expected.count); \ + NSEnumerator *enumerator = expected.objectEnumerator; \ + [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \ + BOOL * stop) { \ + XCTAssertEqualObjects(value, [enumerator nextObject]); \ + }]; \ + self.lastChanges = nil; \ + } while (0) + +/** Asserts that the given keys were removed. */ +#define FSTAssertRemoved(keyPaths) \ + XCTAssertNotNil(self.lastChanges); \ + do { \ + FSTMaybeDocumentDictionary *actual = self.lastChanges; \ + XCTAssertEqual(actual.count, keyPaths.count); \ + NSEnumerator *keyPathEnumerator = keyPaths.objectEnumerator; \ + [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \ + FSTMaybeDocument * value, BOOL * stop) { \ + FSTDocumentKey *expectedKey = \ + [FSTDocumentKey keyWithPathString:[keyPathEnumerator nextObject]]; \ + XCTAssertEqualObjects(actualKey, expectedKey); \ + XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \ + }]; \ + self.lastChanges = nil; \ + } while (0) + +/** Asserts that the given local store contains the given document. */ +#define FSTAssertContains(document) \ + do { \ + FSTMaybeDocument *expected = (document); \ + FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \ + XCTAssertEqualObjects(actual, expected); \ + } while (0) + +/** Asserts that the given local store does not contain the given document. */ +#define FSTAssertNotContains(keyPathString) \ + do { \ + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPathString]; \ + FSTMaybeDocument *actual = [self.localStore readDocument:key]; \ + XCTAssertNil(actual); \ + } while (0) + +- (void)testMutationBatchKeys { + if ([self isTestBaseClass]) return; + + FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}); + FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 + localWriteTime:[FSTTimestamp timestamp] + mutations:@[ set1, set2 ]]; + FSTDocumentKeySet *keys = [batch keys]; + XCTAssertEqual(keys.count, 2); +} + +- (void)testHandlesSetMutation { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:0]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationThenDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesAckThenRejectThenRemoteEvent { + if ([self isTestBaseClass]) return; + + // Start a query that requires acks to be held. + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + // The last seen version is zero, so this ack must be held. + [self acknowledgeMutationWithVersion:1]; + FSTAssertChanged(@[]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})]; + FSTAssertChanged(@[ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); + + [self rejectMutation]; + FSTAssertRemoved(@[ @"bar/baz" ]); + FSTAssertNotContains(@"bar/baz"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO)); + FSTAssertNotContains(@"bar/baz"); +} + +- (void)testHandlesDeletedDocumentThenSetMutationThenAck { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationThenDeletedDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesDocumentThenSetMutationThenAckThenDocument { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO)); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); +} + +- (void)testHandlesPatchWithoutPriorDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testHandlesPatchMutationThenDocumentThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO)); +} + +- (void)testHandlesPatchMutationThenAckThenDocument { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); +} + +- (void)testHandlesDeleteMutationThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDocumentThenDeleteMutationThenAck { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDeleteMutationThenDocumentThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testHandlesDocumentThenDeletedDocumentThenDocument { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO)); +} + +- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO), + @[ @1 ], @[])]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:2]; // delete mutation + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES)); + + [self acknowledgeMutationWithVersion:3]; // patch mutation + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); +} + +- (void)testHandlesSetMutationAndPatchMutationTogether { + if ([self isTestBaseClass]) return; + + [self writeMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), + FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) + ]]; + + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); +} + +- (void)testHandlesSetMutationThenPatchMutationThenReject { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})]; + [self acknowledgeMutationWithVersion:1]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + + [self rejectMutation]; + FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO) ]); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO)); +} + +- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether { + if ([self isTestBaseClass]) return; + + [self writeMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), + FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), + FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil) + ]]; + + FSTAssertChanged((@[ + FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES), + FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) + ])); + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES)); +} + +- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:2]; // delete mutation + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); + + [self acknowledgeMutationWithVersion:3]; // patch mutation + FSTAssertRemoved(@[ @"foo/bar" ]); + FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0)); +} + +- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])]; + FSTAssertRemoved(@[ @"foo/bar" ]); + + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @1 ], @[])]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCollectsGarbageAfterChangeBatch { + if ([self isTestBaseClass]) return; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @2 ], @[])]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"baz"}, NO), + @[], @[ @2 ])]; + [self collectGarbage]; + + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCollectsGarbageAfterAcknowledgedMutation { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), + @[ @1 ], @[])]; + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; + [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:3]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:4]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self acknowledgeMutationWithVersion:5]; + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testCollectsGarbageAfterRejectedMutation { + if ([self isTestBaseClass]) return; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO), + @[ @1 ], @[])]; + [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; + [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // patch mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES)); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // set mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0)); + + [self rejectMutation]; // delete mutation + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/bah"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testPinsDocumentsInTheLocalView { + if ([self isTestBaseClass]) return; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[ @2 ], @[])]; + [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); + FSTAssertContains(FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES)); + + [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[], @[ @2 ])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO), + @[ @1 ], @[])]; + [self acknowledgeMutationWithVersion:2]; + [self collectGarbage]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO)); + FSTAssertContains(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO)); + + [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])]; + [self collectGarbage]; + + FSTAssertNotContains(@"foo/bar"); + FSTAssertNotContains(@"foo/baz"); +} + +- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { + if ([self isTestBaseClass]) return; + + FSTTargetID targetID = 321; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{}, NO), + @[ @(targetID) ], @[])]; + FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{}, NO)); + + [self collectGarbage]; + FSTAssertNotContains(@"foo/bar"); +} + +- (void)testCanExecuteDocumentQueries { + if ([self isTestBaseClass]) return; + + [self.localStore locallyWriteMutations:@[ + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) + ]]; + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); +} + +- (void)testCanExecuteCollectionQueries { + if ([self isTestBaseClass]) return; + + [self.localStore locallyWriteMutations:@[ + FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), + FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) + ]]; + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], (@[ + FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES), + FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES) + ])); +} + +- (void)testCanExecuteMixedCollectionQueries { + if ([self isTestBaseClass]) return; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + + [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + XCTAssertEqualObjects([docs values], (@[ + FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + FSTTestDoc(@"foo/bonk", 0, @{@"a" : @"b"}, YES) + ])); +} + +- (void)testPersistsResumeTokens { + if ([self isTestBaseClass]) return; + + // This test only works in the absence of the FSTEagerGarbageCollector. + [self restartWithNoopGarbageCollector]; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTBoxedTargetID *targetID = @(queryData.targetID); + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); + + FSTWatchChange *watchChange = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ targetID ] + resumeToken:resumeToken]; + NSMutableDictionary *listens = + [NSMutableDictionary dictionary]; + listens[targetID] = queryData; + NSMutableDictionary *pendingResponses = + [NSMutableDictionary dictionary]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000) + listenTargets:listens + pendingTargetResponses:pendingResponses]; + [aggregator addWatchChanges:@[ watchChange ]]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + [self applyRemoteEvent:remoteEvent]; + + // Stop listening so that the query should become inactive (but persistent) + [self.localStore releaseQuery:query]; + + // Should come back with the same resume token + FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; + XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); +} + +- (void)testRemoteDocumentKeysForTarget { + if ([self isTestBaseClass]) return; + [self restartWithNoopGarbageCollector]; + + FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ], @[])]; + + [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + + FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2]; + FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + + [self restartWithNoopGarbageCollector]; + + keys = [self.localStore remoteDocumentKeysForTarget:2]; + FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m new file mode 100644 index 0000000..e7486d0 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m @@ -0,0 +1,44 @@ +/* + * 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 "Local/FSTLocalStore.h" + +#import + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTLocalStoreTests.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests + * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence + * implementation on demand. + */ +@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests +@end + +@implementation FSTMemoryLocalStoreTests + +- (id)persistence { + return [FSTPersistenceTestHelpers memoryPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m new file mode 100644 index 0000000..4d76393 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m @@ -0,0 +1,42 @@ +/* + * 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 "Local/FSTMemoryMutationQueue.h" + +#import "Auth/FSTUser.h" +#import "Local/FSTMemoryPersistence.h" + +#import "FSTMutationQueueTests.h" +#import "FSTPersistenceTestHelpers.h" + +@interface FSTMemoryMutationQueueTests : FSTMutationQueueTests +@end + +/** + * The tests for FSTMemoryMutationQueue are performed on the FSTMutationQueue protocol in + * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue. + */ +@implementation FSTMemoryMutationQueueTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.mutationQueue = + [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m new file mode 100644 index 0000000..6574647 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m @@ -0,0 +1,54 @@ +/* + * 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 "Local/FSTMemoryQueryCache.h" + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTQueryCacheTests.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryQueryCacheTests : FSTQueryCacheTests +@end + +/** + * The tests for FSTMemoryQueryCache are performed on the FSTQueryCache protocol in + * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the + * @a queryCache. + */ +@implementation FSTMemoryQueryCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; +} + +- (void)tearDown { + [self.queryCache shutdown]; + self.persistence = nil; + self.queryCache = nil; + + [super tearDown]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m new file mode 100644 index 0000000..7602134 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.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 "Local/FSTMemoryRemoteDocumentCache.h" + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTRemoteDocumentCacheTests.h" + +@interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests +@end + +/** + * The tests for FSTMemoryRemoteDocumentCache are performed on the FSTRemoteDocumentCache + * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and + * tearing down the @a remoteDocumentCache. + */ +@implementation FSTMemoryRemoteDocumentCacheTests + +- (void)setUp { + [super setUp]; + + self.persistence = [FSTPersistenceTestHelpers memoryPersistence]; + self.remoteDocumentCache = [self.persistence remoteDocumentCache]; +} + +- (void)tearDown { + [self.remoteDocumentCache shutdown]; + self.persistence = nil; + self.remoteDocumentCache = nil; + + [super tearDown]; +} + +@end diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h new file mode 100644 index 0000000..0193c36 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h @@ -0,0 +1,38 @@ +/* + * 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 + +@protocol FSTMutationQueue; +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTMutationQueue protocol. + * + * To test a specific implementation of FSTMutationQueue: + * + * + Subclass FSTMutationQueueTests + * + override -setUp, assigning to mutationQueue and persistence + * + override -tearDown, cleaning up mutationQueue and persistence + */ +@interface FSTMutationQueueTests : XCTestCase +@property(nonatomic, strong, nullable) id mutationQueue; +@property(nonatomic, strong, nullable) id persistence; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m new file mode 100644 index 0000000..42ba0b3 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m @@ -0,0 +1,511 @@ +/* + * 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 "FSTMutationQueueTests.h" + +#import "Auth/FSTUser.h" +#import "Core/FSTQuery.h" +#import "Core/FSTTimestamp.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTMutationQueue.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTWriteGroup.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTMutationQueueTests + +- (void)tearDown { + [self.mutationQueue shutdown]; + [self.persistence shutdown]; + [super tearDown]; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTMutationQueueTests since it is incomplete without the implementations supplied by its + * subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTMutationQueueTests class]; +} + +- (void)testCountBatches { + if ([self isTestBaseClass]) return; + + XCTAssertEqual(0, [self batchCount]); + XCTAssertTrue([self.mutationQueue isEmpty]); + + FSTMutationBatch *batch1 = [self addMutationBatch]; + XCTAssertEqual(1, [self batchCount]); + XCTAssertFalse([self.mutationQueue isEmpty]); + + FSTMutationBatch *batch2 = [self addMutationBatch]; + XCTAssertEqual(2, [self batchCount]); + + [self removeMutationBatches:@[ batch2 ]]; + XCTAssertEqual(1, [self batchCount]); + + [self removeMutationBatches:@[ batch1 ]]; + XCTAssertEqual(0, [self batchCount]); + XCTAssertTrue([self.mutationQueue isEmpty]); +} + +- (void)testAcknowledgeBatchID { + if ([self isTestBaseClass]) return; + + // Initial state of an empty queue + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + // Adding mutation batches should not change the highest acked batchID. + FSTMutationBatch *batch1 = [self addMutationBatch]; + FSTMutationBatch *batch2 = [self addMutationBatch]; + FSTMutationBatch *batch3 = [self addMutationBatch]; + XCTAssertGreaterThan(batch1.batchID, kFSTBatchIDUnknown); + XCTAssertGreaterThan(batch2.batchID, batch1.batchID); + XCTAssertGreaterThan(batch3.batchID, batch2.batchID); + + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + [self acknowledgeBatch:batch1]; + [self acknowledgeBatch:batch2]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch1 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch2 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + // Batch 3 never acknowledged. + [self removeMutationBatches:@[ batch3 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); +} + +- (void)testAcknowledgeThenRemove { + if ([self isTestBaseClass]) return; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:NSStringFromSelector(_cmd)]; + [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil group:group]; + [self.mutationQueue removeMutationBatches:@[ batch1 ] group:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual([self batchCount], 0); + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch1.batchID); +} + +- (void)testHighestAcknowledgedBatchIDNeverExceedsNextBatchID { + if ([self isTestBaseClass]) return; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + FSTMutationBatch *batch2 = [self addMutationBatch]; + [self acknowledgeBatch:batch1]; + [self acknowledgeBatch:batch2]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + [self removeMutationBatches:@[ batch1, batch2 ]]; + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID); + + // Restart the queue so that nextBatchID will be reset. + [self.mutationQueue shutdown]; + self.mutationQueue = + [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; + + // Verify that on restart with an empty queue, nextBatchID falls to a lower value. + XCTAssertLessThan(self.mutationQueue.nextBatchID, batch2.batchID); + + // As a result highestAcknowledgedBatchID must also reset lower. + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); + + // The mutation queue will reset the next batchID after all mutations are removed so adding + // another mutation will cause a collision. + FSTMutationBatch *newBatch = [self addMutationBatch]; + XCTAssertEqual(newBatch.batchID, batch1.batchID); + + // Restart the queue with one unacknowledged batch in it. + group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual([self.mutationQueue nextBatchID], newBatch.batchID + 1); + + // highestAcknowledgedBatchID must still be kFSTBatchIDUnknown. + XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown); +} + +- (void)testLookupMutationBatch { + if ([self isTestBaseClass]) return; + + // Searching on an empty queue should not find a non-existent batch + FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42]; + XCTAssertNil(notFound); + + NSMutableArray *batches = [self createBatches:10]; + NSArray *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + // After removing, a batch should not be found + for (NSUInteger i = 0; i < removed.count; i++) { + notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID]; + XCTAssertNil(notFound); + } + + // Remaining entries should still be found + for (FSTMutationBatch *batch in batches) { + FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID]; + XCTAssertEqual(found.batchID, batch.batchID); + } + + // Even on a nonempty queue searching should not find a non-existent batch + notFound = [self.mutationQueue lookupMutationBatch:42]; + XCTAssertNil(notFound); +} + +- (void)testNextMutationBatchAfterBatchID { + if ([self isTestBaseClass]) return; + + NSMutableArray *batches = [self createBatches:10]; + + // This is an array of successors assuming the removals below will happen: + NSArray *afters = @[ batches[3], batches[8], batches[8] ]; + NSArray *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + for (NSUInteger i = 0; i < batches.count - 1; i++) { + FSTMutationBatch *current = batches[i]; + FSTMutationBatch *next = batches[i + 1]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + XCTAssertEqual(found.batchID, next.batchID); + } + + for (NSUInteger i = 0; i < removed.count; i++) { + FSTMutationBatch *current = removed[i]; + FSTMutationBatch *next = afters[i]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID]; + XCTAssertEqual(found.batchID, next.batchID); + } + + FSTMutationBatch *first = batches[0]; + FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42]; + XCTAssertEqual(found.batchID, first.batchID); + + FSTMutationBatch *last = batches[batches.count - 1]; + FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID]; + XCTAssertNil(notFound); +} + +- (void)testAllMutationBatchesThroughBatchID { + if ([self isTestBaseClass]) return; + + NSMutableArray *batches = [self createBatches:10]; + [self makeHoles:@[ @2, @6, @7 ] inBatches:batches]; + + NSArray *found, *expected; + + found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[0].batchID - 1]; + XCTAssertEqualObjects(found, (@[])); + + for (NSUInteger i = 0; i < batches.count; i++) { + found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[i].batchID]; + expected = [batches subarrayWithRange:NSMakeRange(0, i + 1)]; + XCTAssertEqualObjects(found, expected, @"for index %lu", (unsigned long)i); + } +} + +- (void)testAllMutationBatchesAffectingDocumentKey { + if ([self isTestBaseClass]) return; + + NSArray *mutations = @[ + FSTTestSetMutation(@"fob/bar", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/bar", + @{ @"a" : @1 }), + FSTTestPatchMutation(@"foo/bar", + @{ @"b" : @1 }, nil), + FSTTestSetMutation(@"foo/bar/suffix/key", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/baz", + @{ @"a" : @1 }), + FSTTestSetMutation(@"food/bar", + @{ @"a" : @1 }) + ]; + + // Store all the mutations. + NSMutableArray *batches = [NSMutableArray array]; + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + for (FSTMutation *mutation in mutations) { + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [batches addObject:batch]; + } + [self.persistence commitGroup:group]; + + NSArray *expected = @[ batches[1], batches[2] ]; + NSArray *matches = + [self.mutationQueue allMutationBatchesAffectingDocumentKey:FSTTestDocKey(@"foo/bar")]; + + XCTAssertEqualObjects(matches, expected); +} + +- (void)testAllMutationBatchesAffectingQuery { + if ([self isTestBaseClass]) return; + + NSArray *mutations = @[ + FSTTestSetMutation(@"fob/bar", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/bar", + @{ @"a" : @1 }), + FSTTestPatchMutation(@"foo/bar", + @{ @"b" : @1 }, nil), + FSTTestSetMutation(@"foo/bar/suffix/key", + @{ @"a" : @1 }), + FSTTestSetMutation(@"foo/baz", + @{ @"a" : @1 }), + FSTTestSetMutation(@"food/bar", + @{ @"a" : @1 }) + ]; + + // Store all the mutations. + NSMutableArray *batches = [NSMutableArray array]; + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + for (FSTMutation *mutation in mutations) { + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [batches addObject:batch]; + } + [self.persistence commitGroup:group]; + + NSArray *expected = @[ batches[1], batches[2], batches[4] ]; + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo")]; + NSArray *matches = + [self.mutationQueue allMutationBatchesAffectingQuery:query]; + + XCTAssertEqualObjects(matches, expected); +} + +- (void)testRemoveMutationBatches { + if ([self isTestBaseClass]) return; + + NSMutableArray *batches = [self createBatches:10]; + FSTMutationBatch *last = batches[batches.count - 1]; + + [self removeMutationBatches:@[ batches[0] ]]; + [batches removeObjectAtIndex:0]; + XCTAssertEqual([self batchCount], 9); + + NSArray *found; + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 9); + + [self removeMutationBatches:@[ batches[0], batches[1], batches[2] ]]; + [batches removeObjectsInRange:NSMakeRange(0, 3)]; + XCTAssertEqual([self batchCount], 6); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 6); + + [self removeMutationBatches:@[ batches[batches.count - 1] ]]; + [batches removeObjectAtIndex:batches.count - 1]; + XCTAssertEqual([self batchCount], 5); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 5); + + [self removeMutationBatches:@[ batches[3] ]]; + [batches removeObjectAtIndex:3]; + XCTAssertEqual([self batchCount], 4); + + [self removeMutationBatches:@[ batches[1] ]]; + [batches removeObjectAtIndex:1]; + XCTAssertEqual([self batchCount], 3); + + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, batches); + XCTAssertEqual(found.count, 3); + XCTAssertFalse([self.mutationQueue isEmpty]); + + [self removeMutationBatches:batches]; + found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID]; + XCTAssertEqualObjects(found, @[]); + XCTAssertEqual(found.count, 0); + XCTAssertTrue([self.mutationQueue isEmpty]); +} + +- (void)testRemoveMutationBatchesEmitsGarbageEvents { + if ([self isTestBaseClass]) return; + + FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + [garbageCollector addGarbageSource:self.mutationQueue]; + + NSMutableArray *batches = [NSMutableArray array]; + [batches addObjectsFromArray:@[ + [self addMutationBatchWithKey:@"foo/bar"], + [self addMutationBatchWithKey:@"foo/ba"], + [self addMutationBatchWithKey:@"foo/bar2"], + [self addMutationBatchWithKey:@"foo/bar"], + [self addMutationBatchWithKey:@"foo/bar/suffix/baz"], + [self addMutationBatchWithKey:@"bar/baz"], + ]]; + + [self removeMutationBatches:@[ batches[0] ]]; + NSSet *garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[]); + + [self removeMutationBatches:@[ batches[1] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/ba") ]); + + [self removeMutationBatches:@[ batches[5] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"bar/baz") ]); + + [self removeMutationBatches:@[ batches[2], batches[3] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/bar2") ])); + + [batches addObject:[self addMutationBatchWithKey:@"foo/bar/suffix/baz"]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[]); + + [self removeMutationBatches:@[ batches[4], batches[6] ]]; + garbage = [garbageCollector collectGarbage]; + FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/bar/suffix/baz") ]); +} + +- (void)testStreamToken { + if ([self isTestBaseClass]) return; + + NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"initial stream token"]; + [self.mutationQueue setLastStreamToken:streamToken1 group:group]; + [self.persistence commitGroup:group]; + + FSTMutationBatch *batch1 = [self addMutationBatch]; + [self addMutationBatch]; + + XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1); + + group = [self.persistence startGroupWithAction:@"acknowledgeBatchID"]; + [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2 group:group]; + [self.persistence commitGroup:group]; + + XCTAssertEqual(self.mutationQueue.highestAcknowledgedBatchID, batch1.batchID); + XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2); +} + +/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */ +- (FSTMutationBatch *)addMutationBatch { + return [self addMutationBatchWithKey:@"foo/bar"]; +} + +/** + * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy + * mutations. + */ +- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { + FSTSetMutation *mutation = FSTTestSetMutation(key, @{ @"a" : @1 }); + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"]; + FSTMutationBatch *batch = + [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp] + mutations:@[ mutation ] + group:group]; + [self.persistence commitGroup:group]; + return batch; +} + +/** + * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different + * batchID. + */ +- (NSMutableArray *)createBatches:(int)number { + NSMutableArray *batches = [NSMutableArray array]; + + for (int i = 0; i < number; i++) { + FSTMutationBatch *batch = [self addMutationBatch]; + [batches addObject:batch]; + } + + return batches; +} + +/** + * Calls -acknowledgeBatch:streamToken:group: on the mutation queue in a new group and commits the + * the group. + */ +- (void)acknowledgeBatch:(FSTMutationBatch *)batch { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Ack batchID"]; + [self.mutationQueue acknowledgeBatch:batch streamToken:nil group:group]; + [self.persistence commitGroup:group]; +} + +/** + * Calls -removeMutationBatches:group: on the mutation queue in a new group and commits the group. + */ +- (void)removeMutationBatches:(NSArray *)batches { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Remove mutation batch"]; + [self.mutationQueue removeMutationBatches:batches group:group]; + [self.persistence commitGroup:group]; +} + +/** Returns the number of mutation batches in the mutation queue. */ +- (NSUInteger)batchCount { + return [self.mutationQueue allMutationBatches].count; +} + +/** + * Removes entries from from the given @a batches and returns them. + * + * @param holes An array of indexes in the batches array; in increasing order. Indexes are relative + * to the original state of the batches array, not any intermediate state that might occur. + * @param batches The array to mutate, removing entries from it. + * @return A new array containing all the entries that were removed from @a batches. + */ +- (NSArray *)makeHoles:(NSArray *)holes + inBatches:(NSMutableArray *)batches { + NSMutableArray *removed = [NSMutableArray array]; + for (NSUInteger i = 0; i < holes.count; i++) { + NSUInteger index = holes[i].unsignedIntegerValue - i; + FSTMutationBatch *batch = batches[index]; + [self removeMutationBatches:@[ batch ]]; + + [batches removeObjectAtIndex:index]; + [removed addObject:batch]; + } + return removed; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h new file mode 100644 index 0000000..936bacf --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h @@ -0,0 +1,40 @@ +/* + * 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 + +@class FSTLevelDB; +@class FSTMemoryPersistence; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTPersistenceTestHelpers : NSObject + +/** + * Creates and starts a new FSTLevelDB instance for testing, destroying any previous contents + * if they existed. + * + * Note that in order to avoid generating a bunch of garbage on the filesystem, the path of the + * database is reused. This prevents concurrent running of tests using this database. We may + * need to revisit this if we want to parallelize the tests. + */ ++ (FSTLevelDB *)levelDBPersistence; + +/** Creates and starts a new FSTMemoryPersistence instance for testing. */ ++ (FSTMemoryPersistence *)memoryPersistence; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m new file mode 100644 index 0000000..f3d7914 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTPersistenceTestHelpers.h" + +#import "Local/FSTLevelDB.h" +#import "Local/FSTLocalSerializer.h" +#import "Local/FSTMemoryPersistence.h" +#import "Model/FSTDatabaseID.h" +#import "Remote/FSTSerializerBeta.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTPersistenceTestHelpers + ++ (FSTLevelDB *)levelDBPersistence { + NSError *error; + NSFileManager *files = [NSFileManager defaultManager]; + + NSString *dir = + [NSTemporaryDirectory() stringByAppendingPathComponent:@"FSTPersistenceTestHelpers"]; + if ([files fileExistsAtPath:dir]) { + // Delete the directory first to ensure isolation between runs. + BOOL success = [files removeItemAtPath:dir error:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to clean up leveldb path %@: %@", dir, error]; + } + } + + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + FSTSerializerBeta *remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; + FSTLocalSerializer *serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; + FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; + BOOL success = [db start:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to create leveldb path %@: %@", dir, error]; + } + + return db; +} + ++ (FSTMemoryPersistence *)memoryPersistence { + NSError *error; + FSTMemoryPersistence *persistence = [FSTMemoryPersistence persistence]; + BOOL success = [persistence start:&error]; + if (!success) { + [NSException raise:NSInternalInconsistencyException + format:@"Failed to start memory persistence: %@", error]; + } + + return persistence; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.h b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h new file mode 100644 index 0000000..a615372 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h @@ -0,0 +1,47 @@ +/* + * 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 "Local/FSTQueryCache.h" + +#import + +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTQueryCache protocol. + * + * To test a specific implementation of FSTQueryCache: + * + * + Subclass FSTQueryCacheTests + * + override -setUp, assigning to queryCache and persistence + * + override -tearDown, cleaning up queryCache and persistence + */ +@interface FSTQueryCacheTests : XCTestCase + +/** The implementation of the query cache to test. */ +@property(nonatomic, strong, nullable) id queryCache; + +/** + * The persistence implementation to use while testing the queryCache (e.g. for committing write + * groups). + */ +@property(nonatomic, strong, nullable) id persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m new file mode 100644 index 0000000..ed409b4 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m @@ -0,0 +1,375 @@ +/* + * 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 "FSTQueryCacheTests.h" + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTQueryData.h" +#import "Local/FSTWriteGroup.h" +#import "Model/FSTDocumentKey.h" + +#import "FSTHelpers.h" +#import "FSTImmutableSortedSet+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryCacheTests { + FSTQuery *_queryRooms; +} + +- (void)setUp { + [super setUp]; + + _queryRooms = FSTTestQuery(@"rooms"); +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTQueryCacheTests class]; +} + +- (void)testReadQueryNotInCache { + if ([self isTestBaseClass]) return; + + XCTAssertNil([self.queryCache queryDataForQuery:_queryRooms]); +} + +- (void)testSetAndReadAQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:queryData]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertEqualObjects(result.query, queryData.query); + XCTAssertEqual(result.targetID, queryData.targetID); + XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken); +} + +- (void)testCanonicalIDCollision { + if ([self isTestBaseClass]) return; + + // Type information is currently lost in our canonicalID implementations so this currently an + // easy way to force colliding canonicalIDs + FSTQuery *q1 = [[FSTQuery queryWithPath:FSTTestPath(@"a")] + queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))]; + FSTQuery *q2 = [[FSTQuery queryWithPath:FSTTestPath(@"a")] + queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")]; + XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID); + + FSTQueryData *data1 = [self queryDataWithQuery:q1 targetID:1 version:1]; + [self addQueryData:data1]; + + // Using the other query should not return the query cache entry despite equal canonicalIDs. + XCTAssertNil([self.queryCache queryDataForQuery:q2]); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); + + FSTQueryData *data2 = [self queryDataWithQuery:q2 targetID:2 version:1]; + [self addQueryData:data2]; + + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); + + [self removeQueryData:data1]; + XCTAssertNil([self.queryCache queryDataForQuery:q1]); + XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2); + + [self removeQueryData:data2]; + XCTAssertNil([self.queryCache queryDataForQuery:q1]); + XCTAssertNil([self.queryCache queryDataForQuery:q2]); +} + +- (void)testSetQueryToNewValue { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:queryData1]; + + FSTQueryData *queryData2 = [self queryDataWithQuery:_queryRooms targetID:1 version:2]; + [self addQueryData:queryData2]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); + XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion); + XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); + XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion); +} + +- (void)testRemoveQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:queryData1]; + + [self removeQueryData:queryData1]; + + FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; + XCTAssertNil(result); +} + +- (void)testRemoveNonExistentQuery { + if ([self isTestBaseClass]) return; + + FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + + // no-op, but make sure it doesn't throw. + XCTAssertNoThrow([self removeQueryData:queryData]); +} + +- (void)testRemoveQueryRemovesMatchingKeysToo { + if ([self isTestBaseClass]) return; + + FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; + [self addQueryData:rooms]; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + [self addMatchingKey:key1 forTargetID:rooms.targetID]; + [self addMatchingKey:key2 forTargetID:rooms.targetID]; + + XCTAssertTrue([self.queryCache containsKey:key1]); + XCTAssertTrue([self.queryCache containsKey:key2]); + + [self removeQueryData:rooms]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); +} + +- (void)testAddOrRemoveMatchingKeys { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + + XCTAssertFalse([self.queryCache containsKey:key]); + + [self addMatchingKey:key forTargetID:1]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self addMatchingKey:key forTargetID:2]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self removeMatchingKey:key forTargetID:1]; + XCTAssertTrue([self.queryCache containsKey:key]); + + [self removeMatchingKey:key forTargetID:2]; + XCTAssertFalse([self.queryCache containsKey:key]); +} + +- (void)testRemoveMatchingKeysForTargetID { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + [self addMatchingKey:key3 forTargetID:2]; + XCTAssertTrue([self.queryCache containsKey:key1]); + XCTAssertTrue([self.queryCache containsKey:key2]); + XCTAssertTrue([self.queryCache containsKey:key3]); + + [self removeMatchingKeysForTargetID:1]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); + XCTAssertTrue([self.queryCache containsKey:key3]); + + [self removeMatchingKeysForTargetID:2]; + XCTAssertFalse([self.queryCache containsKey:key1]); + XCTAssertFalse([self.queryCache containsKey:key2]); + XCTAssertFalse([self.queryCache containsKey:key3]); +} + +- (void)testRemoveEmitsGarbageEvents { + if ([self isTestBaseClass]) return; + + FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + [garbageCollector addGarbageSource:self.queryCache]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[]); + + FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms") targetID:1 version:1]; + FSTDocumentKey *room1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + FSTDocumentKey *room2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + [self addQueryData:rooms]; + [self addMatchingKey:room1 forTargetID:rooms.targetID]; + [self addMatchingKey:room2 forTargetID:rooms.targetID]; + + FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls") targetID:2 version:1]; + FSTDocumentKey *hall1 = [FSTDocumentKey keyWithPathString:@"halls/bar"]; + FSTDocumentKey *hall2 = [FSTDocumentKey keyWithPathString:@"halls/foo"]; + [self addQueryData:halls]; + [self addMatchingKey:hall1 forTargetID:halls.targetID]; + [self addMatchingKey:hall2 forTargetID:halls.targetID]; + + FSTAssertEqualSets([garbageCollector collectGarbage], @[]); + + [self removeMatchingKey:room1 forTargetID:rooms.targetID]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[ room1 ]); + + [self removeQueryData:rooms]; + FSTAssertEqualSets([garbageCollector collectGarbage], @[ room2 ]); + + [self removeMatchingKeysForTargetID:halls.targetID]; + FSTAssertEqualSets([garbageCollector collectGarbage], (@[ hall1, hall2 ])); +} + +- (void)testMatchingKeysForTargetID { + if ([self isTestBaseClass]) return; + + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + [self addMatchingKey:key3 forTargetID:2]; + + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]); + + [self addMatchingKey:key1 forTargetID:2]; + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); + FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ])); +} + +- (void)testHighestTargetID { + if ([self isTestBaseClass]) return; + + XCTAssertEqual([self.queryCache highestTargetID], 0); + + FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") + targetID:1 + purpose:FSTQueryPurposeListen]; + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + [self addQueryData:query1]; + [self addMatchingKey:key1 forTargetID:1]; + [self addMatchingKey:key2 forTargetID:1]; + + FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") + targetID:2 + purpose:FSTQueryPurposeListen]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"halls/foo"]; + [self addQueryData:query2]; + [self addMatchingKey:key3 forTargetID:2]; + XCTAssertEqual([self.queryCache highestTargetID], 2); + + // TargetIDs never come down. + [self removeQueryData:query2]; + XCTAssertEqual([self.queryCache highestTargetID], 2); + + // A query with an empty result set still counts. + FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages") + targetID:42 + purpose:FSTQueryPurposeListen]; + [self addQueryData:query3]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + [self removeQueryData:query1]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + [self removeQueryData:query3]; + XCTAssertEqual([self.queryCache highestTargetID], 42); + + // Verify that the highestTargetID even survives restarts. + [self.queryCache shutdown]; + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqual([self.queryCache highestTargetID], 42); +} + +- (void)testLastRemoteSnapshotVersion { + if ([self isTestBaseClass]) return; + + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], + [FSTSnapshotVersion noVersion]); + + // Can set the snapshot version. + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"setLastRemoteSnapshotVersion"]; + [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42) group:group]; + [self.persistence commitGroup:group]; + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + + // Snapshot version persists restarts. + self.queryCache = [self.persistence queryCache]; + [self.queryCache start]; + XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); +} + +#pragma mark - Helpers + +/** + * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from + * the snapshot version. + */ +- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + version:(FSTTestSnapshotVersion)version { + NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version); + return [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:FSTTestVersion(version) + resumeToken:resumeToken]; +} + +/** Adds the given query data to the queryCache under test, committing immediately. */ +- (void)addQueryData:(FSTQueryData *)queryData { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addQueryData"]; + [self.queryCache addQueryData:queryData group:group]; + [self.persistence commitGroup:group]; +} + +/** Removes the given query data from the queryCache under test, committing immediately. */ +- (void)removeQueryData:(FSTQueryData *)queryData { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeQueryData"]; + [self.queryCache removeQueryData:queryData group:group]; + [self.persistence commitGroup:group]; +} + +- (void)addMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { + FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + keys = [keys setByAddingObject:key]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addMatchingKeys"]; + [self.queryCache addMatchingKeys:keys forTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +- (void)removeMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID { + FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + keys = [keys setByAddingObject:key]; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeys"]; + [self.queryCache removeMatchingKeys:keys forTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeysForTargetID"]; + [self.queryCache removeMatchingKeysForTargetID:targetID group:group]; + [self.persistence commitGroup:group]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m new file mode 100644 index 0000000..a8c783a --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m @@ -0,0 +1,84 @@ +/* + * 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 "Local/FSTReferenceSet.h" + +#import + +#import "Model/FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTReferenceSetTests : XCTestCase +@end + +@implementation FSTReferenceSetTests + +- (void)testAddOrRemoveReferences { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + XCTAssertTrue([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key]); + + [referenceSet addReferenceToKey:key forID:1]; + XCTAssertTrue([referenceSet containsKey:key]); + XCTAssertFalse([referenceSet isEmpty]); + + [referenceSet addReferenceToKey:key forID:2]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:1]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:3]; + XCTAssertTrue([referenceSet containsKey:key]); + + [referenceSet removeReferenceToKey:key forID:2]; + XCTAssertFalse([referenceSet containsKey:key]); + XCTAssertTrue([referenceSet isEmpty]); +} + +- (void)testRemoveAllReferencesForTargetID { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; + + [referenceSet addReferenceToKey:key1 forID:1]; + [referenceSet addReferenceToKey:key2 forID:1]; + [referenceSet addReferenceToKey:key3 forID:2]; + XCTAssertFalse([referenceSet isEmpty]); + XCTAssertTrue([referenceSet containsKey:key1]); + XCTAssertTrue([referenceSet containsKey:key2]); + XCTAssertTrue([referenceSet containsKey:key3]); + + [referenceSet removeReferencesForID:1]; + XCTAssertFalse([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key1]); + XCTAssertFalse([referenceSet containsKey:key2]); + XCTAssertTrue([referenceSet containsKey:key3]); + + [referenceSet removeReferencesForID:2]; + XCTAssertTrue([referenceSet isEmpty]); + XCTAssertFalse([referenceSet containsKey:key1]); + XCTAssertFalse([referenceSet containsKey:key2]); + XCTAssertFalse([referenceSet containsKey:key3]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h new file mode 100644 index 0000000..fa2a857 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.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 "Local/FSTRemoteDocumentCache.h" + +#import + +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * These are tests for any implementation of the FSTRemoteDocumentCache protocol. + * + * To test a specific implementation of FSTRemoteDocumentCache: + * + * + Subclass FSTRemoteDocumentCacheTests + * + override -setUp, assigning to remoteDocumentCache and persistence + * + override -tearDown, cleaning up remoteDocumentCache and persistence + */ +@interface FSTRemoteDocumentCacheTests : XCTestCase +@property(nonatomic, strong, nullable) id remoteDocumentCache; +@property(nonatomic, strong, nullable) id persistence; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m new file mode 100644 index 0000000..a875934 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m @@ -0,0 +1,151 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTRemoteDocumentCacheTests.h" + +#import "Core/FSTQuery.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTWriteGroup.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDocPath = @"a/b"; +static NSString *const kLongDocPath = @"a/b/c/d/e/f"; +static const int kVersion = 42; + +@implementation FSTRemoteDocumentCacheTests { + NSDictionary *_kDocData; +} + +- (void)setUp { + [super setUp]; + + // essentially a constant, but can't be a compile-time one. + _kDocData = @{ @"a" : @1, @"b" : @2 }; +} + +- (void)testReadDocumentNotInCache { + if (!self.remoteDocumentCache) return; + + XCTAssertNil([self readEntryAtPath:kDocPath]); +} + +// Helper for next two tests. +- (void)setAndReadADocumentAtPath:(NSString *)path { + FSTDocument *written = [self setTestDocumentAtPath:path]; + FSTMaybeDocument *read = [self readEntryAtPath:path]; + XCTAssertEqualObjects(read, written); +} + +- (void)testSetAndReadADocument { + if (!self.remoteDocumentCache) return; + + [self setAndReadADocumentAtPath:kDocPath]; +} + +- (void)testSetAndReadADocumentAtDeepPath { + if (!self.remoteDocumentCache) return; + + [self setAndReadADocumentAtPath:kLongDocPath]; +} + +- (void)testSetAndReadDeletedDocument { + if (!self.remoteDocumentCache) return; + + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(kDocPath, kVersion); + [self addEntry:deletedDoc]; + + XCTAssertEqualObjects([self readEntryAtPath:kDocPath], deletedDoc); +} + +- (void)testSetDocumentToNewValue { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:kDocPath]; + FSTDocument *newDoc = FSTTestDoc(kDocPath, kVersion, @{ @"data" : @2 }, NO); + [self addEntry:newDoc]; + XCTAssertEqualObjects([self readEntryAtPath:kDocPath], newDoc); +} + +- (void)testRemoveDocument { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:kDocPath]; + [self removeEntryAtPath:kDocPath]; + + XCTAssertNil([self readEntryAtPath:kDocPath]); +} + +- (void)testRemoveNonExistentDocument { + if (!self.remoteDocumentCache) return; + + // no-op, but make sure it doesn't throw. + XCTAssertNoThrow([self removeEntryAtPath:kDocPath]); +} + +// TODO(mikelehen): Write more elaborate tests once we have more elaborate implementations. +- (void)testDocumentsMatchingQuery { + if (!self.remoteDocumentCache) return; + + [self setTestDocumentAtPath:@"a/1"]; + [self setTestDocumentAtPath:@"b/1"]; + [self setTestDocumentAtPath:@"b/2"]; + [self setTestDocumentAtPath:@"c/1"]; + + FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"b")]; + FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; + NSArray *expected = + @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ]; + for (FSTDocument *doc in expected) { + XCTAssertEqualObjects([results objectForKey:doc.key], doc); + } + + // TODO(mikelehen): Perhaps guard against extra documents in the result set once our + // implementations are smarter. +} + +#pragma mark - Helpers + +- (FSTDocument *)setTestDocumentAtPath:(NSString *)path { + FSTDocument *doc = FSTTestDoc(path, kVersion, _kDocData, NO); + [self addEntry:doc]; + return doc; +} + +- (void)addEntry:(FSTMaybeDocument *)maybeDoc { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addEntry"]; + [self.remoteDocumentCache addEntry:maybeDoc group:group]; + [self.persistence commitGroup:group]; +} + +- (FSTMaybeDocument *_Nullable)readEntryAtPath:(NSString *)path { + return [self.remoteDocumentCache entryForKey:FSTTestDocKey(path)]; +} + +- (void)removeEntryAtPath:(NSString *)path { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeEntryAtPath"]; + [self.remoteDocumentCache removeEntryForKey:FSTTestDocKey(path) group:group]; + [self.persistence commitGroup:group]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m new file mode 100644 index 0000000..ebf7713 --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.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 "Local/FSTRemoteDocumentChangeBuffer.h" + +#import + +#import "Local/FSTLevelDB.h" +#import "Local/FSTRemoteDocumentCache.h" +#import "Model/FSTDocument.h" + +#import "FSTHelpers.h" +#import "FSTPersistenceTestHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteDocumentChangeBufferTests : XCTestCase +@end + +@implementation FSTRemoteDocumentChangeBufferTests { + FSTLevelDB *_db; + id _remoteDocumentCache; + FSTRemoteDocumentChangeBuffer *_remoteDocumentBuffer; + + FSTMaybeDocument *_kInitialADoc; + FSTMaybeDocument *_kInitialBDoc; +} + +- (void)setUp { + [super setUp]; + + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + _remoteDocumentCache = [_db remoteDocumentCache]; + + // Add a couple initial items to the cache. + FSTWriteGroup *group = [_db startGroupWithAction:@"Add initial docs."]; + _kInitialADoc = FSTTestDoc(@"coll/a", 42, @{@"test" : @"data"}, NO); + [_remoteDocumentCache addEntry:_kInitialADoc group:group]; + + _kInitialBDoc = + [FSTDeletedDocument documentWithKey:FSTTestDocKey(@"coll/b") version:FSTTestVersion(314)]; + [_remoteDocumentCache addEntry:_kInitialBDoc group:group]; + [_db commitGroup:group]; + + _remoteDocumentBuffer = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:_remoteDocumentCache]; +} + +- (void)tearDown { + _remoteDocumentBuffer = nil; + _remoteDocumentCache = nil; + _db = nil; + + [super tearDown]; +} + +- (void)testReadUnchangedEntry { + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], + _kInitialADoc); +} + +- (void)testAddEntryAndReadItBack { + FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); + [_remoteDocumentBuffer addEntry:newADoc]; + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); + + // B should still be unchanged. + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/b")], + _kInitialBDoc); +} + +- (void)testApplyChanges { + FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO); + [_remoteDocumentBuffer addEntry:newADoc]; + XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc); + + // Reading directly against the cache should still yield the old result. + XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], _kInitialADoc); + + FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; + [_remoteDocumentBuffer applyToWriteGroup:group]; + [_db commitGroup:group]; + + // Reading against the cache should now yield the new result. + XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], newADoc); +} + +- (void)testMethodsThrowAfterApply { + FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"]; + [_remoteDocumentBuffer applyToWriteGroup:group]; + [_db commitGroup:group]; + + XCTAssertThrows([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")]); + XCTAssertThrows([_remoteDocumentBuffer addEntry:_kInitialADoc]); + XCTAssertThrows([_remoteDocumentBuffer applyToWriteGroup:group]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm new file mode 100644 index 0000000..1cd2feb --- /dev/null +++ b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Local/FSTWriteGroup.h" + +#import +#include + +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Local/FSTLevelDB.h" +#import "Local/FSTLevelDBKey.h" + +#import "FSTPersistenceTestHelpers.h" + +using leveldb::ReadOptions; +using leveldb::Status; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWriteGroupTests : XCTestCase +@end + +@implementation FSTWriteGroupTests { + FSTLevelDB *_db; +} + +- (void)setUp { + [super setUp]; + + _db = [FSTPersistenceTestHelpers levelDBPersistence]; +} + +- (void)tearDown { + _db = nil; + + [super tearDown]; +} + +- (void)testCommit { + std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42]; + FSTPBWriteBatch *message = [FSTPBWriteBatch message]; + message.batchId = 42; + + // This is a test that shows that committing an empty group does not fail. There are no side + // effects to verify though. + FSTWriteGroup *group = [_db startGroupWithAction:@"Empty commit"]; + XCTAssertNoThrow([_db commitGroup:group]); + + group = [_db startGroupWithAction:@"Put"]; + [group setMessage:message forKey:key]; + + std::string value; + Status status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.IsNotFound()); + + [_db commitGroup:group]; + status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.ok()); + + group = [_db startGroupWithAction:@"Delete"]; + [group removeMessageForKey:key]; + status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.ok()); + + [_db commitGroup:group]; + status = _db.ptr->Get(ReadOptions(), key, &value); + XCTAssertTrue(status.IsNotFound()); +} + +- (void)testDescription { + std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42]; + FSTPBWriteBatch *message = [FSTPBWriteBatch message]; + message.batchId = 42; + + FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"Action"]; + XCTAssertEqualObjects([group description], @""); + + [group setMessage:message forKey:key]; + XCTAssertEqualObjects([group description], + @""); + + [group removeMessageForKey:key]; + XCTAssertEqualObjects([group description], + @""); +} + +- (void)testCommittingWrongGroupThrows { + // If you don't create the group through persistence, it should throw. + FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"group"]; + XCTAssertThrows([_db commitGroup:group]); +} + +- (void)testCommittingTwiceThrows { + FSTWriteGroup *group = [_db startGroupWithAction:@"group"]; + [_db commitGroup:group]; + XCTAssertThrows([_db commitGroup:group]); +} + +- (void)testNestingGroupsThrows { + [_db startGroupWithAction:@"group1"]; + XCTAssertThrows([_db startGroupWithAction:@"group2"]); +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m new file mode 100644 index 0000000..9e7299f --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m @@ -0,0 +1,45 @@ +/* + * 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 "Model/FSTDatabaseID.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDatabaseIDTests : XCTestCase +@end + +@implementation FSTDatabaseIDTests + +- (void)testConstructor { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + XCTAssertEqualObjects(databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseID.databaseID, @"d"); + XCTAssertFalse([databaseID isDefaultDatabase]); +} + +- (void)testDefaultDatabase { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID]; + XCTAssertEqualObjects(databaseID.projectID, @"p"); + XCTAssertEqualObjects(databaseID.databaseID, @"(default)"); + XCTAssertTrue([databaseID isDefaultDatabase]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m new file mode 100644 index 0000000..ba68009 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m @@ -0,0 +1,60 @@ +/* + * 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 "Model/FSTDocumentKey.h" + +#import + +#import "Model/FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentKeyTests : XCTestCase +@end + +@implementation FSTDocumentKeyTests + +- (void)testConstructor { + FSTResourcePath *path = + [FSTResourcePath pathWithSegments:@[ @"rooms", @"firestore", @"messages", @"1" ]]; + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:path]; + XCTAssertEqual(path, key.path); +} + +- (void)testComparison { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]]; + FSTDocumentKey *key3 = [FSTDocumentKey keyWithSegments:@[ @"x", @"y", @"z", @"w" ]]; + XCTAssertTrue([key1 isEqualToKey:key2]); + XCTAssertFalse([key1 isEqualToKey:key3]); + + FSTDocumentKey *empty = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentKey *a = [FSTDocumentKey keyWithSegments:@[ @"a", @"a" ]]; + FSTDocumentKey *b = [FSTDocumentKey keyWithSegments:@[ @"b", @"b" ]]; + FSTDocumentKey *ab = [FSTDocumentKey keyWithSegments:@[ @"a", @"a", @"b", @"b" ]]; + + XCTAssertEqual(NSOrderedAscending, [empty compare:a]); + XCTAssertEqual(NSOrderedAscending, [a compare:b]); + XCTAssertEqual(NSOrderedAscending, [a compare:ab]); + + XCTAssertEqual(NSOrderedDescending, [a compare:empty]); + XCTAssertEqual(NSOrderedDescending, [b compare:a]); + XCTAssertEqual(NSOrderedDescending, [ab compare:a]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.m b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m new file mode 100644 index 0000000..e473088 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m @@ -0,0 +1,142 @@ +/* + * 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 "Model/FSTDocumentSet.h" + +#import + +#import "Model/FSTDocument.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentSetTests : XCTestCase +@end + +@implementation FSTDocumentSetTests { + NSComparator _comp; + FSTDocument *_doc1; + FSTDocument *_doc2; + FSTDocument *_doc3; +} + +- (void)setUp { + [super setUp]; + + _comp = FSTTestDocComparator(@"sort"); + _doc1 = FSTTestDoc(@"docs/1", 0, @{ @"sort" : @2 }, NO); + _doc2 = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @3 }, NO); + _doc3 = FSTTestDoc(@"docs/3", 0, @{ @"sort" : @1 }, NO); +} + +- (void)testCount { + XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0); + XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3); +} + +- (void)testHasKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + + XCTAssertTrue([set containsKey:_doc1.key]); + XCTAssertTrue([set containsKey:_doc2.key]); + XCTAssertFalse([set containsKey:_doc3.key]); +} + +- (void)testDocumentForKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + + XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1); + XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2); + XCTAssertNil([set documentForKey:_doc3.key]); +} + +- (void)testFirstAndLastDocument { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[]); + XCTAssertNil([set firstDocument]); + XCTAssertNil([set lastDocument]); + + set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects([set firstDocument], _doc3); + XCTAssertEqualObjects([set lastDocument], _doc2); +} + +- (void)testKeepsDocumentsInTheRightOrder { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); +} + +- (void)testPredecessorDocumentForKey { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + XCTAssertNil([set predecessorDocumentForKey:_doc3.key]); + XCTAssertEqualObjects([set predecessorDocumentForKey:_doc1.key], _doc3); + XCTAssertEqualObjects([set predecessorDocumentForKey:_doc2.key], _doc1); +} + +- (void)testDeletes { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key]; + XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ])); + XCTAssertEqual([setWithoutDoc1 count], 2); + + // Original remains unchanged + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); + + FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key]; + XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ])); + XCTAssertEqual([setWithoutDoc3 count], 1); +} + +- (void)testUpdates { + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + + FSTDocument *doc2Prime = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @9 }, NO); + + set = [set documentSetByAddingDocument:doc2Prime]; + XCTAssertEqual([set count], 3); + XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ])); +} + +- (void)testAddsDocsWithEqualComparisonValues { + FSTDocument *doc4 = FSTTestDoc(@"docs/4", 0, @{ @"sort" : @2 }, NO); + + FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]); + XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ])); +} + +- (void)testIsEqual { + FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects(set1, set1); + XCTAssertEqualObjects(set1, set2); + XCTAssertNotEqualObjects(set1, nil); + + FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqualObjects(sortedSet1, sortedSet1); + XCTAssertEqualObjects(sortedSet1, sortedSet2); + XCTAssertNotEqualObjects(sortedSet1, nil); + + FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]); + XCTAssertNotEqualObjects(set1, shortSet); + XCTAssertNotEqualObjects(set1, sortedSet1); +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.m b/Firestore/Example/Tests/Model/FSTDocumentTests.m new file mode 100644 index 0000000..9ceb0cd --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTDocumentTests.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 "Model/FSTDocument.h" + +#import + +#import "Core/FSTSnapshotVersion.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentTests : XCTestCase +@end + +@implementation FSTDocumentTests + +- (void)testConstructor { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"messages/first"]; + FSTSnapshotVersion *version = FSTTestVersion(1); + FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); + FSTDocument *doc = + [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; + + XCTAssertEqualObjects(doc.key, [FSTDocumentKey keyWithPathString:@"messages/first"]); + XCTAssertEqualObjects(doc.version, version); + XCTAssertEqualObjects(doc.data, data); + XCTAssertEqual(doc.hasLocalMutations, NO); +} + +- (void)testExtractsFields { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"rooms/eros"]; + FSTSnapshotVersion *version = FSTTestVersion(1); + FSTObjectValue *data = FSTTestObjectValue(@{ + @"desc" : @"Discuss all the project related stuff", + @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"} + }); + FSTDocument *doc = + [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; + + XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"desc")], + [FSTStringValue stringValue:@"Discuss all the project related stuff"]); + XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"owner.title")], + [FSTStringValue stringValue:@"scallywag"]); +} + +- (void)testIsEqual { + FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"messages/first"]; + FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"messages/second"]; + FSTObjectValue *data1 = FSTTestObjectValue(@{ @"a" : @1 }); + FSTObjectValue *data2 = FSTTestObjectValue(@{ @"b" : @1 }); + FSTSnapshotVersion *version1 = FSTTestVersion(1); + + FSTDocument *doc1 = + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO]; + FSTDocument *doc2 = + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO]; + + XCTAssertEqualObjects(doc1, doc2); + XCTAssertEqualObjects( + doc1, [FSTDocument documentWithData:FSTTestObjectValue( + @{ @"a" : @1 }) + key:[FSTDocumentKey keyWithPathString:@"messages/first"] + version:version1 + hasLocalMutations:NO]); + + FSTSnapshotVersion *version2 = FSTTestVersion(2); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data2 key:key1 version:version1 hasLocalMutations:NO]); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data1 key:key2 version:version1 hasLocalMutations:NO]); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data1 key:key1 version:version2 hasLocalMutations:NO]); + XCTAssertNotEqualObjects( + doc1, [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES]); + + XCTAssertEqualObjects( + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES], + [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:5]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m new file mode 100644 index 0000000..a357e60 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.m @@ -0,0 +1,576 @@ +/* + * 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 "Model/FSTFieldValue.h" + +#import + +#import "API/FIRFirestore+Internal.h" +#import "API/FSTUserDataConverter.h" +#import "Core/FSTTimestamp.h" +#import "Firestore/FIRGeoPoint.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +/** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */ +NSArray *FSTWrapGroups(NSArray *groups) { + NSMutableArray *wrapped = [NSMutableArray array]; + for (NSArray *group in groups) { + NSMutableArray *wrappedGroup = [NSMutableArray array]; + for (id value in group) { + FSTFieldValue *wrappedValue; + // Server Timestamp values can't be parsed directly, so we have a couple predefined sentinel + // strings that can be used instead. + if ([value isEqual:@"server-timestamp-1"]) { + wrappedValue = [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0)]; + } else if ([value isEqual:@"server-timestamp-2"]) { + wrappedValue = [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0)]; + } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { + // We directly convert these here so that the databaseIDs can be different. + FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; + wrappedValue = + [FSTReferenceValue referenceValue:reference.key databaseID:reference.databaseID]; + } else { + wrappedValue = FSTTestFieldValue(value); + } + [wrappedGroup addObject:wrappedValue]; + } + [wrapped addObject:wrappedGroup]; + } + return wrapped; +} + +@interface FSTFieldValueTests : XCTestCase +@end + +@implementation FSTFieldValueTests { + NSDate *date1; + NSDate *date2; +} + +- (void)setUp { + [super setUp]; + // Create a couple date objects for use in tests. + date1 = FSTTestDate(2016, 5, 20, 10, 20, 0); + date2 = FSTTestDate(2016, 10, 21, 15, 32, 0); +} + +- (void)testWrapIntegers { + NSArray *values = @[ + @(INT_MIN), @(-1), @0, @1, @2, @(UCHAR_MAX), @(INT_MAX), // Standard integers + @(LONG_MIN), @(LONG_MAX), @(LLONG_MIN), @(LLONG_MAX) // Larger values + ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTIntegerValue class]); + XCTAssertEqualObjects([wrapped value], @([value longLongValue])); + } +} + +- (void)testWrapsDoubles { + // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal + // number: . + NSArray *values = @[ + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * -1.0), @(-1.1), @(-0x1.0p-1074), @(-0.0), @(0.0), + @(0x1.0p-1074), @(DBL_MIN), @(1.1), @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY) + ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTDoubleValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapsNilAndNSNull { + FSTNullValue *nullValue = [FSTNullValue nullValue]; + XCTAssertEqual(FSTTestFieldValue(nil), nullValue); + XCTAssertEqual(FSTTestFieldValue([NSNull null]), nullValue); + XCTAssertEqual([nullValue value], [NSNull null]); +} + +- (void)testWrapsBooleans { + NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]); + XCTAssertEqualObjects([wrapped value], value); + } + + // Unsigned chars could conceivably be handled consistently with signed chars but on arm64 these + // end up being stored as signed shorts. + FSTFieldValue *wrapped = FSTTestFieldValue([NSNumber numberWithUnsignedChar:1]); + XCTAssertEqualObjects(wrapped, [FSTIntegerValue integerValue:1]); +} + +union DoubleBits { + double d; + uint64_t bits; +}; + +- (void)testNormalizesNaNs { + // NOTE: With v1beta1 query semantics, it's no longer as important that our NaN representation + // matches the backend, since all NaNs are defined to sort as equal, but we preserve the + // normalization and this test regardless for now. + + // We use a canonical NaN bit pattern that's common for both Java and Objective-C. Specifically: + // - sign: 0 + // - exponent: 11 bits, all 1 + // - significand: 52 bits, MSB=1, rest=0 + // + // This matches the Firestore backend which uses Java's Double.doubleToLongBits which is defined + // to normalize all NaNs to this value. + union DoubleBits canonical = {.bits = 0x7ff8000000000000ULL}; + + // IEEE 754 specifies that NaN isn't equal to itself. + XCTAssertTrue(isnan(canonical.d)); + XCTAssertEqual(canonical.bits, canonical.bits); + XCTAssertNotEqual(canonical.d, canonical.d); + + // All permutations of the 51 other non-MSB significand bits are also NaNs. + union DoubleBits alternate = {.bits = 0x7fff000000000000ULL}; + XCTAssertTrue(isnan(alternate.d)); + XCTAssertNotEqual(alternate.bits, canonical.bits); + XCTAssertNotEqual(alternate.d, canonical.d); + + // Even though at the C-level assignment preserves non-canonical NaNs, NSNumber normalizes all + // NaNs to single shared instance, kCFNumberNaN. That NaN has no public definition for its value + // but it happens to match what we need. + union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:alternate.d] doubleValue]}; + XCTAssertEqual(normalized.bits, canonical.bits); + + // Ensure we get the same normalization behavior (currently implemented explicitly by checking + // for isnan() and then explicitly assigning NAN). + union DoubleBits result; + result.d = [[FSTDoubleValue doubleValue:canonical.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); + + result.d = [[FSTDoubleValue doubleValue:alternate.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); + + // A NaN that's canonical except it has the sign bit set (would be negative if signs mattered) + union DoubleBits negative = {.bits = 0xfff8000000000000ULL}; + result.d = [[FSTDoubleValue doubleValue:negative.d] internalValue]; + XCTAssertTrue(isnan(negative.d)); + XCTAssertEqual(result.bits, canonical.bits); + + // A signaling NaN with significand where MSB is 0, and some non-MSB bit is one. + union DoubleBits signaling = {.bits = 0xfff4000000000000ULL}; + XCTAssertTrue(isnan(signaling.d)); + result.d = [[FSTDoubleValue doubleValue:signaling.d] internalValue]; + XCTAssertEqual(result.bits, canonical.bits); +} + +- (void)testZeros { + // Floating point numbers have an explicit sign bit so it's possible to end up with negative + // zero as a distinct value from positive zero. + union DoubleBits zero = {.d = 0.0}; + union DoubleBits negativeZero = {.d = -0.0}; + + // IEEE 754 requires these two zeros to compare equal. + XCTAssertNotEqual(zero.bits, negativeZero.bits); + XCTAssertEqual(zero.d, negativeZero.d); + + // NSNumber preserves the negative zero value but compares equal according to IEEE 754. + union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:negativeZero.d] doubleValue]}; + XCTAssertEqual(normalized.bits, negativeZero.bits); + XCTAssertEqualObjects([NSNumber numberWithDouble:0.0], [NSNumber numberWithDouble:-0.0]); + + // FSTDoubleValue preserves positive/negative zero + union DoubleBits result; + result.d = [[[FSTDoubleValue doubleValue:zero.d] value] doubleValue]; + XCTAssertEqual(result.bits, zero.bits); + result.d = [[[FSTDoubleValue doubleValue:negativeZero.d] value] doubleValue]; + XCTAssertEqual(result.bits, negativeZero.bits); + + // ... but compares positive/negative zero as unequal, compatibly with Firestore. + XCTAssertNotEqualObjects([FSTDoubleValue doubleValue:0.0], [FSTDoubleValue doubleValue:-0.0]); +} + +- (void)testWrapStrings { + NSArray *values = @[ @"", @"abc" ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTStringValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapDates { + NSArray *values = @[ FSTTestDate(1900, 12, 1, 1, 20, 30), FSTTestDate(2017, 4, 24, 13, 20, 30) ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTTimestampValue class]); + XCTAssertEqualObjects([wrapped value], value); + + XCTAssertEqualObjects(((FSTTimestampValue *)wrapped).internalValue, + [FSTTimestamp timestampWithDate:value]); + } +} + +- (void)testWrapGeoPoints { + NSArray *values = @[ FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-20, 100) ]; + + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTGeoPointValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapBlobs { + NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ]; + for (id value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTBlobValue class]); + XCTAssertEqualObjects([wrapped value], value); + } +} + +- (void)testWrapResourceNames { + NSArray *values = @[ + FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"), + FSTTestRef(@"project", kDefaultDatabaseID, @"foo/baz") + ]; + for (FSTDocumentKeyReference *value in values) { + FSTFieldValue *wrapped = FSTTestFieldValue(value); + XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]); + XCTAssertEqualObjects([wrapped value], value.key); + XCTAssertEqualObjects(((FSTDatabaseID *)wrapped).databaseID, value.databaseID); + } +} + +- (void)testWrapsEmptyObjects { + XCTAssertEqualObjects(FSTTestFieldValue(@{}), [FSTObjectValue objectValue]); +} + +- (void)testWrapsSimpleObjects { + FSTObjectValue *actual = FSTTestObjectValue( + @{ @"a" : @"foo", + @"b" : @(1L), + @"c" : @YES, + @"d" : [NSNull null] }); + FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ + @"a" : [FSTStringValue stringValue:@"foo"], + @"b" : [FSTIntegerValue integerValue:1LL], + @"c" : [FSTBooleanValue trueValue], + @"d" : [FSTNullValue nullValue] + }]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testWrapsNestedObjects { + FSTObjectValue *actual = FSTTestObjectValue(@{ @"a" : @{@"b" : @{@"c" : @"foo"}, @"d" : @YES} }); + FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{ + @"a" : [[FSTObjectValue alloc] initWithDictionary:@{ + @"b" : + [[FSTObjectValue alloc] initWithDictionary:@{@"c" : [FSTStringValue stringValue:@"foo"]}], + @"d" : [FSTBooleanValue booleanValue:YES] + }] + }]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testExtractsFields { + FSTObjectValue *obj = FSTTestObjectValue(@{ @"foo" : @{@"a" : @YES, @"b" : @"string"} }); + FSTAssertIsKindOfClass(obj, FSTObjectValue); + + FSTAssertIsKindOfClass([obj valueForPath:FSTTestFieldPath(@"foo")], FSTObjectValue); + XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.a")], [FSTBooleanValue trueValue]); + XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.b")], + [FSTStringValue stringValue:@"string"]); + + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"foo.a.b")]); + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar")]); + XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar.a")]); +} + +- (void)testOverwritesExistingFields { + FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; + + // Should return a new object, leaving the old one unmodified. + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); +} + +- (void)testAddsNewFields { + FSTObjectValue *empty = [FSTObjectValue objectValue]; + FSTObjectValue *mod = + [empty objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(empty, mod); + XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"})); + + FSTObjectValue *old = mod; + mod = [old objectBySettingValue:FSTTestFieldValue(@1) forPath:FSTTestFieldPath(@"b")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"mod"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @"mod", @"b" : @1 })); +} + +- (void)testImplicitlyCreatesObjects { + FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"}); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"b.c.d")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"})); + XCTAssertEqualObjects(mod, FSTTestFieldValue( + @{ @"a" : @"old", + @"b" : @{@"c" : @{@"d" : @"mod"}} })); +} + +- (void)testCanOverwritePrimitivesWithObjects { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@{@"b" : @"mod"}) forPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"mod"} })); +} + +- (void)testAddsToNestedObjects { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} }); + FSTObjectValue *mod = + [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a.c")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old", @"c" : @"mod"} })); +} + +- (void)testDeletesKeys { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @1, @"b" : @2 }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @1, @"b" : @2 })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); + + FSTObjectValue *empty = [mod objectByDeletingPath:FSTTestFieldPath(@"b")]; + XCTAssertNotEqual(mod, empty); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 })); + XCTAssertEqualObjects(empty, FSTTestFieldValue(@{})); +} + +- (void)testDeletesHandleMissingKeys { + FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"b")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); + + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.d")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); + + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.b.c")]; + XCTAssertEqualObjects(old, mod); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} })); +} + +- (void)testDeletesNestedKeys { + FSTObjectValue *old = FSTTestObjectValue( + @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} }); + FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c.d")]; + XCTAssertNotEqual(old, mod); + XCTAssertEqualObjects(old, FSTTestFieldValue( + @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); + + old = mod; + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c")]; + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); + + old = mod; + mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")]; + XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} })); + XCTAssertEqualObjects(mod, FSTTestFieldValue(@{})); +} + +- (void)testArrays { + FSTArrayValue *expected = [[FSTArrayValue alloc] + initWithValueNoCopy:@[ [FSTStringValue stringValue:@"value"], [FSTBooleanValue trueValue] ]]; + + FSTArrayValue *actual = (FSTArrayValue *)FSTTestFieldValue(@[ @"value", @YES ]); + XCTAssertEqualObjects(actual, expected); +} + +- (void)testValueEquality { + NSArray *groups = @[ + @[ FSTTestFieldValue(@YES), [FSTBooleanValue booleanValue:YES] ], + @[ FSTTestFieldValue(@NO), [FSTBooleanValue booleanValue:NO] ], + @[ FSTTestFieldValue([NSNull null]), [FSTNullValue nullValue] ], + @[ FSTTestFieldValue(@(0.0 / 0.0)), FSTTestFieldValue(@(NAN)), [FSTDoubleValue nanValue] ], + // -0.0 and 0.0 compare: the same (but are not isEqual:) + @[ FSTTestFieldValue(@(-0.0)) ], @[ FSTTestFieldValue(@0.0) ], + @[ FSTTestFieldValue(@1), FSTTestFieldValue(@1LL), [FSTIntegerValue integerValue:1LL] ], + // double and unit64_t values can compare: the same (but won't be isEqual:) + @[ FSTTestFieldValue(@1.0), [FSTDoubleValue doubleValue:1.0] ], + @[ FSTTestFieldValue(@1.1), [FSTDoubleValue doubleValue:1.1] ], + @[ + FSTTestFieldValue(FSTTestData(0, 1, 2, -1)), [FSTBlobValue blobValue:FSTTestData(0, 1, 2, -1)] + ], + @[ FSTTestFieldValue(FSTTestData(0, 1, -1)) ], + @[ FSTTestFieldValue(@"string"), [FSTStringValue stringValue:@"string"] ], + @[ FSTTestFieldValue(@"strin") ], + @[ FSTTestFieldValue(@"e\u0301b") ], // latin small letter e + combining acute accent + @[ FSTTestFieldValue(@"\u00e9a") ], // latin small letter e with acute accent + @[ + FSTTestFieldValue(date1), + [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:date1]] + ], + @[ FSTTestFieldValue(date2) ], + @[ + // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue(). + [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]], + [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]] + ], + @[ [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2]] ], + @[ + FSTTestFieldValue(FSTTestGeoPoint(0, 1)), + [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)] + ], + @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ], + @[ + [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1") + databaseID:[FSTDatabaseID databaseIDWithProject:@"project" + database:kDefaultDatabaseID]], + FSTTestFieldValue(FSTTestRef(@"project", kDefaultDatabaseID, @"coll/doc1")) + ], + @[ FSTTestRef(@"project", @"(default)", @"coll/doc2") ], + @[ FSTTestFieldValue(@[ @"foo", @"bar" ]), FSTTestFieldValue(@[ @"foo", @"bar" ]) ], + @[ FSTTestFieldValue(@[ @"foo", @"bar", @"baz" ]) ], @[ FSTTestFieldValue(@[ @"foo" ]) ], + @[ + FSTTestFieldValue( + @{ @"bar" : @1, + @"foo" : @2 }), + FSTTestFieldValue( + @{ @"foo" : @2, + @"bar" : @1 }) + ], + @[ FSTTestFieldValue( + @{ @"bar" : @2, + @"foo" : @1 }) ], + @[ FSTTestFieldValue( + @{ @"bar" : @1, + @"foo" : @1 }) ], + @[ FSTTestFieldValue( + @{ @"foo" : @1 }) ] + ]; + + FSTAssertEqualityGroups(groups); +} + +- (void)testValueOrdering { + NSArray *groups = @[ + // null first + @[ [NSNull null] ], + + // booleans + @[ @NO ], @[ @YES ], + + // numbers + @[ @(0.0 / 0.0) ], @[ @(-INFINITY) ], @[ @(-DBL_MAX) ], @[ @(LLONG_MIN) ], @[ @(-1.1) ], + @[ @(-1.0), @(-1LL) ], // longs and doubles compare the same + @[ @(-DBL_MIN) ], + @[ @(-0x1.0p-1074) ], // negative smallest subnormal + @[ @(-0.0), @(0.0), @(0LL) ], // zeros all compare the same + @[ @(0x1.0p-1074) ], // positive smallest subnormal + @[ @(DBL_MIN) ], @[ @1.0, @1LL ], // longs and doubles compare the same + @[ @1.1 ], @[ @(LLONG_MAX) ], @[ @(DBL_MAX) ], @[ @(INFINITY) ], + + // timestamps + @[ date1 ], @[ date2 ], + + // server timestamps come after all concrete timestamps. + // NOTE: server timestamps can't be parsed directly, so we have special sentinel strings (see + // FSTWrapGroups()). + @[ @"server-timestamp-1" ], @[ @"server-timestamp-2" ], + + // strings + @[ @"" ], @[ @"\000\ud7ff\ue000\uffff" ], @[ @"(╯°□°)╯︵ ┻━┻" ], @[ @"a" ], @[ @"abc def" ], + @[ @"e\u0301b" ], // latin small letter e + combining acute accent + latin small letter b + @[ @"æ" ], + @[ @"\u00e9a" ], // latin small letter e with acute accent + latin small letter a + + // blobs + @[ FSTTestData(-1) ], @[ FSTTestData(0, -1) ], @[ FSTTestData(0, 1, 2, 3, 4, -1) ], + @[ FSTTestData(0, 1, 2, 4, 3, -1) ], @[ FSTTestData(255, -1) ], + + // resource names + @[ FSTTestRef(@"p1", @"d1", @"c1/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c1/doc2") ], + @[ FSTTestRef(@"p1", @"d1", @"c10/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c2/doc1") ], + @[ FSTTestRef(@"p1", @"d2", @"c1/doc1") ], @[ FSTTestRef(@"p2", @"d1", @"c1/doc1") ], + + // Geo points + @[ FSTTestGeoPoint(-90, -180) ], @[ FSTTestGeoPoint(-90, 0) ], @[ FSTTestGeoPoint(-90, 180) ], + @[ FSTTestGeoPoint(0, -180) ], @[ FSTTestGeoPoint(0, 0) ], @[ FSTTestGeoPoint(0, 180) ], + @[ FSTTestGeoPoint(1, -180) ], @[ FSTTestGeoPoint(1, 0) ], @[ FSTTestGeoPoint(1, 180) ], + @[ FSTTestGeoPoint(90, -180) ], @[ FSTTestGeoPoint(90, 0) ], @[ FSTTestGeoPoint(90, 180) ], + + // Arrays + @[ @[] ], @[ @[ @"bar" ] ], @[ @[ @"foo" ] ], @[ @[ @"foo", @1 ] ], @[ @[ @"foo", @2 ] ], + @[ @[ @"foo", @"0" ] ], + + // Objects + @[ + @{ @"bar" : @0 } + ], + @[ + @{ @"bar" : @0, + @"foo" : @1 } + ], + @[ + @{ @"foo" : @1 } + ], + @[ + @{ @"foo" : @2 } + ], + @[ @{@"foo" : @"0"} ] + ]; + + NSArray *wrapped = FSTWrapGroups(groups); + FSTAssertComparisons(wrapped); +} + +- (void)testValue { + NSDate *date = [NSDate date]; + id input = @{ @"array" : @[ @1, date ], @"obj" : @{@"date" : date, @"string" : @"hi"} }; + FSTObjectValue *value = FSTTestObjectValue(input); + id output = [value value]; + { + XCTAssertTrue([output[@"array"][1] isKindOfClass:[NSDate class]]); + NSDate *actual = output[@"array"][1]; + XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, + 0.000000001); + } + { + XCTAssertTrue([output[@"obj"][@"date"] isKindOfClass:[NSDate class]]); + NSDate *actual = output[@"obj"][@"date"]; + XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970, + 0.000000001); + } +} + +@end diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.m b/Firestore/Example/Tests/Model/FSTMutationTests.m new file mode 100644 index 0000000..5ad9f94 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTMutationTests.m @@ -0,0 +1,216 @@ +/* + * 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 "Model/FSTMutation.h" + +#import + +#import "Core/FSTTimestamp.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTPath.h" + +#import "FSTHelpers.h" + +@interface FSTMutationTests : XCTestCase +@end + +@implementation FSTMutationTests { + FSTTimestamp *_timestamp; +} + +- (void)setUp { + _timestamp = [FSTTimestamp timestamp]; +} + +- (void)testAppliesSetsToDocuments { + NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"}); + FSTMaybeDocument *setDoc = [set applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{@"bar" : @"bar-value"}; + XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testAppliesPatchesToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = + FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testDeletesValuesFromTheFieldMask { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"collection/key"]; + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]]; + FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key + fieldMask:mask + value:[FSTObjectValue objectValue] + precondition:[FSTPrecondition none]]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testPatchesPrimitiveValue { + NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = + FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + + NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); +} + +- (void)testPatchingDeletedDocumentsDoesNothing { + FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0); + FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil); + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + XCTAssertEqualObjects(patchedDoc, baseDoc); +} + +- (void)testAppliesLocalTransformsToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc localWriteTime:_timestamp]; + + // Server timestamps aren't parsed, so we manually insert it. + FSTObjectValue *expectedData = FSTTestObjectValue( + @{ @"foo" : @{@"bar" : @""}, + @"baz" : @"baz-value" }); + expectedData = + [expectedData objectBySettingValue:[FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:_timestamp] + forPath:FSTTestFieldPath(@"foo.bar")]; + + FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData + key:FSTTestDocKey(@"collection/key") + version:FSTTestVersion(0) + hasLocalMutations:YES]; + + XCTAssertEqualObjects(transformedDoc, expectedDoc); +} + +- (void)testAppliesServerAckedTransformsToDocuments { + NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + + FSTMutationResult *mutationResult = [[FSTMutationResult alloc] + initWithVersion:FSTTestVersion(1) + transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; + + FSTMaybeDocument *transformedDoc = + [transform applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + + NSDictionary *expectedData = + @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue}, + @"baz" : @"baz-value" }; + XCTAssertEqualObjects(transformedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +- (void)testDeleteDeletes { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key"); + FSTMaybeDocument *result = [mutation applyTo:baseDoc localWriteTime:_timestamp]; + XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0)); +} + +- (void)testSetWithMutationResult { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + FSTMaybeDocument *setDoc = + [set applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"foo" : @"new-bar"}; + XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +- (void)testPatchWithMutationResult { + NSDictionary *docData = @{@"foo" : @"bar"}; + FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); + + FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil); + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"foo" : @"new-bar"}; + XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); +} + +#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ + do { \ + FSTMutationResult *mutationResult = \ + [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \ + FSTMaybeDocument *actual = \ + [mutation applyTo:base localWriteTime:_timestamp mutationResult:mutationResult]; \ + XCTAssertEqualObjects(actual, expected); \ + } while (0); + +/** + * Tests the transition table documented in FSTMutation.h. + */ +- (void)testTransitions { + FSTDocument *docV0 = FSTTestDoc(@"collection/key", 0, @{}, NO); + FSTDeletedDocument *deletedV0 = FSTTestDeletedDoc(@"collection/key", 0); + + FSTDocument *docV3 = FSTTestDoc(@"collection/key", 3, @{}, NO); + FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc(@"collection/key", 3); + + FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{}); + FSTMutation *patchMutation = FSTTestPatchMutation(@"collection/key", @{}, nil); + FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key"); + + ASSERT_VERSION_TRANSITION(setMutation, docV3, docV3); + ASSERT_VERSION_TRANSITION(setMutation, deletedV3, docV0); + ASSERT_VERSION_TRANSITION(setMutation, nil, docV0); + + ASSERT_VERSION_TRANSITION(patchMutation, docV3, docV3); + ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, deletedV3); + ASSERT_VERSION_TRANSITION(patchMutation, nil, nil); + + ASSERT_VERSION_TRANSITION(deleteMutation, docV3, deletedV0); + ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, deletedV0); + ASSERT_VERSION_TRANSITION(deleteMutation, nil, deletedV0); +} + +#undef ASSERT_TRANSITION + +@end diff --git a/Firestore/Example/Tests/Model/FSTPathTests.m b/Firestore/Example/Tests/Model/FSTPathTests.m new file mode 100644 index 0000000..a5d3d80 --- /dev/null +++ b/Firestore/Example/Tests/Model/FSTPathTests.m @@ -0,0 +1,196 @@ +/* + * 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 "FSTHelpers.h" +#import "Model/FSTPath.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTFieldPathTests : XCTestCase +@end + +@implementation FSTFieldPathTests + +- (void)testConstructor { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + XCTAssertEqual(3, path.length); +} + +- (void)testIndexing { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + XCTAssertEqualObjects(@"rooms", path.firstSegment); + XCTAssertEqualObjects(@"rooms", [path segmentAtIndex:0]); + XCTAssertEqualObjects(@"rooms", path[0]); + + XCTAssertEqualObjects(@"Eros", [path segmentAtIndex:1]); + XCTAssertEqualObjects(@"Eros", path[1]); + + XCTAssertEqualObjects(@"messages", [path segmentAtIndex:2]); + XCTAssertEqualObjects(@"messages", path[2]); + XCTAssertEqualObjects(@"messages", path.lastSegment); +} + +- (void)testPathByRemovingFirstSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"Eros", @"messages" ]]; + FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"messages" ]]; + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + + XCTAssertEqualObjects(second, path.pathByRemovingFirstSegment); + XCTAssertEqualObjects(third, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment); + XCTAssertEqualObjects( + empty, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment.pathByRemovingFirstSegment); + // unmodified original + XCTAssertEqualObjects(same, path); +} + +- (void)testPathByRemovingLastSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]]; + FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros" ]]; + FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + + XCTAssertEqualObjects(second, path.pathByRemovingLastSegment); + XCTAssertEqualObjects(third, path.pathByRemovingLastSegment.pathByRemovingLastSegment); + XCTAssertEqualObjects( + empty, path.pathByRemovingLastSegment.pathByRemovingLastSegment.pathByRemovingLastSegment); + // unmodified original + XCTAssertEqualObjects(same, path); +} + +- (void)testPathByAppendingSegment { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *rooms = [FSTFieldPath pathWithSegments:@[ @"rooms" ]]; + FSTFieldPath *roomsEros = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros" ]]; + FSTFieldPath *roomsEros1 = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros", @"1" ]]; + + XCTAssertEqualObjects(roomsEros, [path pathByAppendingSegment:@"eros"]); + XCTAssertEqualObjects(roomsEros1, + [[path pathByAppendingSegment:@"eros"] pathByAppendingSegment:@"1"]); + // unmodified original + XCTAssertEqualObjects(rooms, path); + + FSTFieldPath *sub = [FSTTestFieldPath(@"rooms.eros.1") pathByRemovingFirstSegment]; + FSTFieldPath *appended = [sub pathByAppendingSegment:@"2"]; + XCTAssertEqualObjects(appended, FSTTestFieldPath(@"eros.1.2")); +} + +- (void)testPathComparison { + FSTFieldPath *path1 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *path2 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *path3 = [FSTFieldPath pathWithSegments:@[ @"x", @"y", @"z" ]]; + XCTAssertTrue([path1 isEqual:path2]); + XCTAssertFalse([path1 isEqual:path3]); + + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; + FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; + FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; + + XCTAssertEqual(NSOrderedAscending, [empty compare:a]); + XCTAssertEqual(NSOrderedAscending, [a compare:b]); + XCTAssertEqual(NSOrderedAscending, [a compare:ab]); + + XCTAssertEqual(NSOrderedDescending, [a compare:empty]); + XCTAssertEqual(NSOrderedDescending, [b compare:a]); + XCTAssertEqual(NSOrderedDescending, [ab compare:a]); +} + +- (void)testIsPrefixOfPath { + FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]]; + FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]]; + FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]]; + FSTFieldPath *abc = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]]; + FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]]; + FSTFieldPath *ba = [FSTFieldPath pathWithSegments:@[ @"b", @"a" ]]; + + XCTAssertTrue([empty isPrefixOfPath:a]); + XCTAssertTrue([empty isPrefixOfPath:ab]); + XCTAssertTrue([empty isPrefixOfPath:abc]); + XCTAssertTrue([empty isPrefixOfPath:empty]); + XCTAssertTrue([empty isPrefixOfPath:b]); + XCTAssertTrue([empty isPrefixOfPath:ba]); + + XCTAssertTrue([a isPrefixOfPath:a]); + XCTAssertTrue([a isPrefixOfPath:ab]); + XCTAssertTrue([a isPrefixOfPath:abc]); + XCTAssertFalse([a isPrefixOfPath:empty]); + XCTAssertFalse([a isPrefixOfPath:b]); + XCTAssertFalse([a isPrefixOfPath:ba]); + + XCTAssertFalse([ab isPrefixOfPath:a]); + XCTAssertTrue([ab isPrefixOfPath:ab]); + XCTAssertTrue([ab isPrefixOfPath:abc]); + XCTAssertFalse([ab isPrefixOfPath:empty]); + XCTAssertFalse([ab isPrefixOfPath:b]); + XCTAssertFalse([ab isPrefixOfPath:ba]); + + XCTAssertFalse([abc isPrefixOfPath:a]); + XCTAssertFalse([abc isPrefixOfPath:ab]); + XCTAssertTrue([abc isPrefixOfPath:abc]); + XCTAssertFalse([abc isPrefixOfPath:empty]); + XCTAssertFalse([abc isPrefixOfPath:b]); + XCTAssertFalse([abc isPrefixOfPath:ba]); +} + +- (void)testInvalidPaths { + XCTAssertThrows(FSTTestFieldPath(@"")); + XCTAssertThrows(FSTTestFieldPath(@".")); + XCTAssertThrows(FSTTestFieldPath(@".foo")); + XCTAssertThrows(FSTTestFieldPath(@"foo.")); + XCTAssertThrows(FSTTestFieldPath(@"foo..bar")); +} + +#define ASSERT_ROUND_TRIP(str, segments) \ + do { \ + FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:str]; \ + XCTAssertEqual([path length], segments); \ + NSString *canonical = [path canonicalString]; \ + XCTAssertEqualObjects(canonical, str); \ + } while (0); + +- (void)testCanonicalString { + ASSERT_ROUND_TRIP(@"foo", 1); + ASSERT_ROUND_TRIP(@"foo.bar", 2); + ASSERT_ROUND_TRIP(@"foo.bar.baz", 3); + ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1); + ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2); + ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3); +} + +#undef ASSERT_ROUND_TRIP + +- (void)testCanonicalStringOfSubstring { + FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo.bar.baz"]; + XCTAssertEqualObjects([path canonicalString], @"foo.bar.baz"); + + FSTFieldPath *pathTail = [path pathByRemovingFirstSegment]; + XCTAssertEqualObjects([pathTail canonicalString], @"bar.baz"); + + FSTFieldPath *pathHead = [path pathByRemovingLastSegment]; + XCTAssertEqualObjects([pathHead canonicalString], @"foo.bar"); + + XCTAssertEqualObjects([[pathTail pathByRemovingLastSegment] canonicalString], @"bar"); + XCTAssertEqualObjects([[pathHead pathByRemovingFirstSegment] canonicalString], @"bar"); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m new file mode 100644 index 0000000..511de72 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore/FIRFirestoreErrors.h" +#import "Remote/FSTDatastore.h" + +#import +#import + +@interface FSTDatastoreTests : XCTestCase +@end + +@implementation FSTDatastoreTests + +- (void)testIsPermanentWriteError { + // From GRPCCall -cancel + NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeCancelled + userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startNextRead + error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeResourceExhausted + userInfo:@{ + NSLocalizedDescriptionKey : + @"Client does not have enough memory to hold the server response." + }]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // From GRPCCall -startWithWriteable + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // User info doesn't matter: + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:nil]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m new file mode 100644 index 0000000..a172af7 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m @@ -0,0 +1,556 @@ +/* + * 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 "Remote/FSTRemoteEvent.h" + +#import + +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Remote/FSTExistenceFilter.h" +#import "Remote/FSTWatchChange.h" + +#import "FSTHelpers.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteEventTests : XCTestCase +@end + +@implementation FSTRemoteEventTests { + NSData *_resumeToken1; + NSMutableDictionary *_noPendingResponses; +} + +- (void)setUp { + _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; + _noPendingResponses = [NSMutableDictionary dictionary]; +} + +- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray *)targets + outstanding: + (NSDictionary *)outstanding + changes:(NSArray *)watchChanges { + NSMutableDictionary *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in targets) { + listens[targetID] = dummyQueryData; + } + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) + listenTargets:listens + pendingTargetResponses:outstanding]; + [aggregator addWatchChanges:watchChanges]; + return aggregator; +} + +- (void)testWillAccumulateDocumentAddedAndRemovedEvents { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5, @6 ] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ] + removedTargetIDs:@[ @2, @6 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 6); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); + + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + + FSTUpdateMapping *mapping4 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4); + + FSTUpdateMapping *mapping5 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]]; + XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5); + + FSTUpdateMapping *mapping6 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6); +} + +- (void)testWillIgnoreEventsForPendingTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + // We're waiting for the unwatch and watch ack + NSDictionary *pendingResponses = @{ @1 : @2 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4 ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes + // because it become active. + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); +} + +- (void)testWillIgnoreEventsForRemovedTargets { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + + // We're waiting for the unwatch ack + NSDictionary *pendingResponses = @{ @1 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + // doc1 is ignored because it was part of an inactive target + XCTAssertEqual(event.documentUpdates.count, 0); + + // Target 1 is ignored because it was removed + XCTAssertEqual(event.targetChanges.count, 0); +} + +- (void)testWillKeepResetMappingEvenWithUpdates { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + // Reset stream, ignoring doc1 + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + // Add doc2, doc3 + FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + + // Remove doc2 again, should not show up in reset mapping + FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3, change4, change5 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Only doc3 is part of the new mapping + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]]; + + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleSingleReset { + // Reset target + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + + XCTAssertEqual(event.targetChanges.count, 1); + + // Reset mapping is empty + FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); +} + +- (void)testWillHandleTargetAddAndRemovalInSameBatch { + FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1a.key + document:doc1a]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:doc1b.key + document:doc1b]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 1); + XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b); + + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + + FSTUpdateMapping *mapping2 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); +} + +- (void)testTargetCurrentChangeWillMarkTheTargetCurrent { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + FSTTargetChange *targetChange = event.targetChanges[@1]; + XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1); +} + +- (void)testTargetAddedChangeWillResetPreviousState { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ] + removedTargetIDs:@[ @2 ] + documentKey:doc1.key + document:doc1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1, @2, @3 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @2 ] + cause:nil]; + FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1 ] + cause:nil]; + FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[ @3 ] + documentKey:doc2.key + document:doc2]; + + NSDictionary *pendingResponses = @{ @1 : @2, @2 : @1 }; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @3 ] + outstanding:pendingResponses + changes:@[ change1, change2, change3, change4, change5, change6 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove + XCTAssertEqual(event.targetChanges.count, 2); + + // doc1 was before the remove, so it does not show up in the mapping + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + + // Doc1 was before the remove + FSTUpdateMapping *mapping3 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; + XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + // Current was before the remove + XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1); +} + +- (void)testNoChangeWillStillMarkTheAffectedTargets { + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); +} + +- (void)testExistenceFiltersWillReplacePreviousExistenceFilters { + FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1]; + FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2]; + FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1]; + FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2]; + // replace filter1 for target 2 + FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 0); + XCTAssertEqual(event.targetChanges.count, 0); + XCTAssertEqual(aggregator.existenceFilters.count, 2); + XCTAssertEqual(aggregator.existenceFilters[@1], filter1); + XCTAssertEqual(aggregator.existenceFilters[@2], filter2); +} + +- (void)testExistenceFilterMismatchResetsTarget { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + [event handleExistenceFilterMismatchForTargetID:@1]; + + // Mapping is reset + XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); + // Reset the resume snapshot + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); + // Target needs to be set to not current + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); + XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); +} + +- (void)testDocumentUpdate { + FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO); + FSTDeletedDocument *deletedDoc1 = + [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; + FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO); + FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO); + + FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + + FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 2); + XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1); + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + + // Update doc1 + [event addDocumentUpdate:deletedDoc1]; + [event addDocumentUpdate:doc3]; + + XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.documentUpdates.count, 3); + // doc1 is replaced + XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1); + // doc2 is untouched + XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2); + // doc3 is new + XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3); + + // Target is unchanged + XCTAssertEqual(event.targetChanges.count, 1); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); +} + +- (void)testResumeTokensHandledPerTarget { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @2 ] + resumeToken:resumeToken2]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTUpdateMapping *mapping1 = + [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); +} + +- (void)testLastResumeTokenWins { + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; + + FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + resumeToken:resumeToken2]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @2 ] + resumeToken:resumeToken3]; + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @1, @2 ] + outstanding:_noPendingResponses + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEvent]; + XCTAssertEqual(event.targetChanges.count, 2); + + FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; + XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); + XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2); + + XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); + XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); + XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m new file mode 100644 index 0000000..c4cf9df --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m @@ -0,0 +1,794 @@ +/* + * 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 "Remote/FSTSerializerBeta.h" + +#import +#import + +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Firestore/FIRFieldPath.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Firestore/FIRGeoPoint.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTMutationBatch.h" +#import "Model/FSTPath.h" +#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h" +#import "Protos/objc/firestore/local/Mutation.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h" +#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" +#import "Protos/objc/google/rpc/Status.pbobjc.h" +#import "Protos/objc/google/type/Latlng.pbobjc.h" +#import "Remote/FSTWatchChange.h" + +#import "FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta (Test) +- (GCFSValue *)encodedNull; +- (GCFSValue *)encodedBool:(BOOL)value; +- (GCFSValue *)encodedDouble:(double)value; +- (GCFSValue *)encodedInteger:(int64_t)value; +- (GCFSValue *)encodedString:(NSString *)value; +- (GCFSValue *)encodedDate:(NSDate *)value; + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask; +- (NSMutableArray *)encodedFieldTransforms: + (NSArray *)fieldTransforms; + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter; +@end + +@interface GCFSStructuredQuery_Order (Test) ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending; +@end + +@implementation GCFSStructuredQuery_Order (Test) + ++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending { + GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message]; + order.field.fieldPath = property; + order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending + : GCFSStructuredQuery_Direction_Descending; + return order; +} +@end + +@interface FSTSerializerBetaTests : XCTestCase +@property(nonatomic, strong) FSTSerializerBeta *serializer; +@end + +@implementation FSTSerializerBetaTests + +- (void)setUp { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"]; + self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID]; +} + +- (void)testEncodesNull { + FSTFieldValue *model = [FSTNullValue nullValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.nullValue = GPBNullValue_NullValue; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue]; +} + +- (void)testEncodesBool { + NSArray *examples = @[ @YES, @NO ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.booleanValue = [example boolValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_BooleanValue]; + } +} + +- (void)testEncodesIntegers { + NSArray *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.integerValue = [example longLongValue]; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_IntegerValue]; + } +} + +- (void)testEncodesDoubles { + NSArray *examples = @[ + // normal negative numbers. + @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN), + + // negative smallest subnormal, zeroes, positive smallest subnormal + @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074), + + // and the rest + @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY), + + // NaN. + @(0.0 / 0.0) + ]; + for (NSNumber *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.doubleValue = [example doubleValue]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue]; + } +} + +- (void)testEncodesStrings { + NSArray *examples = @[ + @"", + @"a", + @"abc def", + @"æ", + @"\0\ud7ff\ue000\uffff", + @"(╯°□°)╯︵ ┻━┻", + ]; + for (NSString *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.stringValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue]; + } +} + +- (void)testEncodesDates { + NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50); + dateWithNanos.nanosecond = 500000000; + + NSArray *examples = @[ + [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos], + FSTTestDate(2016, 6, 17, 10, 50, 15) + ]; + + GCFSValue *timestamp1 = [GCFSValue message]; + timestamp1.timestampValue.seconds = 1451730050; + timestamp1.timestampValue.nanos = 500000000; + + GCFSValue *timestamp2 = [GCFSValue message]; + timestamp2.timestampValue.seconds = 1466160615; + timestamp2.timestampValue.nanos = 0; + NSArray *expectedTimestamps = @[ timestamp1, timestamp2 ]; + + for (NSUInteger i = 0; i < [examples count]; i++) { + [self assertRoundTripForModel:FSTTestFieldValue(examples[i]) + proto:expectedTimestamps[i] + type:GCFSValue_ValueType_OneOfCase_TimestampValue]; + } +} + +- (void)testEncodesGeoPoints { + NSArray *examples = + @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ]; + for (FIRGeoPoint *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.geoPointValue = [GTPLatLng message]; + proto.geoPointValue.latitude = example.latitude; + proto.geoPointValue.longitude = example.longitude; + + [self assertRoundTripForModel:model + proto:proto + type:GCFSValue_ValueType_OneOfCase_GeoPointValue]; + } +} + +- (void)testEncodesBlobs { + NSArray *examples = @[ + FSTTestData(-1), + FSTTestData(0, -1), + FSTTestData(0, 1, 2, -1), + FSTTestData(255, -1), + FSTTestData(0, 1, 255, -1), + ]; + for (NSData *example in examples) { + FSTFieldValue *model = FSTTestFieldValue(example); + + GCFSValue *proto = [GCFSValue message]; + proto.bytesValue = example; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue]; + } +} + +- (void)testEncodesResourceNames { + FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"); + GCFSValue *proto = [GCFSValue message]; + proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar"; + + [self assertRoundTripForModel:FSTTestFieldValue(reference) + proto:proto + type:GCFSValue_ValueType_OneOfCase_ReferenceValue]; +} + +- (void)testEncodesArrays { + FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]); + + GCFSValue *proto = [GCFSValue message]; + [proto.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"] + ]]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue]; +} + +- (void)testEncodesEmptyMap { + FSTFieldValue *model = [FSTObjectValue objectValue]; + + GCFSValue *proto = [GCFSValue message]; + proto.mapValue = [GCFSMapValue message]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)testEncodesNestedObjects { + FSTFieldValue *model = FSTTestFieldValue(@{ + @"b" : @YES, + @"d" : @(DBL_MAX), + @"i" : @1, + @"n" : [NSNull null], + @"s" : @"foo", + @"a" : @[ @2, @"bar", @{@"b" : @NO} ], + @"o" : @{ + @"d" : @100, + @"nested" : @{@"e" : @(LLONG_MIN)}, + }, + }); + + GCFSValue *innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO]; + + GCFSValue *middleArray = [GCFSValue message]; + [middleArray.arrayValue.valuesArray addObjectsFromArray:@[ + [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject + ]]; + + innerObject = [GCFSValue message]; + innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN]; + + GCFSValue *middleObject = [GCFSValue message]; + [middleObject.mapValue.fields addEntriesFromDictionary:@{ + @"d" : [self.serializer encodedInteger:100], + @"nested" : innerObject + }]; + + GCFSValue *proto = [GCFSValue message]; + [proto.mapValue.fields addEntriesFromDictionary:@{ + @"b" : [self.serializer encodedBool:YES], + @"d" : [self.serializer encodedDouble:DBL_MAX], + @"i" : [self.serializer encodedInteger:1], + @"n" : [self.serializer encodedNull], + @"s" : [self.serializer encodedString:@"foo"], + @"a" : middleArray, + @"o" : middleObject + }]; + + [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue]; +} + +- (void)assertRoundTripForModel:(FSTFieldValue *)model + proto:(GCFSValue *)value + type:(GCFSValue_ValueType_OneOfCase)type { + GCFSValue *actualProto = [self.serializer encodedFieldValue:model]; + XCTAssertEqual(actualProto.valueTypeOneOfCase, type); + XCTAssertEqualObjects(actualProto, value); + + FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value]; + XCTAssertEqualObjects(actualModel, model); +} + +- (void)testEncodesSetMutation { + FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 }); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesPatchMutation { + FSTPatchMutation *mutation = + FSTTestPatchMutation(@"docs/1", + @{ @"a" : @"b", + @"num" : @1, + @"some.de\\\\ep.th\\ing'" : @2 }, + nil); + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesDeleteMutation { + FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1"); + GCFSWrite *proto = [GCFSWrite message]; + proto.delete_p = @"projects/p/databases/d/documents/docs/1"; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesTransformMutation { + FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]); + GCFSWrite *proto = [GCFSWrite message]; + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; + proto.transform.fieldTransformsArray = + [self.serializer encodedFieldTransforms:mutation.fieldTransforms]; + proto.currentDocument.exists = YES; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)testEncodesSetMutationWithPrecondition { + FSTSetMutation *mutation = [[FSTSetMutation alloc] + initWithKey:FSTTestDocKey(@"foo/bar") + value:FSTTestObjectValue( + @{ @"a" : @"b", + @"num" : @1 }) + precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]]; + GCFSWrite *proto = [GCFSWrite message]; + proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; + proto.currentDocument.updateTime = + [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]]; + + [self assertRoundTripForMutation:mutation proto:proto]; +} + +- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto { + GCFSWrite *actualProto = [self.serializer encodedMutation:mutation]; + XCTAssertEqualObjects(actualProto, proto); + + FSTMutation *actualMutation = [self.serializer decodedMutation:proto]; + XCTAssertEqualObjects(actualMutation, mutation); +} + +- (void)testRoundTripSpecialFieldNames { + FSTMutation *set = FSTTestSetMutation(@"collection/key", @{ + @"field" : [NSString stringWithFormat:@"field %d", 1], + @"field.dot" : @2, + @"field\\slash" : @3 + }); + GCFSWrite *encoded = [self.serializer encodedMutation:set]; + FSTMutation *decoded = [self.serializer decodedMutation:encoded]; + XCTAssertEqualObjects(set, decoded); +} + +- (void)testEncodesListenRequestLabels { + FSTQuery *query = FSTTestQuery(@"collection/key"); + FSTQueryData *queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeListen]; + + NSDictionary *result = + [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertNil(result); + + queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeLimboResolution]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); + + queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:2 + purpose:FSTQueryPurposeExistenceFilterMismatch]; + result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; + XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); +} + +- (void)testEncodesRelationFilter { + FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food"); + GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input]; + + GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter; + prop.field.fieldPath = @"item.part.top"; + prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + prop.value.stringValue = @"food"; + XCTAssertEqualObjects(actual, expected); +} + +#pragma mark - encodedQuery + +- (void)testEncodesFirstLevelKeyQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs/1")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesFirstLevelAncestorQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"messages")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"messages"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNestedAncestorQueries { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSingleFiltersAtFirstLevelCollections { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter; + filter.field.fieldPath = @"prop"; + filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan; + filter.value.integerValue = 42; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesMultipleFiltersOnDeeperCollections { + FSTQuery *q = [[[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))] + queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + + GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter; + field1.field.fieldPath = @"prop"; + field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + field1.value.integerValue = 42; + + GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter; + field2.field.fieldPath = @"author"; + field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal; + field2.value.stringValue = @"dimond"; + + GCFSStructuredQuery_CompositeFilter *composite = + expected.query.structuredQuery.where.compositeFilter; + composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + [composite.filtersArray addObject:filter1]; + [composite.filtersArray addObject:filter2]; + + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesNullFilter { + [self unaryFilterTestWithValue:[NSNull null] + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull]; +} + +- (void)testEncodesNanFilter { + [self unaryFilterTestWithValue:@(NAN) + expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan]; +} + +- (void)unaryFilterTestWithValue:(id)value + expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator) + operator{ + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + + GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter; + filter.field.fieldPath = @"prop"; + filter.op = operator; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrders { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:YES]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesSortOrdersDescending { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") + ascending:NO]]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"attachments"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]]; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesLimits { + FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] queryBySettingLimit:26]; + FSTQueryData *model = [self queryDataForQuery:q]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.query.structuredQuery.limit.value = 26; + expected.targetId = 1; + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (void)testEncodesResumeTokens { + FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs")]; + FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q + targetID:1 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:FSTTestData(1, 2, 3, -1)]; + + GCFSTarget *expected = [GCFSTarget message]; + expected.query.parent = @"projects/p/databases/d"; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = @"docs"; + [expected.query.structuredQuery.fromArray addObject:from]; + [expected.query.structuredQuery.orderByArray + addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; + expected.targetId = 1; + expected.resumeToken = FSTTestData(1, 2, 3, -1); + + [self assertRoundTripForQueryData:model proto:expected]; +} + +- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return [[FSTQueryData alloc] initWithQuery:query + targetID:1 + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto { + // Verify that the encoded FSTQueryData matches the target. + GCFSTarget *actualProto = [self.serializer encodedTarget:queryData]; + XCTAssertEqualObjects(actualProto, proto); + + // We don't have deserialization logic for full targets since they're not used for RPC + // interaction, but the query deserialization only *is* used for the local store. + FSTQuery *actualModel; + if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) { + actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query]; + } else { + actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents]; + } + XCTAssertEqualObjects(actualModel, queryData.query); +} + +- (void)testConvertsTargetChangeWithAdded { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithRemoved { + FSTWatchChange *expected = [[FSTWatchTargetChange alloc] + initWithState:FSTWatchTargetChangeStateRemoved + targetIDs:@[ @1, @4 ] + resumeToken:FSTTestData(0, 1, 2, -1) + cause:[NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodePermissionDenied + userInfo:@{ + NSLocalizedDescriptionKey : @"Error message", + }]]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove; + listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied; + listenResponse.targetChange.cause.message = @"Error message"; + listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1); + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsTargetChangeWithNoChange { + FSTWatchChange *expected = + [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1, @4 ] + resumeToken:[NSData data] + cause:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange; + [listenResponse.targetChange.targetIdsArray addValue:1]; + [listenResponse.targetChange.targetIdsArray addValue:4]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @1, @2 ] + removedTargetIDs:@[] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.targetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemovedTargetIds { + FSTWatchChange *expected = [[FSTDocumentWatchChange alloc] + initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[ @1 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentChange.document.updateTime.nanos = 5000; + GCFSValue *fooValue = [GCFSValue message]; + fooValue.stringValue = @"bar"; + [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"]; + [listenResponse.documentChange.removedTargetIdsArray addValue:1]; + [listenResponse.documentChange.targetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithDeletions { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:FSTTestDeletedDoc(@"coll/1", 5)]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1"; + listenResponse.documentDelete.readTime.nanos = 5000; + [listenResponse.documentDelete.removedTargetIdsArray addValue:1]; + [listenResponse.documentDelete.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +- (void)testConvertsDocumentChangeWithRemoves { + FSTWatchChange *expected = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1, @2 ] + documentKey:FSTTestDocKey(@"coll/1") + document:nil]; + GCFSListenResponse *listenResponse = [GCFSListenResponse message]; + listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1"; + [listenResponse.documentRemove.removedTargetIdsArray addValue:1]; + [listenResponse.documentRemove.removedTargetIdsArray addValue:2]; + FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse]; + + XCTAssertEqualObjects(actual, expected); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTStreamTests.m b/Firestore/Example/Tests/Remote/FSTStreamTests.m new file mode 100644 index 0000000..f27b200 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTStreamTests.m @@ -0,0 +1,139 @@ +/* + * 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 "Remote/FSTDatastore.h" + +#import +#import + +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Model/FSTDatabaseID.h" +#import "Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" +#import "Util/FSTDispatchQueue.h" + +/** Expose otherwise private methods for testing. */ +@interface FSTStream (Testing) + +- (void)writesFinishedWithError:(NSError *_Nullable)error; + +@end + +@interface FSTStreamTests : XCTestCase +@end + +@implementation FSTStreamTests { + dispatch_queue_t _testQueue; + FSTDatabaseInfo *_databaseInfo; + FSTDispatchQueue *_workerDispatchQueue; + id _credentials; +} + +- (void)setUp { + [super setUp]; + + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; + _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"test" + host:@"test-host" + sslEnabled:NO]; + + _testQueue = dispatch_queue_create("com.firebase.testing", DISPATCH_QUEUE_SERIAL); + _workerDispatchQueue = [FSTDispatchQueue queueWith:_testQueue]; + _credentials = [[FSTEmptyCredentialsProvider alloc] init]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testWatchStreamStop { + id delegate = OCMStrictProtocolMock(@protocol(FSTWatchStreamDelegate)); + + FSTWatchStream *stream = + OCMPartialMock([[FSTWatchStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]); + OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil); + + // Start the stream up but that's not really the interesting bit. This is complicated by the fact + // that startup involves redispatching after credentials are returned. + dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0); + OCMStub([delegate watchStreamDidOpen]).andDo(^(NSInvocation *invocation) { + dispatch_semaphore_signal(openCompleted); + }); + dispatch_async(_testQueue, ^{ + [stream start]; + }); + dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER); + OCMVerifyAll(delegate); + + // Stop must not call watchStreamDidClose because the full implementation of the delegate could + // attempt to restart the stream in the event it had pending watches. + dispatch_sync(_testQueue, ^{ + [stream stop]; + }); + OCMVerifyAll(delegate); + + // Simulate a final callback from GRPC + [stream writesFinishedWithError:nil]; + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + OCMVerifyAll(delegate); +} + +- (void)testWriteStreamStop { + id delegate = OCMStrictProtocolMock(@protocol(FSTWriteStreamDelegate)); + + FSTWriteStream *stream = + OCMPartialMock([[FSTWriteStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]); + OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil); + + // Start the stream up but that's not really the interesting bit. + dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0); + OCMStub([delegate writeStreamDidOpen]).andDo(^(NSInvocation *invocation) { + dispatch_semaphore_signal(openCompleted); + }); + dispatch_async(_testQueue, ^{ + [stream start]; + }); + dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER); + OCMVerifyAll(delegate); + + // Stop must not call writeStreamDidClose because the full implementation of this delegate could + // attempt to restart the stream in the event it had pending writes. + dispatch_sync(_testQueue, ^{ + [stream stop]; + }); + OCMVerifyAll(delegate); + + // Simulate a final callback from GRPC + [stream writesFinishedWithError:nil]; + // Drain queue + dispatch_sync(_testQueue, ^{ + }); + OCMVerifyAll(delegate); +} + +@end diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h new file mode 100644 index 0000000..f94fe05 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h @@ -0,0 +1,40 @@ +/* + * 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 + +#import "Core/FSTTypes.h" +#import "Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +/** FSTWatchTargetChange is a change to a watch target. */ +@interface FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs; + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + cause:(nullable NSError *)cause; + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + resumeToken:(nullable NSData *)resumeToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m new file mode 100644 index 0000000..cb5e479 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m @@ -0,0 +1,54 @@ +/* + * 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 "FSTWatchChange+Testing.h" + +#import "Model/FSTDocument.h" +#import "Remote/FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchTargetChange (Testing) + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:nil]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + cause:(nullable NSError *)cause { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:[NSData data] + cause:cause]; +} + ++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + resumeToken:(nullable NSData *)resumeToken { + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:resumeToken + cause:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m new file mode 100644 index 0000000..ccbd644 --- /dev/null +++ b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m @@ -0,0 +1,66 @@ +/* + * 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 "Remote/FSTWatchChange.h" + +#import + +#import "Model/FSTDocument.h" +#import "Remote/FSTExistenceFilter.h" + +#import "FSTHelpers.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWatchChangeTests : XCTestCase +@end + +@implementation FSTWatchChangeTests + +- (void)testDocumentChange { + FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO); + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] + removedTargetIDs:@[ @4, @5 ] + documentKey:doc.key + document:doc]; + XCTAssertEqual(change.updatedTargetIDs.count, 3); + XCTAssertEqual(change.removedTargetIDs.count, 2); + // Testing object identity here is fine. + XCTAssertEqual(change.document, doc); +} + +- (void)testExistenceFilterChange { + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5]; + XCTAssertEqual(change.filter.count, 7); + XCTAssertEqual(change.targetID, 5); +} + +- (void)testWatchTargetChange { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1, @2 ] + cause:nil]; + XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset); + XCTAssertEqual(change.targetIDs.count, 2); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m new file mode 100644 index 0000000..88b3f12 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m @@ -0,0 +1,43 @@ +/* + * 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 "FSTSpecTests.h" + +#import "Local/FSTLevelDB.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTSyncEngineTestDriver.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of FSTSpecTests that uses the LevelDB implementation of local storage. + * + * See the FSTSpecTests class comments for more information about how this works. + */ +@interface FSTLevelDBSpecTests : FSTSpecTests +@end + +@implementation FSTLevelDBSpecTests + +/** Overrides -[FSTSpecTests persistence] */ +- (id)persistence { + return [FSTPersistenceTestHelpers levelDBPersistence]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m new file mode 100644 index 0000000..9cf1f39 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m @@ -0,0 +1,42 @@ +/* + * 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 "FSTSpecTests.h" + +#import "Local/FSTMemoryPersistence.h" + +#import "FSTPersistenceTestHelpers.h" +#import "FSTSyncEngineTestDriver.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of FSTSpecTests that uses the memory-only implementation of local storage. + * + * @see the FSTSpecTests class comments for more information about how this works. + */ +@interface FSTMemorySpecTests : FSTSpecTests +@end + +@implementation FSTMemorySpecTests + +/** Overrides -[FSTSpecTests persistence] */ +- (id)persistence { + return [FSTPersistenceTestHelpers memoryPersistence]; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h new file mode 100644 index 0000000..4ff1220 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -0,0 +1,68 @@ +/* + * 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 + +#import "Remote/FSTDatastore.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMockDatastore : FSTDatastore + ++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue; + +#pragma mark - Watch Stream manipulation. + +/** Injects an Added WatchChange containing the given targetIDs. */ +- (void)writeWatchTargetAddedWithTargetIDs:(NSArray *)targetIDs; + +/** Injects an Added WatchChange that marks the given targetIDs current. */ +- (void)writeWatchCurrentWithTargetIDs:(NSArray *)targetIDs + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken; + +/** Injects a WatchChange as though it had come from the backend. */ +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap; + +/** Injects a stream failure as though it had come from the backend. */ +- (void)failWatchStreamWithError:(NSError *)error; + +/** Returns the set of active targets on the watch stream. */ +- (NSDictionary *)activeTargets; + +/** Helper method to expose watch stream state to verify in tests. */ +- (BOOL)isWatchStreamOpen; + +#pragma mark - Write Stream manipulation. + +/** + * Returns the next write that was "sent to the backend", failing if there are no queued sent + */ +- (NSArray *)nextSentWrite; + +/** Returns the number of writes that have been sent to the backend but not waited on yet. */ +- (int)writesSent; + +/** Injects a write ack as though it had come from the backend in response to a write. */ +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results; + +/** Injects a stream failure as though it had come from the backend. */ +- (void)failWriteWithError:(NSError *_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m new file mode 100644 index 0000000..1a1f659 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m @@ -0,0 +1,344 @@ +/* + * 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 "FSTMockDatastore.h" + +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Core/FSTDatabaseInfo.h" +#import "Core/FSTSnapshotVersion.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTMutation.h" +#import "Util/FSTAssert.h" +#import "Util/FSTLogger.h" + +#import "FSTWatchChange+Testing.h" + +@class GRPCProtoCall; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMockWatchStream + +@interface FSTMockWatchStream : FSTWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate NS_UNAVAILABLE; + +@property(nonatomic, assign) BOOL open; + +@property(nonatomic, strong, readonly) + NSMutableDictionary *activeTargets; + +@end + +@implementation FSTMockWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + delegate:(id)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[FSTWatchChange class] + delegate:delegate]; + if (self) { + FSTAssert(database, @"Database must not be nil"); + _activeTargets = [NSMutableDictionary dictionary]; + } + return self; +} + +#pragma mark - Overridden FSTWatchStream methods. + +- (void)start { + FSTAssert(!self.open, @"Trying to start already started watch stream"); + self.open = YES; + [self handleStreamOpen]; +} + +- (BOOL)isOpen { + return self.open; +} + +- (BOOL)isStarted { + return self.open; +} + +- (void)handleStreamOpen { + [self.delegate watchStreamDidOpen]; +} + +- (void)watchQuery:(FSTQueryData *)query { + FSTLog(@"watchQuery: %d: %@", query.targetID, query.query); + // Snapshot version is ignored on the wire + FSTQueryData *sentQueryData = + [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:query.resumeToken]; + self.activeTargets[@(query.targetID)] = sentQueryData; +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + FSTLog(@"unwatchTargetID: %d", targetID); + [self.activeTargets removeObjectForKey:@(targetID)]; +} + +- (void)failStreamWithError:(NSError *)error { + self.open = NO; + [self.delegate watchStreamDidClose:error]; +} + +#pragma mark - Helper methods. + +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { + if ([change isKindOfClass:[FSTWatchTargetChange class]]) { + FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; + if (targetChange.cause) { + for (NSNumber *targetID in targetChange.targetIDs) { + if (!self.activeTargets[targetID]) { + // Technically removing an unknown target is valid (e.g. it could race with a + // server-side removal), but we want to pay extra careful attention in tests + // that we only remove targets we listened too. + FSTFail(@"Removing a non-active target"); + } + [self.activeTargets removeObjectForKey:targetID]; + } + } + } + [self.delegate watchStreamDidChange:change snapshotVersion:snap]; +} + +@end + +#pragma mark - FSTMockWriteStream + +@interface FSTMockWriteStream : FSTWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate NS_UNAVAILABLE; + +@property(nonatomic, assign) BOOL open; +@property(nonatomic, strong, readonly) NSMutableArray *> *sentMutations; + +@end + +@implementation FSTMockWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + delegate:(id)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[FSTMutationResult class] + delegate:delegate]; + if (self) { + _sentMutations = [NSMutableArray array]; + } + return self; +} + +#pragma mark - Overridden FSTWriteStream methods. + +- (void)start { + FSTAssert(!self.open, @"Trying to start already started write stream"); + self.open = YES; + [self.sentMutations removeAllObjects]; + [self handleStreamOpen]; +} + +- (BOOL)isOpen { + return self.open; +} + +- (BOOL)isStarted { + return self.open; +} + +- (void)writeHandshake { + self.handshakeComplete = YES; + [self.delegate writeStreamDidCompleteHandshake]; +} + +- (void)writeMutations:(NSArray *)mutations { + [self.sentMutations addObject:mutations]; +} + +- (void)handleStreamOpen { + [self.delegate writeStreamDidOpen]; +} + +#pragma mark - Helper methods. + +/** Injects a write ack as though it had come from the backend in response to a write. */ +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; +} + +/** Injects a failed write response as though it had come from the backend. */ +- (void)failStreamWithError:(NSError *)error { + self.open = NO; + [self.delegate writeStreamDidClose:error]; +} + +/** + * Returns the next write that was "sent to the backend", failing if there are no queued sent + */ +- (NSArray *)nextSentWrite { + FSTAssert(self.sentMutations.count > 0, + @"Writes need to happen before you can call nextSentWrite."); + NSArray *result = [self.sentMutations objectAtIndex:0]; + [self.sentMutations removeObjectAtIndex:0]; + return result; +} + +/** + * Returns the number of mutations that have been sent to the backend but not retrieved via + * nextSentWrite yet. + */ +- (int)sentMutationsCount { + return (int)self.sentMutations.count; +} + +@end + +#pragma mark - FSTMockDatastore + +@interface FSTMockDatastore () +@property(nonatomic, strong, nullable) FSTMockWatchStream *watchStream; +@property(nonatomic, strong, nullable) FSTMockWriteStream *writeStream; + +/** Properties implemented in FSTDatastore that are nonpublic. */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; +@property(nonatomic, strong, readonly) id credentials; + +@end + +@implementation FSTMockDatastore + ++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"project" database:@"database"]; + FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID + persistenceKey:@"persistence" + host:@"host" + sslEnabled:NO]; + + FSTEmptyCredentialsProvider *credentials = [[FSTEmptyCredentialsProvider alloc] init]; + + return [[FSTMockDatastore alloc] initWithDatabaseInfo:databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials]; +} + +#pragma mark - Overridden FSTDatastore methods. + +- (FSTWatchStream *)createWatchStreamWithDelegate:(id)delegate { + FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); + self.watchStream = [[FSTMockWatchStream alloc] initWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentials + delegate:delegate]; + return self.watchStream; +} + +- (FSTWriteStream *)createWriteStreamWithDelegate:(id)delegate { + FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil"); + self.writeStream = [[FSTMockWriteStream alloc] initWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentials + delegate:delegate]; + return self.writeStream; +} + +- (void)authorizeAndStartRPC:(GRPCProtoCall *)rpc completion:(FSTVoidErrorBlock)completion { + FSTFail(@"FSTMockDatastore shouldn't be starting any RPCs."); +} + +#pragma mark - Method exposed for tests to call. + +- (NSArray *)nextSentWrite { + return [self.writeStream nextSentWrite]; +} + +- (int)writesSent { + return [self.writeStream sentMutationsCount]; +} + +- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results]; +} + +- (void)failWriteWithError:(NSError *_Nullable)error { + [self.writeStream failStreamWithError:error]; +} + +- (void)writeWatchTargetAddedWithTargetIDs:(NSArray *)targetIDs { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:targetIDs + cause:nil]; + [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]]; +} + +- (void)writeWatchCurrentWithTargetIDs:(NSArray *)targetIDs + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:targetIDs + resumeToken:resumeToken]; + [self writeWatchChange:change snapshotVersion:snapshotVersion]; +} + +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { + [self.watchStream writeWatchChange:change snapshotVersion:snap]; +} + +- (void)failWatchStreamWithError:(NSError *)error { + [self.watchStream failStreamWithError:error]; +} + +- (NSDictionary *)activeTargets { + return [self.watchStream.activeTargets copy]; +} + +- (BOOL)isWatchStreamOpen { + return self.watchStream.isOpen; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.h b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h new file mode 100644 index 0000000..3a3dbb2 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h @@ -0,0 +1,46 @@ +/* + * 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 +#import + +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTSpecTests run a set of portable event specifications from JSON spec files against a + * special isolated version of the Firestore client that allows precise control over when events + * are delivered. This allows us to test client behavior in a very reliable, deterministic way, + * including edge cases that would be difficult to reliably reproduce in a full integration test. + * + * Both events from user code (adding/removing listens, performing mutations) and events from the + * Datastore are simulated, while installing as much of the system in between as possible. + * + * FSTSpecTests is an abstract base class that must be subclassed to test against a specific local + * store implementation. To create a new variant of FSTSpecTests: + * + * + Subclass FSTSpecTests + * + override -persistence to create and return an appropriate id implementation. + */ +@interface FSTSpecTests : XCTestCase + +/** Creates and returns an appropriate id implementation. */ +- (id)persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m new file mode 100644 index 0000000..f681347 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m @@ -0,0 +1,642 @@ +/* + * 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 "FSTSpecTests.h" + +#import + +#import "Auth/FSTUser.h" +#import "Core/FSTEventManager.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTViewSnapshot.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Local/FSTEagerGarbageCollector.h" +#import "Local/FSTNoOpGarbageCollector.h" +#import "Local/FSTPersistence.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTPath.h" +#import "Remote/FSTExistenceFilter.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTAssert.h" +#import "Util/FSTClasses.h" +#import "Util/FSTLogger.h" + +#import "FSTHelpers.h" +#import "FSTSyncEngineTestDriver.h" +#import "FSTWatchChange+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +// Disables all other tests; useful for debugging. Multiple tests can have this tag and they'll all +// be run (but all others won't). +static NSString *const kExclusiveTag = @"exclusive"; + +// A tag for tests that should be excluded from execution (on iOS), useful to allow the platforms +// to temporarily diverge. +static NSString *const kNoIOSTag = @"no-ios"; + +@interface FSTSpecTests () +@property(nonatomic, strong) FSTSyncEngineTestDriver *driver; + +// Some config info for the currently running spec; used when restarting the driver (for doRestart). +@property(nonatomic, assign) BOOL GCEnabled; +@property(nonatomic, strong) id driverPersistence; +@end + +@implementation FSTSpecTests + +- (id)persistence { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)setUpForSpecWithConfig:(NSDictionary *)config { + // Store persistence / GCEnabled so we can re-use it in doRestart. + self.driverPersistence = [self persistence]; + NSNumber *GCEnabled = config[@"useGarbageCollection"]; + self.GCEnabled = [GCEnabled boolValue]; + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence + garbageCollector:self.garbageCollector]; + [self.driver start]; +} + +- (void)tearDownForSpec { + [self.driver shutdown]; + [self.driverPersistence shutdown]; +} + +/** + * Creates the appropriate garbage collector for the test configuration: an eager collector if + * GC is enabled or a no-op collector otherwise. + */ +- (id)garbageCollector { + return self.GCEnabled ? [[FSTEagerGarbageCollector alloc] init] + : [[FSTNoOpGarbageCollector alloc] init]; +} + +/** + * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for + * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses. + */ +- (BOOL)isTestBaseClass { + return [self class] == [FSTSpecTests class]; +} + +#pragma mark - Methods for constructing objects from specs. + +- (nullable FSTQuery *)parseQuery:(id)querySpec { + if ([querySpec isKindOfClass:[NSString class]]) { + return [FSTQuery queryWithPath:[FSTResourcePath pathWithString:querySpec]]; + } else if ([querySpec isKindOfClass:[NSDictionary class]]) { + NSDictionary *queryDict = (NSDictionary *)querySpec; + NSString *path = queryDict[@"path"]; + __block FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithString:path]]; + if (queryDict[@"limit"]) { + NSNumber *limit = queryDict[@"limit"]; + query = [query queryBySettingLimit:limit.integerValue]; + } + if (queryDict[@"filters"]) { + NSArray *filters = queryDict[@"filters"]; + [filters enumerateObjectsUsingBlock:^(NSArray *_Nonnull filter, NSUInteger idx, + BOOL *_Nonnull stop) { + query = [query queryByAddingFilter:FSTTestFilter(filter[0], filter[1], filter[2])]; + }]; + } + if (queryDict[@"orderBys"]) { + NSArray *orderBys = queryDict[@"orderBys"]; + [orderBys enumerateObjectsUsingBlock:^(NSArray *_Nonnull orderBy, NSUInteger idx, + BOOL *_Nonnull stop) { + query = [query queryByAddingSortOrder:FSTTestOrderBy(orderBy[0], orderBy[1])]; + }]; + } + return query; + } else { + XCTFail(@"Invalid query: %@", querySpec); + return nil; + } +} + +- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version { + return FSTTestVersion(version.longLongValue); +} + +- (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type { + BOOL hasMutations = NO; + for (NSUInteger i = 3; i < change.count; ++i) { + if ([change[i] isEqual:@"local"]) { + hasMutations = YES; + } + } + NSNumber *version = change[1]; + FSTDocument *doc = FSTTestDoc(change[0], version.longLongValue, change[2], hasMutations); + return [FSTDocumentViewChange changeWithDocument:doc type:type]; +} + +#pragma mark - Methods for doing the steps of the spec test. + +- (void)doListen:(NSArray *)listenSpec { + FSTQuery *query = [self parseQuery:listenSpec[1]]; + FSTTargetID actualID = [self.driver addUserListenerWithQuery:query]; + + FSTTargetID expectedID = [listenSpec[0] intValue]; + XCTAssertEqual(actualID, expectedID); +} + +- (void)doUnlisten:(NSArray *)unlistenSpec { + FSTQuery *query = [self parseQuery:unlistenSpec[1]]; + [self.driver removeUserListenerWithQuery:query]; +} + +- (void)doSet:(NSArray *)setSpec { + [self.driver writeUserMutation:FSTTestSetMutation(setSpec[0], setSpec[1])]; +} + +- (void)doPatch:(NSArray *)patchSpec { + [self.driver writeUserMutation:FSTTestPatchMutation(patchSpec[0], patchSpec[1], nil)]; +} + +- (void)doDelete:(NSString *)key { + [self.driver writeUserMutation:FSTTestDeleteMutation(key)]; +} + +- (void)doWatchAck:(NSArray *)ackedTargets snapshot:(NSNumber *)watchSnapshot { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded + targetIDs:ackedTargets + cause:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchCurrent:(NSArray *)currentSpec snapshot:(NSNumber *)watchSnapshot { + NSArray *currentTargets = currentSpec[0]; + NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:currentTargets + resumeToken:resumeToken]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot { + NSError *error = nil; + NSDictionary *cause = watchRemoveSpec[@"cause"]; + if (cause) { + int code = ((NSNumber *)cause[@"code"]).intValue; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey : @"Error from watchRemove.", + }; + error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:userInfo]; + } + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved + targetIDs:watchRemoveSpec[@"targetIds"] + cause:error]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active + // targets +} + +- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot { + if (watchEntity[@"docs"]) { + FSTAssert(!watchEntity[@"doc"], @"Exactly one of |doc| or |docs| needs to be set."); + int count = 0; + NSArray *docs = watchEntity[@"docs"]; + for (NSDictionary *doc in docs) { + count++; + bool isLast = (count == docs.count); + NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary]; + watchSpec[@"doc"] = doc; + if (watchEntity[@"targets"]) { + watchSpec[@"targets"] = watchEntity[@"targets"]; + } + if (watchEntity[@"removedTargets"]) { + watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"]; + } + NSNumber *_Nullable version = nil; + if (isLast) { + version = watchSnapshot; + } + [self doWatchEntity:watchSpec snapshot:version]; + } + } else if (watchEntity[@"doc"]) { + NSArray *docSpec = watchEntity[@"doc"]; + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:docSpec[0]]; + FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); + FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; + FSTMaybeDocument *doc = + [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + FSTWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] + removedTargetIDs:watchEntity[@"removedTargets"] + documentKey:doc.key + document:doc]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + } else if (watchEntity[@"key"]) { + FSTDocumentKey *docKey = [FSTDocumentKey keyWithPathString:watchEntity[@"key"]]; + FSTWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:watchEntity[@"removedTargets"] + documentKey:docKey + document:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; + } else { + FSTFail(@"Either key, doc or docs must be set."); + } +} + +- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot { + NSArray *targets = watchFilter[0]; + FSTAssert(targets.count == 1, @"ExistenceFilters currently support exactly one target only."); + + int keyCount = watchFilter.count == 0 ? 0 : (int)watchFilter.count - 1; + + // TODO(dimond): extend this with different existence filters over time. + FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount]; + FSTExistenceFilterWatchChange *change = + [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchReset:(NSArray *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot { + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:watchReset + cause:nil]; + [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; +} + +- (void)doWatchStreamClose:(NSDictionary *)closeSpec { + NSDictionary *errorSpec = closeSpec[@"error"]; + int code = ((NSNumber *)(errorSpec[@"code"])).intValue; + [self.driver receiveWatchStreamError:code userInfo:errorSpec]; +} + +- (void)doWriteAck:(NSDictionary *)spec { + FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]]; + NSNumber *expectUserCallback = spec[@"expectUserCallback"]; + + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; + FSTOutstandingWrite *write = + [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]]; + + if (expectUserCallback.boolValue) { + FSTAssert(write.done, @"Write should be done"); + FSTAssert(!write.error, @"Ack should not fail"); + } +} + +- (void)doFailWrite:(NSDictionary *)spec { + NSDictionary *errorSpec = spec[@"error"]; + NSNumber *expectUserCallback = spec[@"expectUserCallback"]; + + int code = ((NSNumber *)(errorSpec[@"code"])).intValue; + FSTOutstandingWrite *write = [self.driver receiveWriteError:code userInfo:errorSpec]; + + if (expectUserCallback.boolValue) { + FSTAssert(write.done, @"Write should be done"); + XCTAssertNotNil(write.error, @"Write should have failed"); + XCTAssertEqualObjects(write.error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(write.error.code, code); + } +} + +- (void)doChangeUser:(id)UID { + FSTUser *user = [UID isEqual:[NSNull null]] ? [FSTUser unauthenticatedUser] + : [[FSTUser alloc] initWithUID:UID]; + [self.driver changeUser:user]; +} + +- (void)doRestart { + // Any outstanding user writes should be automatically re-sent, so we want to preserve them + // when re-creating the driver. + FSTOutstandingWriteQueues *outstandingWrites = self.driver.outstandingWrites; + + [self.driver shutdown]; + + // NOTE: We intentionally don't shutdown / re-create driverPersistence, since we want to + // preserve the persisted state. This is a bit of a cheat since it means we're not exercising + // the initialization / start logic that would normally be hit, but simplifies the plumbing and + // allows us to run these tests against FSTMemoryPersistence as well (there would be no way to + // re-create FSTMemoryPersistence without losing all persisted state). + + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence + garbageCollector:self.garbageCollector + initialUser:self.driver.currentUser + outstandingWrites:outstandingWrites]; + [self.driver start]; +} + +- (void)doStep:(NSDictionary *)step { + if (step[@"userListen"]) { + [self doListen:step[@"userListen"]]; + } else if (step[@"userUnlisten"]) { + [self doUnlisten:step[@"userUnlisten"]]; + } else if (step[@"userSet"]) { + [self doSet:step[@"userSet"]]; + } else if (step[@"userPatch"]) { + [self doPatch:step[@"userPatch"]]; + } else if (step[@"userDelete"]) { + [self doDelete:step[@"userDelete"]]; + } else if (step[@"watchAck"]) { + [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchCurrent"]) { + [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchRemove"]) { + [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchEntity"]) { + [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchFilter"]) { + [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchReset"]) { + [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]]; + } else if (step[@"watchStreamClose"]) { + [self doWatchStreamClose:step[@"watchStreamClose"]]; + } else if (step[@"watchProto"]) { + // watchProto isn't yet used, and it's unclear how to create arbitrary protos from JSON. + FSTFail(@"watchProto is not yet supported."); + } else if (step[@"writeAck"]) { + [self doWriteAck:step[@"writeAck"]]; + } else if (step[@"failWrite"]) { + [self doFailWrite:step[@"failWrite"]]; + } else if (step[@"changeUser"]) { + [self doChangeUser:step[@"changeUser"]]; + } else if (step[@"restart"]) { + [self doRestart]; + } else { + XCTFail(@"Unknown step: %@", step); + } +} + +- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { + FSTQuery *expectedQuery = [self parseQuery:expected[@"query"]]; + XCTAssertEqualObjects(actual.query, expectedQuery); + if ([expected[@"errorCode"] integerValue] != 0) { + XCTAssertNotNil(actual.error); + XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]); + } else { + NSMutableArray *expectedChanges = [NSMutableArray array]; + NSMutableArray *removed = expected[@"removed"]; + for (NSArray *changeSpec in removed) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeRemoved]]; + } + NSMutableArray *added = expected[@"added"]; + for (NSArray *changeSpec in added) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeAdded]]; + } + NSMutableArray *modified = expected[@"modified"]; + for (NSArray *changeSpec in modified) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeModified]]; + } + NSMutableArray *metadata = expected[@"metadata"]; + for (NSArray *changeSpec in metadata) { + [expectedChanges + addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeMetadata]]; + } + XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges); + + BOOL expectedHasPendingWrites = + expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO; + BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO; + XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites, + @"hasPendingWrites"); + XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache"); + } +} + +- (void)validateStepExpectations:(NSMutableArray *_Nullable)stepExpectations { + NSArray *events = self.driver.capturedEventsSinceLastCall; + + if (!stepExpectations) { + XCTAssertEqual(events.count, 0); + for (FSTQueryEvent *event in events) { + XCTFail(@"Unexpected event: %@", event); + } + return; + } + + events = + [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) { + return [q1.query.canonicalID compare:q2.query.canonicalID]; + }]; + + XCTAssertEqual(events.count, stepExpectations.count); + NSUInteger i = 0; + for (; i < stepExpectations.count && i < events.count; ++i) { + [self validateEvent:events[i] matches:stepExpectations[i]]; + } + for (; i < stepExpectations.count; ++i) { + XCTFail(@"Missing event: %@", stepExpectations[i]); + } + for (; i < events.count; ++i) { + XCTFail(@"Unexpected event: %@", events[i]); + } +} + +- (void)validateStateExpectations:(nullable NSDictionary *)expected { + if (expected) { + if (expected[@"numOutstandingWrites"]) { + XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]); + } + if (expected[@"limboDocs"]) { + NSMutableSet *expectedLimboDocuments = [NSMutableSet set]; + NSArray *docNames = expected[@"limboDocs"]; + for (NSString *name in docNames) { + [expectedLimboDocuments addObject:FSTTestDocKey(name)]; + } + // Update the expected limbo documents + self.driver.expectedLimboDocuments = expectedLimboDocuments; + } + if (expected[@"activeTargets"]) { + NSMutableDictionary *expectedActiveTargets = [NSMutableDictionary dictionary]; + [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, + NSDictionary *queryData, + BOOL *stop) { + FSTTargetID targetID = [targetIDString intValue]; + FSTQuery *query = [self parseQuery:queryData[@"query"]]; + NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; + // TODO(mcg): populate the purpose of the target once it's possible to encode that in the + // spec tests. For now, hard-code that it's a listen despite the fact that it's not always + // the right value. + expectedActiveTargets[@(targetID)] = + [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:resumeToken]; + }]; + self.driver.expectedActiveTargets = expectedActiveTargets; + } + } + + // Always validate that the expected limbo docs match the actual limbo docs. + [self validateLimboDocuments]; + // Always validate that the expected active targets match the actual active targets. + [self validateActiveTargets]; +} + +- (void)validateLimboDocuments { + // Make a copy so it can modified while checking against the expected limbo docs. + NSMutableDictionary *actualLimboDocs = + [NSMutableDictionary dictionaryWithDictionary:self.driver.currentLimboDocuments]; + + // Validate that each limbo doc has an expected active target + [actualLimboDocs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTBoxedTargetID *targetID, BOOL *stop) { + XCTAssertNotNil(self.driver.expectedActiveTargets[targetID], + @"Found limbo doc without an expected active target"); + }]; + + for (FSTDocumentKey *expectedLimboDoc in self.driver.expectedLimboDocuments) { + XCTAssertNotNil(actualLimboDocs[expectedLimboDoc], + @"Expected doc to be in limbo, but was not: %@", expectedLimboDoc); + [actualLimboDocs removeObjectForKey:expectedLimboDoc]; + } + XCTAssertTrue(actualLimboDocs.count == 0, "Unexpected docs in limbo: %@", actualLimboDocs); +} + +- (void)validateActiveTargets { + // Create a copy so we can modify it in tests + NSMutableDictionary *actualTargets = + [NSMutableDictionary dictionaryWithDictionary:self.driver.activeTargets]; + + [self.driver.expectedActiveTargets enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, + FSTQueryData *queryData, + BOOL *stop) { + XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData); + + // TODO(mcg): validate the purpose of the target once it's possible to encode that in the + // spec tests. For now, only validate properties that can be validated. + // XCTAssertEqualObjects(actualTargets[targetID], queryData); + + FSTQueryData *actual = actualTargets[targetID]; + XCTAssertEqualObjects(actual.query, queryData.query); + XCTAssertEqual(actual.targetID, queryData.targetID); + XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion); + XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); + + [actualTargets removeObjectForKey:targetID]; + }]; + XCTAssertTrue(actualTargets.count == 0, "Unexpected active targets: %@", actualTargets); +} + +- (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config { + @try { + [self setUpForSpecWithConfig:config]; + for (NSDictionary *step in steps) { + FSTLog(@"Doing step %@", step); + [self doStep:step]; + [self validateStepExpectations:step[@"expect"]]; + [self validateStateExpectations:step[@"stateExpect"]]; + } + [self.driver validateUsage]; + } @finally { + // Ensure that the driver is torn down even if the test is failing due to a thrown exception so + // that any resources held by the driver are released. This is important when the driver is + // backed by LevelDB because LevelDB locks its database. If -tearDownForSpec were not called + // after an exception then subsequent attempts to open the LevelDB will fail, making it harder + // to zero in on the spec tests as a culprit. + [self tearDownForSpec]; + } +} + +#pragma mark - The actual test methods. + +- (void)testSpecTests { + if ([self isTestBaseClass]) return; + + // Enumerate the .json files containing the spec tests. + NSMutableArray *specFiles = [NSMutableArray array]; + NSMutableArray *parsedSpecs = [NSMutableArray array]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSFileManager *fs = [NSFileManager defaultManager]; + BOOL exclusiveMode = NO; + for (NSString *file in [fs enumeratorAtPath:[bundle bundlePath]]) { + if (![@"json" isEqual:[file pathExtension]]) { + continue; + } + + // Read and parse the JSON from the file. + NSString *fileName = [file stringByDeletingPathExtension]; + NSString *path = [bundle pathForResource:fileName ofType:@"json"]; + NSData *json = [NSData dataWithContentsOfFile:path]; + XCTAssertNotNil(json); + NSError *error = nil; + id _Nullable parsed = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error]; + XCTAssertNil(error, @"%@", error); + XCTAssertTrue([parsed isKindOfClass:[NSDictionary class]]); + NSDictionary *testDict = (NSDictionary *)parsed; + + exclusiveMode = exclusiveMode || [self anyTestsAreMarkedExclusive:testDict]; + [specFiles addObject:fileName]; + [parsedSpecs addObject:testDict]; + } + + // Now iterate over them and run them. + __block bool ranAtLeastOneTest = NO; + for (NSUInteger i = 0; i < specFiles.count; i++) { + NSLog(@"Spec test file: %@", specFiles[i]); + // Iterate over the tests in the file and run them. + [parsedSpecs[i] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); + NSDictionary *testDescription = (NSDictionary *)obj; + NSString *describeName = testDescription[@"describeName"]; + NSString *itName = testDescription[@"itName"]; + NSString *name = [NSString stringWithFormat:@"%@ %@", describeName, itName]; + NSDictionary *config = testDescription[@"config"]; + NSArray *steps = testDescription[@"steps"]; + NSArray *tags = testDescription[@"tags"]; + + BOOL runTest = !exclusiveMode || [tags indexOfObject:kExclusiveTag] != NSNotFound; + if ([tags indexOfObject:kNoIOSTag] != NSNotFound) { + runTest = NO; + } + if (runTest) { + NSLog(@" Spec test: %@", name); + [self runSpecTestSteps:steps config:config]; + ranAtLeastOneTest = YES; + } else { + NSLog(@" [SKIPPED] Spec test: %@", name); + } + }]; + } + XCTAssertTrue(ranAtLeastOneTest); +} + +- (BOOL)anyTestsAreMarkedExclusive:(NSDictionary *)tests { + __block BOOL found = NO; + [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]); + NSDictionary *testDescription = (NSDictionary *)obj; + NSArray *tags = testDescription[@"tags"]; + if ([tags indexOfObject:kExclusiveTag] != NSNotFound) { + found = YES; + *stop = YES; + } + }]; + return found; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h new file mode 100644 index 0000000..0643d76 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -0,0 +1,248 @@ +/* + * 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 + +#import "Core/FSTTypes.h" + +@class FSTDocumentKey; +@class FSTMutation; +@class FSTMutationResult; +@class FSTQuery; +@class FSTQueryData; +@class FSTSnapshotVersion; +@class FSTUser; +@class FSTViewSnapshot; +@class FSTWatchChange; +@protocol FSTGarbageCollector; +@protocol FSTPersistence; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Interface used for object that contain exactly one of either a view snapshot or an error for the + * given query. + */ +@interface FSTQueryEvent : NSObject +@property(nonatomic, strong) FSTQuery *query; +@property(nonatomic, strong, nullable) FSTViewSnapshot *viewSnapshot; +@property(nonatomic, strong, nullable) NSError *error; +@end + +/** Holds an outstanding write and its result. */ +@interface FSTOutstandingWrite : NSObject +/** The write that is outstanding. */ +@property(nonatomic, strong, readwrite) FSTMutation *write; +/** Whether this write is done (regardless of whether it was successful or not). */ +@property(nonatomic, assign, readwrite) BOOL done; +/** The error - if any - of this write. */ +@property(nonatomic, strong, nullable, readwrite) NSError *error; +@end + +/** Mapping of user => array of FSTMutations for that user. */ +typedef NSDictionary *> FSTOutstandingWriteQueues; + +/** + * A test driver for FSTSyncEngine that allows simulated event delivery and capture. As much as + * possible, all sources of nondeterminism are removed so that test execution is consistent and + * reliable. + * + * FSTSyncEngineTestDriver: + * + * + constructs an FSTSyncEngine using a mocked FSTDatastore for the backend; + * + allows the caller to trigger events (user API calls and incoming FSTDatastore messages); + * + performs sequencing validation internally (e.g. that when a user mutation is initiated, the + * FSTSyncEngine correctly sends it to the remote store); and + * + exposes the set of FSTQueryEvents generated for the caller to verify. + * + * Events come in three major flavors: + * + * + user events: simulate user API calls + * + watch events: simulate RPC interactions with the Watch backend + * + write events: simulate RPC interactions with the Streaming Write backend + * + * Each method on the driver injects a different event into the system. + */ +@interface FSTSyncEngineTestDriver : NSObject + +/** + * Initializes the underlying FSTSyncEngine with the given local persistence implementation and + * garbage collection policy. + */ +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector; + +/** + * Initializes the underlying FSTSyncEngine with the given local persistence implementation and + * a set of existing outstandingWrites (useful when your FSTPersistence object has + * persisted mutation queues). + */ +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector + initialUser:(FSTUser *)initialUser + outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** Starts the FSTSyncEngine and its underlying components. */ +- (void)start; + +/** Validates that the API has been used correctly after a test is complete. */ +- (void)validateUsage; + +/** Shuts the FSTSyncEngine down. */ +- (void)shutdown; + +/** + * Adds a listener to the FSTSyncEngine as if the user had initiated a new listen for the given + * query. + * + * Resulting events are captured and made available via the capturedEventsSinceLastCall method. + * + * @param query A valid query to execute against the backend. + * @return The target ID assigned by the system to track the query. + */ +- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query; + +/** + * Removes a listener from the FSTSyncEngine as if the user had removed a listener corresponding + * to the given query. + * + * Resulting events are captured and made available via the capturedEventsSinceLastCall method. + * + * @param query An identical query corresponding to one passed to -addUserListenerWithQuery. + */ +- (void)removeUserListenerWithQuery:(FSTQuery *)query; + +/** + * Delivers a WatchChange RPC to the FSTSyncEngine as if it were received from the backend watch + * service, either in response to addUserListener: or removeUserListener calls or because the + * simulated backend has new data. + * + * Resulting events are captured and made available via the capturedEventsSinceLastCall method. + * + * @param change Any type of watch change + * @param snapshot A snapshot version to attach, if applicable. This should be sent when + * simulating the server having sent a complete snapshot. + */ +- (void)receiveWatchChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot; + +/** + * Delivers a watch stream error as if the Streaming Watch backend has generated some kind of error. + * + * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h + * @param userInfo Any additional details that the server might have sent along with the error. + * For the moment this is effectively unused, but is logged. + */ +- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary *)userInfo; + +/** + * Performs a mutation against the FSTSyncEngine as if the user had written the mutation through + * the API. + * + * Also retains the mutation so that the driver can validate that the sync engine sent the mutation + * to the remote store before receiveWatchChange:snapshotVersion: and receiveWriteError:userInfo: + * events are processed. + * + * @param mutation Any type of valid mutation. + */ +- (void)writeUserMutation:(FSTMutation *)mutation; + +/** + * Delivers a write error as if the Streaming Write backend has generated some kind of error. + * + * For the moment write errors are usually must be in response to a mutation that has been written + * with writeUserMutation:. Spontaneously errors due to idle timeout, server restart, or credential + * expiration aren't yet supported. + * + * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h + * @param userInfo Any additional details that the server might have sent along with the error. + * For the moment this is effectively unused, but is logged. + */ +- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode + userInfo:(NSDictionary *)userInfo; + +/** + * Delivers a write acknowledgement as if the Streaming Write backend has acknowledged a write with + * the snapshot version at which the write was committed. + * + * @param commitVersion The snapshot version at which the simulated server has committed + * the mutation. Snapshot versions must be monotonically increasing. + * @param mutationResults The mutation results for the write that is being acked. + */ +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults; + +/** + * A count of the mutations written to the write stream by the FSTSyncEngine, but not yet + * acknowledged via receiveWriteError: or receiveWriteAckWithVersion:mutationResults. + */ +@property(nonatomic, readonly) int sentWritesCount; + +/** + * Switches the FSTSyncEngine to a new user. The test driver tracks the outstanding mutations for + * each user, so future receiveWriteAck/Error operations will validate the write sent to the mock + * datastore matches the next outstanding write for that user. + */ +- (void)changeUser:(FSTUser *)user; + +/** + * Returns all query events generated by the FSTSyncEngine in response to the event injection + * methods called previously. The events are cleared after each invocation of this method. + */ +- (NSArray *)capturedEventsSinceLastCall; + +/** + * The writes that have been sent to the FSTSyncEngine via writeUserMutation: but not yet + * acknowledged by calling receiveWriteAck/Error:. They are tracked per-user. + * + * It is mostly an implementation detail used internally to validate that the writes sent to the + * mock backend by the FSTSyncEngine match the user mutations that initiated them. + * + * It is exposed specifically for use with the + * initWithPersistence:GCEnabled:outstandingWrites: initializer to test persistence + * scenarios where the FSTSyncEngine is restarted while the FSTPersistence implementation still has + * outstanding persisted mutations. + * + * Note: The size of the list for the current user will generally be the same as + * sentWritesCount, but not necessarily, since the FSTRemoteStore limits the number of + * outstanding writes to the backend at a given time. + */ +@property(nonatomic, strong, readonly) FSTOutstandingWriteQueues *outstandingWrites; + +/** The current user for the FSTSyncEngine; determines which mutation queue is active. */ +@property(nonatomic, strong, readonly) FSTUser *currentUser; + +/** The current set of documents in limbo. */ +@property(nonatomic, strong, readonly) + NSDictionary *currentLimboDocuments; + +/** The expected set of documents in limbo. */ +@property(nonatomic, strong, readwrite) NSSet *expectedLimboDocuments; + +/** The set of active targets as observed on the watch stream. */ +@property(nonatomic, strong, readonly) + NSDictionary *activeTargets; + +/** The expected set of active targets, keyed by target ID. */ +@property(nonatomic, strong, readwrite) + NSDictionary *expectedActiveTargets; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m new file mode 100644 index 0000000..b4c0b02 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m @@ -0,0 +1,291 @@ +/* + * 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 "FSTSyncEngineTestDriver.h" + +#import + +#import "Auth/FSTUser.h" +#import "Core/FSTEventManager.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTSyncEngine.h" +#import "Firestore/FIRFirestoreErrors.h" +#import "Local/FSTLocalStore.h" +#import "Local/FSTPersistence.h" +#import "Model/FSTMutation.h" +#import "Remote/FSTDatastore.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTAssert.h" +#import "Util/FSTDispatchQueue.h" +#import "Util/FSTLogger.h" + +#import "FSTMockDatastore.h" +#import "FSTSyncEngine+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryEvent + +- (NSString *)description { + // The Query is also included in the view, so we skip it. + return [NSString stringWithFormat:@"", + self.viewSnapshot, self.error]; +} + +@end + +@implementation FSTOutstandingWrite +@end + +@interface FSTSyncEngineTestDriver () + +#pragma mark - Parts of the Firestore system that the spec tests need to control. + +@property(nonatomic, strong, readonly) FSTMockDatastore *datastore; +@property(nonatomic, strong, readonly) FSTEventManager *eventManager; +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; + +#pragma mark - Data structures for holding events sent by the watch stream. + +/** A block for the FSTEventAggregator to use to report events to the test. */ +@property(nonatomic, strong, readonly) void (^eventHandler)(FSTQueryEvent *); +/** The events received by our eventHandler and not yet retrieved via capturedEventsSinceLastCall */ +@property(nonatomic, strong, readonly) NSMutableArray *events; +/** A dictionary for tracking the listens on queries. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *queryListeners; + +#pragma mark - Other data structures. +@property(nonatomic, strong, readwrite) FSTUser *currentUser; + +@end + +@implementation FSTSyncEngineTestDriver { + // ivar is declared as mutable. + NSMutableDictionary *> *_outstandingWrites; +} + +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector { + return [self initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:[FSTUser unauthenticatedUser] + outstandingWrites:@{}]; +} + +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector + initialUser:(FSTUser *)initialUser + outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites { + if (self = [super init]) { + // Create mutable copy of outstandingWrites. + _outstandingWrites = [NSMutableDictionary dictionary]; + [outstandingWrites enumerateKeysAndObjectsUsingBlock:^( + FSTUser *user, NSArray *writes, BOOL *stop) { + _outstandingWrites[user] = [writes mutableCopy]; + }]; + + _events = [NSMutableArray array]; + + // Set up the sync engine and various stores. + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue]; + _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence + garbageCollector:garbageCollector + initialUser:initialUser]; + _datastore = [FSTMockDatastore mockDatastoreWithWorkerDispatchQueue:dispatchQueue]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore]; + + _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore + remoteStore:_remoteStore + initialUser:initialUser]; + _remoteStore.syncEngine = _syncEngine; + _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; + + _remoteStore.onlineStateDelegate = _eventManager; + + // Set up internal event tracking for the spec tests. + NSMutableArray *events = [NSMutableArray array]; + _eventHandler = ^(FSTQueryEvent *e) { + [events addObject:e]; + }; + _events = events; + + _queryListeners = [NSMutableDictionary dictionary]; + + _expectedLimboDocuments = [NSSet set]; + + _expectedActiveTargets = [NSDictionary dictionary]; + + _currentUser = initialUser; + } + return self; +} + +- (void)start { + [self.localStore start]; + [self.remoteStore start]; +} + +- (void)validateUsage { + // We could relax this if we found a reason to. + FSTAssert(self.events.count == 0, + @"You must clear all pending events by calling" + " capturedEventsSinceLastCall before calling shutdown."); +} + +- (void)shutdown { + [self.remoteStore shutdown]; + [self.localStore shutdown]; +} + +- (void)validateNextWriteSent:(FSTMutation *)expectedWrite { + NSArray *request = [self.datastore nextSentWrite]; + // Make sure the write went through the pipe like we expected it to. + FSTAssert(request.count == 1, @"Only single mutation requests are supported at the moment"); + FSTMutation *actualWrite = request[0]; + FSTAssert([actualWrite isEqual:expectedWrite], + @"Mock datastore received write %@ but first outstanding mutation was %@", actualWrite, + expectedWrite); + FSTLog(@"A write was sent: %@", actualWrite); +} + +- (int)sentWritesCount { + return [self.datastore writesSent]; +} + +- (void)changeUser:(FSTUser *)user { + self.currentUser = user; + [self.syncEngine userDidChange:user]; +} + +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults: + (NSArray *)mutationResults { + FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; + [[self currentOutstandingWrites] removeObjectAtIndex:0]; + [self validateNextWriteSent:write.write]; + + [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults]; + + return write; +} + +- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode + userInfo:(NSDictionary *)userInfo { + NSError *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; + + FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; + [self validateNextWriteSent:write.write]; + + // If this is a permanent error, the mutation is not expected to be sent again so we remove it + // from currentOutstandingWrites. + if ([FSTDatastore isPermanentWriteError:error]) { + [[self currentOutstandingWrites] removeObjectAtIndex:0]; + } + + FSTLog(@"Failing a write."); + [self.datastore failWriteWithError:error]; + + return write; +} + +- (NSArray *)capturedEventsSinceLastCall { + NSArray *result = [self.events copy]; + [self.events removeAllObjects]; + return result; +} + +- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query { + // TODO(dimond): Allow customizing listen options in spec tests + // TODO(dimond): Change spec tests to verify isFromCache on snapshots + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:NO]; + FSTQueryListener *listener = [[FSTQueryListener alloc] + initWithQuery:query + options:options + viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; + event.query = query; + event.viewSnapshot = snapshot; + event.error = error; + [self.events addObject:event]; + }]; + self.queryListeners[query] = listener; + return [self.eventManager addListener:listener]; +} + +- (void)removeUserListenerWithQuery:(FSTQuery *)query { + FSTQueryListener *listener = self.queryListeners[query]; + [self.queryListeners removeObjectForKey:query]; + [self.eventManager removeListener:listener]; +} + +- (void)writeUserMutation:(FSTMutation *)mutation { + FSTOutstandingWrite *write = [[FSTOutstandingWrite alloc] init]; + write.write = mutation; + [[self currentOutstandingWrites] addObject:write]; + FSTLog(@"sending a user write."); + [self.syncEngine writeMutations:@[ mutation ] + completion:^(NSError *_Nullable error) { + FSTLog(@"A callback was called with error: %@", error); + write.done = YES; + write.error = error; + }]; +} + +- (void)receiveWatchChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { + [self.datastore writeWatchChange:change snapshotVersion:snapshot]; +} + +- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary *)userInfo { + NSError *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo]; + + [self.datastore failWatchStreamWithError:error]; + // Unlike web, stream should re-open synchronously + FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open"); +} + +- (NSDictionary *)currentLimboDocuments { + return [self.syncEngine currentLimboDocuments]; +} + +- (NSDictionary *)activeTargets { + return [[self.datastore activeTargets] copy]; +} + +#pragma mark - Helper Methods + +- (NSMutableArray *)currentOutstandingWrites { + NSMutableArray *writes = _outstandingWrites[self.currentUser]; + if (!writes) { + writes = [NSMutableArray array]; + _outstandingWrites[self.currentUser] = writes; + } + return writes; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/SpecTests/json/README.md b/Firestore/Example/Tests/SpecTests/json/README.md new file mode 100644 index 0000000..bcc9b38 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/README.md @@ -0,0 +1,3 @@ +These json files are generated from the web test sources. + +TODO(mikelehen): Re-add instructions for generating these. diff --git a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json new file mode 100644 index 0000000..ef41afe --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json @@ -0,0 +1,147 @@ +{ + "Events are raised after watch ack": { + "describeName": "Collections:", + "itName": "Events are raised after watch ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Events are raised for local sets before watch ack": { + "describeName": "Collections:", + "itName": "Events are raised for local sets before watch ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json new file mode 100644 index 0000000..ab42241 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json @@ -0,0 +1,738 @@ +{ + "Existence filter mismatch triggers re-run of query": { + "describeName": "Existence Filters:", + "itName": "Existence filter mismatch triggers re-run of query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter mismatch will drop resume token": { + "describeName": "Existence Filters:", + "itName": "Existence filter mismatch will drop resume token", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "existence-filter-resume-token" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "existence-filter-resume-token" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter limbo resolution is denied": { + "describeName": "Existence Filters:", + "itName": "Existence filter limbo resolution is denied", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 1 + ], + "cause": { + "code": 7 + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + }, + "limboDocs": [] + }, + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json new file mode 100644 index 0000000..ee2d883 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json @@ -0,0 +1,1150 @@ +{ + "Limbo documents are deleted without an existence filter": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are deleted without an existence filter", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Limbo documents are deleted with an existence filter": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are deleted with an existence filter", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchFilter": [ + [ + 1 + ] + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Limbo documents are resolved with updates": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are resolved with updates", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "b" + } + ] + ], + "targets": [ + 1 + ] + } + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Limbo documents are resolved with updates in different snapshot than \"current\"": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents are resolved with updates in different snapshot than \"current\"", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "b" + } + ] + ], + "targets": [ + 1, + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "a" + ] + ], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "filters": [ + [ + "key", + "==", + "b" + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1003" + ], + "watchSnapshot": 1003 + } + ] + }, + "Document remove message will cause docs to go in limbo": { + "describeName": "Limbo Documents:", + "itName": "Document remove message will cause docs to go in limbo", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "key": "collection/b", + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1003, + "stateExpect": { + "limboDocs": [ + "collection/b" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-1004" + ], + "watchSnapshot": 1004, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json new file mode 100644 index 0000000..5a02463 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json @@ -0,0 +1,1626 @@ +{ + "Documents in limit are replaced by remote event": { + "describeName": "Limits:", + "itName": "Documents in limit are replaced by remote event", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1002, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Deleted Document in limbo in full limit query": { + "describeName": "Limits:", + "itName": "Deleted Document in limbo in full limit query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1002" + ], + "watchSnapshot": 1002, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ], + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/a" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Documents in limit can handle removed messages": { + "describeName": "Limits:", + "itName": "Documents in limit can handle removed messages", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "key": "collection/c", + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Documents in limit are can handle removed messages for only one of many query": { + "describeName": "Limits:", + "itName": "Documents in limit are can handle removed messages for only one of many query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ] + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "targets": [ + 2, + 4 + ] + } + }, + { + "watchEntity": { + "key": "collection/c", + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1002, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/c", + 1001, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 3, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1002, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Multiple docs in limbo in full limit query": { + "describeName": "Limits:", + "itName": "Multiple docs in limbo in full limit query", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1001, + { + "key": "b" + } + ], + [ + "collection/c", + 1002, + { + "key": "c" + } + ], + [ + "collection/d", + 1003, + { + "key": "d" + } + ], + [ + "collection/e", + 1004, + { + "key": "e" + } + ], + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1005" + ], + "watchSnapshot": 1005, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ], + [ + "collection/d", + 1003, + { + "key": "d" + } + ], + [ + "collection/e", + 1004, + { + "key": "e" + } + ], + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchReset": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/e", + 1004, + { + "key": "e" + } + ], + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/a", + "collection/b" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "3": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/b", + "collection/c" + ], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "3": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "5": { + "query": { + "path": "collection/c", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 1 + ] + } + }, + { + "watchAck": [ + 3 + ] + }, + { + "watchCurrent": [ + [ + 3 + ], + "resume-token-2001" + ], + "watchSnapshot": 2001, + "stateExpect": { + "limboDocs": [ + "collection/c", + "collection/d" + ], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "5": { + "query": { + "path": "collection/c", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "7": { + "query": { + "path": "collection/d", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/d", + 1003, + { + "key": "d" + } + ] + ], + "removed": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 3 + ] + } + }, + { + "watchAck": [ + 5 + ] + }, + { + "watchCurrent": [ + [ + 5 + ], + "resume-token-2002" + ], + "watchSnapshot": 2002, + "stateExpect": { + "limboDocs": [ + "collection/d" + ], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "7": { + "query": { + "path": "collection/d", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/e", + 1004, + { + "key": "e" + } + ] + ], + "removed": [ + [ + "collection/c", + 1002, + { + "key": "c" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 5 + ] + } + }, + { + "watchAck": [ + 7 + ] + }, + { + "watchCurrent": [ + [ + 7 + ], + "resume-token-2003" + ], + "watchSnapshot": 2003, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/d", + 1003, + { + "key": "d" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection", + "limit": 2, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/f", + 1005, + { + "key": "f" + } + ] + ], + "removed": [ + [ + "collection/d", + 1003, + { + "key": "d" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchRemove": { + "targetIds": [ + 7 + ] + } + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json new file mode 100644 index 0000000..35704f2 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json @@ -0,0 +1,1524 @@ +{ + "Contents of query are cleared when listen is removed.": { + "describeName": "Listens:", + "itName": "Contents of query are cleared when listen is removed.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + } + ] + }, + "Contents of query update when new data is received.": { + "describeName": "Listens:", + "itName": "Contents of query update when new data is received.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Ensure correct query results with latency-compensated deletes": { + "describeName": "Listens:", + "itName": "Ensure correct query results with latency-compensated deletes", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userDelete": "collection/b" + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "a": true + } + ], + [ + "collection/b", + 1000, + { + "b": true + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "a": true + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection", + "limit": 10, + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 10, + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 10, + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "a": true + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will process removals without waiting for a consistent snapshot": { + "describeName": "Listens:", + "itName": "Will process removals without waiting for a consistent snapshot", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchRemove": { + "targetIds": [ + 2 + ], + "cause": { + "code": 8 + } + }, + "stateExpect": { + "activeTargets": {} + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 8, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will gracefully process failed targets": { + "describeName": "Listens:", + "itName": "Will gracefully process failed targets", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection1", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userListen": [ + 4, + { + "path": "collection2", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection1/a", + 1000, + { + "a": true + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection2/a", + 1001, + { + "b": true + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ], + "cause": { + "code": 8 + } + }, + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "errorCode": 8, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection2/a", + 1001, + { + "b": true + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will gracefully handle watch stream reverting snapshots": { + "describeName": "Listens:", + "itName": "Will gracefully handle watch stream reverting snapshots", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Will gracefully handle watch stream reverting snapshots (with restart)": { + "describeName": "Listens:", + "itName": "Will gracefully handle watch stream reverting snapshots (with restart)", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Individual documents cannot revert": { + "describeName": "Listens:", + "itName": "Individual documents cannot revert", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": "v1000", + "visible": true + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 3000, + { + "v": "v3000", + "visible": false + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-4000" + ], + "watchSnapshot": 4000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 3000, + { + "v": "v3000", + "visible": false + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 4 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": "v2000", + "visible": false + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-5000" + ], + "watchSnapshot": 5000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-4000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 3000, + { + "v": "v3000", + "visible": false + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-6000" + ], + "watchSnapshot": 6000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json new file mode 100644 index 0000000..e58bae1 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json @@ -0,0 +1,151 @@ +{ + "Empty queries are resolved if client goes offline": { + "describeName": "Offline:", + "itName": "Empty queries are resolved if client goes offline", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + } + ] + }, + "A successful message delays offline status": { + "describeName": "Offline:", + "itName": "A successful message delays offline status", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json new file mode 100644 index 0000000..1009206 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json @@ -0,0 +1,155 @@ +{ + "orderBy applies filtering based on local state": { + "describeName": "OrderBy:", + "itName": "orderBy applies filtering based on local state", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "key": "a", + "sort": 1 + } + ] + }, + { + "userPatch": [ + "collection/b", + { + "sort": 2 + } + ] + }, + { + "userSet": [ + "collection/c", + { + "key": "b" + } + ] + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + }, + "added": [ + [ + "collection/a", + 0, + { + "key": "a", + "sort": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1001, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [ + [ + "sort", + "asc" + ] + ] + }, + "added": [ + [ + "collection/b", + 1001, + { + "key": "b", + "sort": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json new file mode 100644 index 0000000..158e337 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json @@ -0,0 +1,858 @@ +{ + "Local mutations are persisted and re-sent": { + "describeName": "Persistence:", + "itName": "Local mutations are persisted and re-sent", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/key1", + { + "foo": "bar" + } + ] + }, + { + "userSet": [ + "collection/key2", + { + "baz": "quu" + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [], + "numOutstandingWrites": 2 + } + }, + { + "writeAck": { + "version": 1, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 2, + "expectUserCallback": false + }, + "stateExpect": { + "numOutstandingWrites": 0 + } + } + ] + }, + "Persisted local mutations are visible to listeners": { + "describeName": "Persistence:", + "itName": "Persisted local mutations are visible to listeners", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/key1", + { + "foo": "bar" + } + ] + }, + { + "userSet": [ + "collection/key2", + { + "baz": "quu" + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key1", + 0, + { + "foo": "bar" + }, + "local" + ], + [ + "collection/key2", + 0, + { + "baz": "quu" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + } + ] + }, + "Remote documents are persisted": { + "describeName": "Persistence:", + "itName": "Remote documents are persisted", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Remote documents from watch are not GC'd": { + "describeName": "Persistence:", + "itName": "Remote documents from watch are not GC'd", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Remote documents from user sets are not GC'd": { + "describeName": "Persistence:", + "itName": "Remote documents from user sets are not GC'd", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ] + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Mutation Queue is persisted across uid switches": { + "describeName": "Persistence:", + "itName": "Mutation Queue is persisted across uid switches", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "users/anon", + { + "uid": "anon" + } + ] + }, + { + "changeUser": "user1", + "stateExpect": { + "numOutstandingWrites": 0 + } + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1" + } + ] + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1", + "extra": true + } + ] + }, + { + "changeUser": null, + "stateExpect": { + "numOutstandingWrites": 1 + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "changeUser": "user1", + "stateExpect": { + "numOutstandingWrites": 2 + } + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + } + } + ] + }, + "Mutation Queue is persisted across uid switches (with restarts)": { + "describeName": "Persistence:", + "itName": "Mutation Queue is persisted across uid switches (with restarts)", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "users/anon", + { + "uid": "anon" + } + ] + }, + { + "changeUser": "user1", + "stateExpect": { + "numOutstandingWrites": 0 + } + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1" + } + ] + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1", + "extra": true + } + ] + }, + { + "changeUser": null + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [], + "numOutstandingWrites": 1 + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": false + } + }, + { + "changeUser": "user1" + }, + { + "restart": true, + "stateExpect": { + "activeTargets": {}, + "limboDocs": [], + "numOutstandingWrites": 2 + } + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": false + } + } + ] + }, + "Visible mutations reflect uid switches": { + "describeName": "Persistence:", + "itName": "Visible mutations reflect uid switches", + "tags": [ + "persistence" + ], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "users", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "users/existing", + 0, + { + "uid": "existing" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/existing", + 0, + { + "uid": "existing" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "users/anon", + { + "uid": "anon" + } + ], + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/anon", + 0, + { + "uid": "anon" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "changeUser": "user1", + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-500" + } + } + }, + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "users/anon", + 0, + { + "uid": "anon" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "users/user1", + { + "uid": "user1" + } + ], + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/user1", + 0, + { + "uid": "user1" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "changeUser": null, + "expect": [ + { + "query": { + "path": "users", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "users/anon", + 0, + { + "uid": "anon" + }, + "local" + ] + ], + "removed": [ + [ + "users/user1", + 0, + { + "uid": "user1" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json new file mode 100644 index 0000000..edb0751 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json @@ -0,0 +1,559 @@ +{ + "Waits for watch to remove targets": { + "describeName": "Remote store:", + "itName": "Waits for watch to remove targets", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token" + ], + "watchSnapshot": 1000 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Waits for watch to ack last target add": { + "describeName": "Remote store:", + "itName": "Waits for watch to ack last target add", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token" + ], + "watchSnapshot": 1000 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1000, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/c", + 1000, + { + "key": "c" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/d", + 1000, + { + "key": "d" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/d", + 1000, + { + "key": "d" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cleans up watch state correctly": { + "describeName": "Remote store:", + "itName": "Cleans up watch state correctly", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1001" + ], + "watchSnapshot": 1001, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json new file mode 100644 index 0000000..25ea84a --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json @@ -0,0 +1,250 @@ +{ + "Resume tokens are sent after watch stream restarts": { + "describeName": "Resume tokens:", + "itName": "Resume tokens are sent after watch stream restarts", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "custom-query-resume-token" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "custom-query-resume-token" + } + } + } + } + ] + }, + "Resume tokens are used across new listens": { + "describeName": "Resume tokens:", + "itName": "Resume tokens are used across new listens", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "custom-query-resume-token" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "custom-query-resume-token" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ], + "watchSnapshot": 1001 + } + ] + } +} diff --git a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json new file mode 100644 index 0000000..60ed107 --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json @@ -0,0 +1,5437 @@ +{ + "Two sequential writes to different documents smoke test.": { + "describeName": "Writes:", + "itName": "Two sequential writes to different documents smoke test.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 500, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 500, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/b", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/b", + 500, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2500, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 3000 + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/b", + 2500, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Event is raised for a local set before and after the write ack": { + "describeName": "Writes:", + "itName": "Event is raised for a local set before and after the write ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cache will not keep data for an outdated write ack": { + "describeName": "Writes:", + "itName": "Cache will not keep data for an outdated write ack", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 10000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 10000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 10000, + { + "v": 3 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cache raises correct event if write is acked before watch delivers it": { + "describeName": "Writes:", + "itName": "Cache raises correct event if write is acked before watch delivers it", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Cache will hold local write until watch catches up": { + "describeName": "Writes:", + "itName": "Cache will hold local write until watch catches up", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/key", + { + "v": 3 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/key", + 1000, + { + "v": 3 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 3000, + { + "doc": "b" + } + ], + [ + "collection/key", + 3000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 3000, + { + "doc": "b" + } + ] + ], + "metadata": [ + [ + "collection/key", + 3000, + { + "v": 3 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes are pipelined": { + "describeName": "Writes:", + "itName": "Writes are pipelined", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token" + ] + }, + { + "userSet": [ + "collection/a0", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a0", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a1", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a1", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a2", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a2", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a3", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a3", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a4", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a4", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a5", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a5", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a6", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a6", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a7", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a7", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a8", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a8", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a9", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a9", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a10", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a10", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a11", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a11", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a12", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a12", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a13", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a13", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a14", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a14", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ], + "stateExpect": { + "numOutstandingWrites": 10 + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a0", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a0", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a1", + 2000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a1", + 2000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 3000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a2", + 3000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a2", + 3000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 4000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a3", + 4000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 4000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a3", + 4000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 5000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a4", + 5000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 5000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a4", + 5000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 6000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a5", + 6000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 6000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a5", + 6000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 7000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a6", + 7000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 7000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a6", + 7000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 8000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a7", + 8000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 8000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a7", + 8000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 9000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a8", + 9000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 9000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a8", + 9000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 10000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a9", + 10000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 10000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a9", + 10000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 11000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a10", + 11000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 11000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a10", + 11000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 12000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a11", + 12000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 12000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a11", + 12000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 13000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a12", + 13000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 13000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a12", + 13000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 14000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a13", + 14000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 14000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a13", + 14000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 15000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a14", + 15000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 15000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a14", + 15000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Pipelined writes can fail": { + "describeName": "Writes:", + "itName": "Pipelined writes can fail", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/a0", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a0", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a1", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a1", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a2", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a2", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a3", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a3", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a4", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a4", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a5", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a5", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a6", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a6", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a7", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a7", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a8", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a8", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a9", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a9", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a10", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a10", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a11", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a11", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a12", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a12", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a13", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a13", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userSet": [ + "collection/a14", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a14", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ], + "stateExpect": { + "numOutstandingWrites": 10 + } + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a0", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a1", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a2", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a3", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a4", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a5", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a6", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a7", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a8", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a9", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a10", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a11", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a12", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a13", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a14", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "numOutstandingWrites": 0 + } + } + ] + }, + "Failed writes are released immediately.": { + "describeName": "Writes:", + "itName": "Failed writes are released immediately.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/b", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 1000, + { + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Held writes are not re-sent.": { + "describeName": "Writes:", + "itName": "Held writes are not re-sent.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/a", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "userSet": [ + "collection/b", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ], + [ + "collection/b", + 2000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Held writes are released when there are no queries left.": { + "describeName": "Writes:", + "itName": "Held writes are released when there are no queries left.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "collection/a", + { + "v": 1 + } + ], + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 0, + { + "v": 1 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + } + ] + }, + "Writes that fail with code invalid-argument are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code invalid-argument are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 3 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code not-found are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code not-found are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 5 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code already-exists are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code already-exists are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 6 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code permission-denied are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code permission-denied are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 7 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code failed-precondition are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code failed-precondition are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 9 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code aborted are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code aborted are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 10 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code out-of-range are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code out-of-range are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 11 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unimplemented are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code unimplemented are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 12 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code data-loss are rejected": { + "describeName": "Writes:", + "itName": "Writes that fail with code data-loss are rejected", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 15 + }, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code cancelled are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code cancelled are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 1 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unknown are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code unknown are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 2 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code deadline-exceeded are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code deadline-exceeded are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 4 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code resource-exhausted are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code resource-exhausted are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 8 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code internal are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code internal are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 13 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unavailable are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code unavailable are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 14 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Writes that fail with code unauthenticated are retried": { + "describeName": "Writes:", + "itName": "Writes that fail with code unauthenticated are retried", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/key", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userSet": [ + "collection/key", + { + "foo": "bar" + } + ], + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/key", + 0, + { + "foo": "bar" + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "failWrite": { + "error": { + "code": 16 + }, + "expectUserCallback": false + } + }, + { + "writeAck": { + "version": 1000, + "expectUserCallback": true + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/key", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/key", + 0, + { + "foo": "bar" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Ensure correct events after patching a doc (including a delete) and getting watcher events.": { + "describeName": "Writes:", + "itName": "Ensure correct events after patching a doc (including a delete) and getting watcher events.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/doc", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/doc", + 1000, + { + "a": { + "b": 2 + }, + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-500" + ], + "watchSnapshot": 500, + "expect": [ + { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/doc", + 1000, + { + "a": { + "b": 2 + }, + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userPatch": [ + "collection/doc", + { + "v": 2, + "a.c": "" + } + ], + "expect": [ + { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "modified": [ + [ + "collection/doc", + 1000, + { + "a": { + "b": 2 + }, + "v": 2 + }, + "local" + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/doc", + 2000, + { + "a": { + "b": 2 + }, + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "writeAck": { + "version": 2000, + "expectUserCallback": true + }, + "expect": [ + { + "query": { + "path": "collection/doc", + "filters": [], + "orderBys": [] + }, + "metadata": [ + [ + "collection/doc", + 2000, + { + "a": { + "b": 2 + }, + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/Tests-Info.plist b/Firestore/Example/Tests/Tests-Info.plist new file mode 100644 index 0000000..169b6f7 --- /dev/null +++ b/Firestore/Example/Tests/Tests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Firestore/Example/Tests/Util/FSTAssertTests.m b/Firestore/Example/Tests/Util/FSTAssertTests.m new file mode 100644 index 0000000..f0734df --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTAssertTests.m @@ -0,0 +1,105 @@ +/* + * 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 "Util/FSTAssert.h" + +#import + +@interface FSTAssertTests : XCTestCase +@end + +@implementation FSTAssertTests + +- (void)testFail { + @try { + [self failingMethod]; + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); + } +} + +// A method guaranteed to fail. Note that the return type is intentionally something that is +// not actually returned in the body to ensure that the function attribute declarations in the +// macro properly mark this macro invocation as never returning. +- (int)failingMethod { + FSTFail(@"%d:%s:%@", 0, "foo", @"bar"); +} + +- (void)testCFail { + @try { + failingFunction(); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar"); + } +} + +// A function guaranteed to fail. Note that the return type is intentionally something that is +// not actually returned in the body to ensure that the function attribute declarations in the +// macro properly mark this macro invocation as never returning. +int failingFunction() { + FSTCFail(@"%d:%s:%@", 0, "foo", @"bar"); +} + +- (void)testAssertNonFailing { + @try { + FSTAssert(YES, @"shouldn't fail"); + } @catch (NSException *ex) { + XCTFail("Should not have failed, but got %@", ex); + } +} + +- (void)testAssertFailing { + @try { + FSTAssert(NO, @"should fail"); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); + } +} + +- (void)testCAssertNonFailing { + @try { + nonAssertingFunction(); + } @catch (NSException *ex) { + XCTFail("Should not have failed, but got %@", ex); + } +} + +int nonAssertingFunction() { + FSTCAssert(YES, @"shouldn't fail"); + return 0; +} + +- (void)testCAssertFailing { + @try { + assertingFunction(); + XCTFail("Should not have succeeded"); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail"); + } +} + +int assertingFunction() { + FSTCAssert(NO, @"should fail"); +} + +@end diff --git a/Firestore/Example/Tests/Util/FSTComparisonTests.m b/Firestore/Example/Tests/Util/FSTComparisonTests.m new file mode 100644 index 0000000..9728f20 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTComparisonTests.m @@ -0,0 +1,143 @@ +/* + * 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 "Util/FSTComparison.h" + +#import + +union DoubleBits { + double d; + uint64_t bits; +}; + +#define ASSERT_BIT_EQUALS(expected, actual) \ + do { \ + union DoubleBits expectedBits = {.d = expected}; \ + union DoubleBits actualBits = {.d = expected}; \ + if (expectedBits.bits != actualBits.bits) { \ + XCTFail(@"Expected <%f> to compare equal to <%f> with bits <%llX> equal to <%llX>", actual, \ + expected, actualBits.bits, expectedBits.bits); \ + } \ + } while (0); + +#define ASSERT_ORDERED_SAME(doubleValue, longValue) \ + do { \ + NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ + if (result != NSOrderedSame) { \ + XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + } \ + } while (0); + +#define ASSERT_ORDERED_DESCENDING(doubleValue, longValue) \ + do { \ + NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ + if (result != NSOrderedDescending) { \ + XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + } \ + } while (0); + +#define ASSERT_ORDERED_ASCENDING(doubleValue, longValue) \ + do { \ + NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \ + if (result != NSOrderedAscending) { \ + XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \ + } \ + } while (0); + +@interface FSTComparisonTests : XCTestCase +@end + +@implementation FSTComparisonTests + +- (void)testMixedComparison { + // Infinities + ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MIN); + ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MAX); + ASSERT_ORDERED_ASCENDING(-INFINITY, 0LL); + + ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MIN); + ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MAX); + ASSERT_ORDERED_DESCENDING(INFINITY, 0LL); + + // NaN + ASSERT_ORDERED_ASCENDING(NAN, LLONG_MIN); + ASSERT_ORDERED_ASCENDING(NAN, LLONG_MAX); + ASSERT_ORDERED_ASCENDING(NAN, 0LL); + + // Large values (note DBL_MIN is positive and near zero). + ASSERT_ORDERED_ASCENDING(-DBL_MAX, LLONG_MIN); + + // Tests around LLONG_MIN + ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63); + ASSERT_ORDERED_SAME(-0x1.0p63, LLONG_MIN); + ASSERT_ORDERED_ASCENDING(-0x1.0p63, LLONG_MIN + 1); + + XCTAssertLessThan(-0x1.0000000000001p63, -0x1.0p63); + ASSERT_ORDERED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN); + ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN); + + // Tests around LLONG_MAX + // Note LLONG_MAX cannot be exactly represented by a double, so the system rounds it to the + // nearest double, which is 2^63. This number, in turn is larger than the maximum representable + // as a long. + ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX); + ASSERT_ORDERED_DESCENDING(0x1.0p63, LLONG_MAX); + + // The largest value with an exactly long representation + XCTAssertEqual((long)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); + ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL); + + ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL); + ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL); + + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL); + + // Tests around MAX_SAFE_INTEGER + ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL); + + // Tests around MIN_SAFE_INTEGER + ASSERT_ORDERED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL); + ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL); + ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL); + + // Tests around zero. + ASSERT_ORDERED_SAME(-0.0, 0LL); + ASSERT_ORDERED_SAME(0.0, 0LL); + + // The smallest representable positive value should be greater than zero + ASSERT_ORDERED_DESCENDING(DBL_MIN, 0LL); + ASSERT_ORDERED_ASCENDING(-DBL_MIN, 0LL); + + // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal + // number: . + double minSubNormal = 0x1.0p-1074; + ASSERT_ORDERED_DESCENDING(minSubNormal, 0LL); + ASSERT_ORDERED_ASCENDING(-minSubNormal, 0LL); + + // Other sanity checks + ASSERT_ORDERED_ASCENDING(0.5, 1LL); + ASSERT_ORDERED_DESCENDING(0.5, 0LL); + ASSERT_ORDERED_ASCENDING(1.5, 2LL); + ASSERT_ORDERED_DESCENDING(1.5, 1LL); +} + +@end diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h new file mode 100644 index 0000000..ae5392c --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h @@ -0,0 +1,41 @@ +/* + * 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 + +@class FIRDocumentSnapshot; +@class FIRQuerySnapshot; +@class XCTestCase; +@class XCTestExpectation; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error); + +@interface FSTEventAccumulator : NSObject + ++ (instancetype)accumulatorForTest:(XCTestCase *)testCase; + +- (instancetype)init NS_UNAVAILABLE; + +- (id)awaitEventWithName:(NSString *)name; + +- (NSArray *)awaitEvents:(NSUInteger)events name:(NSString *)name; + +@property(nonatomic, strong, readonly) FSTGenericEventHandler handler; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m new file mode 100644 index 0000000..c7e5b41 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.m @@ -0,0 +1,94 @@ +/* + * 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 "FSTEventAccumulator.h" + +#import + +#import "Util/FSTAssert.h" + +#import "XCTestCase+Await.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTEventAccumulator () +- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, weak, readonly) XCTestCase *testCase; + +@property(nonatomic, assign) NSUInteger maxEvents; +@property(nonatomic, strong, nullable) XCTestExpectation *expectation; +@end + +@implementation FSTEventAccumulator { + NSMutableArray *_events; +} + ++ (instancetype)accumulatorForTest:(XCTestCase *)testCase { + return [[FSTEventAccumulator alloc] initForTest:testCase]; +} + +- (instancetype)initForTest:(XCTestCase *)testCase { + if (self = [super init]) { + _testCase = testCase; + _events = [NSMutableArray array]; + } + return self; +} + +- (NSArray *)awaitEvents:(NSUInteger)events name:(NSString *)name { + @synchronized(self) { + FSTAssert(!self.expectation, @"Existing expectation still pending?"); + self.expectation = [self.testCase expectationWithDescription:name]; + self.maxEvents = self.maxEvents + events; + [self checkFulfilled]; + } + + // Don't await within @synchronized block to avoid deadlocking. + [self.testCase awaitExpectations]; + + return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)]; +} + +- (id)awaitEventWithName:(NSString *)name { + NSArray *events = [self awaitEvents:1 name:name]; + return events[0]; +} + +// Overrides the handler property +- (void (^)(id _Nullable, NSError *))handler { + return ^void(id _Nullable value, NSError *error) { + // We can't store nil in the _events array, but these are still interesting to tests so store + // NSNull instead. + id event = value ? value : [NSNull null]; + + @synchronized(self) { + [_events addObject:event]; + [self checkFulfilled]; + } + }; +} + +- (void)checkFulfilled { + if (_events.count >= self.maxEvents) { + [self.expectation fulfill]; + self.expectation = nil; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h new file mode 100644 index 0000000..3facc9c --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -0,0 +1,258 @@ +/* + * 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 + +#import "API/FIRDocumentReference+Internal.h" +#import "Core/FSTTypes.h" +#import "Model/FSTDocumentDictionary.h" +#import "Model/FSTDocumentKeySet.h" + +@class FIRGeoPoint; +@class FSTDeleteMutation; +@class FSTDeletedDocument; +@class FSTDocument; +@class FSTDocumentKeyReference; +@class FSTDocumentSet; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTLocalViewChanges; +@class FSTPatchMutation; +@class FSTQuery; +@class FSTRemoteEvent; +@class FSTResourceName; +@class FSTResourcePath; +@class FSTSetMutation; +@class FSTSnapshotVersion; +@class FSTSortOrder; +@class FSTTargetChange; +@class FSTTimestamp; +@class FSTTransformMutation; +@class FSTView; +@class FSTViewSnapshot; +@class FSTObjectValue; +@protocol FSTFilter; + +NS_ASSUME_NONNULL_BEGIN + +#if __cplusplus +extern "C" { +#endif + +#define FSTAssertIsKindOfClass(value, classType) \ + do { \ + XCTAssertEqualObjects([value class], [classType class]); \ + } while (0); + +/** + * Asserts that the given NSSet of FSTDocumentKeys contains exactly the given expected keys. + * This is a macro instead of a method so that the failure shows up on the right line. + * + * @param actualSet An NSSet of FSTDocumentKeys. + * @param expectedArray A sorted array of keys that actualSet must be equal to (after converting + * to an array and sorting). + */ +#define FSTAssertEqualSets(actualSet, expectedArray) \ + do { \ + NSArray *actual = [(actualSet)allObjects]; \ + actual = [actual sortedArrayUsingSelector:@selector(compare:)]; \ + XCTAssertEqualObjects(actual, (expectedArray)); \ + } while (0) + +/** + * Takes an array of "equality group" arrays and asserts that the compare: selector returns the + * same as compare: on the indexes of the "equality groups" (NSOrderedSame for items in the same + * group). + */ +#define FSTAssertComparisons(values) \ + do { \ + for (NSUInteger i = 0; i < [values count]; i++) { \ + for (id left in values[i]) { \ + for (NSUInteger j = 0; j < [values count]; j++) { \ + for (id right in values[j]) { \ + NSComparisonResult expected = [@(i) compare:@(j)]; \ + NSComparisonResult result = [left compare:right]; \ + NSComparisonResult inverseResult = [right compare:left]; \ + XCTAssertEqual(result, expected, @"comparing %@ with %@ at (%lu, %lu)", left, right, \ + i, j); \ + XCTAssertEqual(inverseResult, -expected, @"comparing %@ with %@ at (%lu, %lu)", right, \ + left, j, i); \ + } \ + } \ + } \ + } \ + } while (0) + +/** + * Takes an array of "equality group" arrays and asserts that the isEqual: selector returns TRUE + * if-and-only-if items are in the same group. + * + * Additionally checks that the hash: selector returns the same value for items in the same group. + */ +#define FSTAssertEqualityGroups(values) \ + do { \ + for (NSUInteger i = 0; i < [values count]; i++) { \ + for (id left in values[i]) { \ + for (NSUInteger j = 0; j < [values count]; j++) { \ + for (id right in values[j]) { \ + if (i == j) { \ + XCTAssertEqualObjects(left, right); \ + XCTAssertEqual([left hash], [right hash], @"comparing hash of %@ with hash of %@", \ + left, right); \ + } else { \ + XCTAssertNotEqualObjects(left, right); \ + } \ + } \ + } \ + } \ + } \ + } while (0) + +// Helper for validating API exceptions. +#define FSTAssertThrows(expression, exceptionReason, ...) \ + ({ \ + BOOL __didThrow = NO; \ + @try { \ + (void)(expression); \ + } @catch (NSException * exception) { \ + __didThrow = YES; \ + XCTAssertEqualObjects(exception.reason, exceptionReason); \ + } \ + XCTAssertTrue(__didThrow, ##__VA_ARGS__); \ + }) + +/** Creates a new FSTTimestamp from components. Note that year, month, and day are all one-based. */ +FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second); + +/** Creates a new NSDate from components. Note that year, month, and day are all one-based. */ +NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second); + +/** + * Creates a new NSData from the var args of bytes, must be terminated with a negative byte + */ +NSData *FSTTestData(int bytes, ...); + +/** Creates a new GeoPoint from the latitude and longitude values */ +FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude); + +/** + * Creates a new NSDateComponents from components. Note that year, month, and day are all + * one-based. + */ +NSDateComponents *FSTTestDateComponents( + int year, int month, int day, int hour, int minute, int second); + +FSTFieldPath *FSTTestFieldPath(NSString *field); + +/** Wraps a plain value into an FSTFieldValue instance. */ +FSTFieldValue *FSTTestFieldValue(id _Nullable value); + +/** Wraps a NSDictionary value into an FSTObjectValue instance. */ +FSTObjectValue *FSTTestObjectValue(NSDictionary *data); + +/** A convenience method for creating document keys for tests. */ +FSTDocumentKey *FSTTestDocKey(NSString *path); + +/** A convenience method for creating a document key set for tests. */ +FSTDocumentKeySet *FSTTestDocKeySet(NSArray *keys); + +/** Allow tests to just use an int literal for versions. */ +typedef int64_t FSTTestSnapshotVersion; + +/** A convenience method for creating snapshot versions for tests. */ +FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion version); + +/** A convenience method for creating docs for tests. */ +FSTDocument *FSTTestDoc(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary *data, + BOOL hasMutations); + +/** A convenience method for creating deleted docs for tests. */ +FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version); + +/** A convenience method for creating resource paths from a path string. */ +FSTResourcePath *FSTTestPath(NSString *path); + +/** + * A convenience method for creating a document reference from a path string. + */ +FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *databaseID, NSString *path); + +/** A convenience method for creating a query for the given path (without any other filters). */ +FSTQuery *FSTTestQuery(NSString *path); + +/** + * A convenience method to create a FSTFilter using a string representation for both field + * and operator (<, <=, ==, >=, >). + */ +id FSTTestFilter(NSString *field, NSString *op, id value); + +/** A convenience method for creating sort orders. */ +FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction); + +/** + * Creates an NSComparator that will compare FSTDocuments by the given fieldPath string then by + * key. + */ +NSComparator FSTTestDocComparator(NSString *fieldPath); + +/** + * Creates a FSTDocumentSet based on the given comparator, initially containing the given + * documents. + */ +FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs); + +/** Computes changes to the view with the docs and then applies them and returns the snapshot. */ +FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, + NSArray *docs, + FSTTargetChange *_Nullable targetChange); + +/** Creates a set mutation for the document key at the given path. */ +FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary *values); + +/** Creates a patch mutation for the document key at the given path. */ +FSTPatchMutation *FSTTestPatchMutation(NSString *path, + NSDictionary *values, + NSArray *_Nullable updateMask); + +FSTTransformMutation *FSTTestTransformMutation(NSString *path, + NSArray *serverTimestampFields); + +/** Creates a delete mutation for the document key at the given path. */ +FSTDeleteMutation *FSTTestDeleteMutation(NSString *path); + +/** Converts a list of documents to a sorted map. */ +FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray *docs); + +/** Creates a remote event with changes to a document. */ +FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + NSArray *updatedInTargets, + NSArray *removedFromTargets); + +/** Creates a test view changes. */ +FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, + NSArray *addedKeys, + NSArray *removedKeys); + +/** Creates a resume token to match the given snapshot version. */ +NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot); + +#if __cplusplus +} // extern "C" +#endif + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m new file mode 100644 index 0000000..3b7f47f --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTHelpers.m @@ -0,0 +1,348 @@ +/* + * 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 "FSTHelpers.h" + +#import "API/FIRFieldPath+Internal.h" +#import "API/FSTUserDataConverter.h" +#import "Core/FSTQuery.h" +#import "Core/FSTSnapshotVersion.h" +#import "Core/FSTTimestamp.h" +#import "Core/FSTView.h" +#import "Firestore/FIRFieldPath.h" +#import "Firestore/FIRGeoPoint.h" +#import "Local/FSTLocalViewChanges.h" +#import "Local/FSTQueryData.h" +#import "Model/FSTDatabaseID.h" +#import "Model/FSTDocument.h" +#import "Model/FSTDocumentKey.h" +#import "Model/FSTDocumentSet.h" +#import "Model/FSTFieldValue.h" +#import "Model/FSTMutation.h" +#import "Model/FSTPath.h" +#import "Remote/FSTRemoteEvent.h" +#import "Remote/FSTWatchChange.h" +#import "Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */ +static NSString *const kDeleteSentinel = @""; + +static const int kMicrosPerSec = 1000000; +static const int kMillisPerSec = 1000; + +FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) { + NSDate *date = FSTTestDate(year, month, day, hour, minute, second); + return [FSTTimestamp timestampWithDate:date]; +} + +NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) { + NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second); + return [[NSCalendar currentCalendar] dateFromComponents:comps]; +} + +NSData *FSTTestData(int bytes, ...) { + va_list args; + va_start(args, bytes); /* Initialize the argument list. */ + + NSMutableData *data = [NSMutableData data]; + + int next = bytes; + while (next >= 0) { + uint8_t byte = (uint8_t)next; + [data appendBytes:&byte length:1]; + next = va_arg(args, int); + } + + va_end(args); + return [data copy]; +} + +FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) { + return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude]; +} + +NSDateComponents *FSTTestDateComponents( + int year, int month, int day, int hour, int minute, int second) { + NSDateComponents *comps = [[NSDateComponents alloc] init]; + comps.year = year; + comps.month = month; + comps.day = day; + comps.hour = hour; + comps.minute = minute; + comps.second = second; + + // Force time zone to UTC to avoid these values changing due to daylight saving. + comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + return comps; +} + +FSTFieldPath *FSTTestFieldPath(NSString *field) { + return [FIRFieldPath pathWithDotSeparatedString:field].internalValue; +} + +FSTFieldValue *FSTTestFieldValue(id _Nullable value) { + FSTDatabaseID *databaseID = + [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID]; + FSTUserDataConverter *converter = + [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID + preConverter:^id _Nullable(id _Nullable input) { + return input; + }]; + // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and + // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent. + return [converter parsedQueryValue:value]; +} + +FSTObjectValue *FSTTestObjectValue(NSDictionary *data) { + FSTFieldValue *wrapped = FSTTestFieldValue(data); + FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data); + return (FSTObjectValue *)wrapped; +} + +FSTDocumentKey *FSTTestDocKey(NSString *path) { + return [FSTDocumentKey keyWithPathString:path]; +} + +FSTDocumentKeySet *FSTTestDocKeySet(NSArray *keys) { + FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + for (FSTDocumentKey *key in keys) { + result = [result setByAddingObject:key]; + } + return result; +} + +FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) { + int64_t seconds = versionMicroseconds / kMicrosPerSec; + int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec; + + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; + return [FSTSnapshotVersion versionWithTimestamp:timestamp]; +} + +FSTDocument *FSTTestDoc(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary *data, + BOOL hasMutations) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path]; + return [FSTDocument documentWithData:FSTTestObjectValue(data) + key:key + version:FSTTestVersion(version) + hasLocalMutations:hasMutations]; +} + +FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path]; + return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; +} + +static NSArray *FSTTestSplitPath(NSString *path) { + if ([path isEqualToString:@""]) { + return @[]; + } else { + return [path componentsSeparatedByString:@"/"]; + } +} + +FSTResourcePath *FSTTestPath(NSString *path) { + return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)]; +} + +FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) { + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID]; +} + +FSTQuery *FSTTestQuery(NSString *path) { + return [FSTQuery queryWithPath:FSTTestPath(path)]; +} + +id FSTTestFilter(NSString *field, NSString *opString, id value) { + FSTFieldPath *path = FSTTestFieldPath(field); + FSTRelationFilterOperator op; + if ([opString isEqualToString:@"<"]) { + op = FSTRelationFilterOperatorLessThan; + } else if ([opString isEqualToString:@"<="]) { + op = FSTRelationFilterOperatorLessThanOrEqual; + } else if ([opString isEqualToString:@"=="]) { + op = FSTRelationFilterOperatorEqual; + } else if ([opString isEqualToString:@">="]) { + op = FSTRelationFilterOperatorGreaterThanOrEqual; + } else if ([opString isEqualToString:@">"]) { + op = FSTRelationFilterOperatorGreaterThan; + } else { + FSTCFail(@"Unsupported operator type: %@", opString); + } + + FSTFieldValue *data = FSTTestFieldValue(value); + if ([data isEqual:[FSTDoubleValue nanValue]]) { + FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN."); + return [[FSTNanFilter alloc] initWithField:path]; + } else if ([data isEqual:[FSTNullValue nullValue]]) { + FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null."); + return [[FSTNullFilter alloc] initWithField:path]; + } else { + return [FSTRelationFilter filterWithField:path filterOperator:op value:data]; + } +} + +FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) { + FSTFieldPath *path = FSTTestFieldPath(field); + BOOL ascending; + if ([direction isEqualToString:@"asc"]) { + ascending = YES; + } else if ([direction isEqualToString:@"desc"]) { + ascending = NO; + } else { + FSTCFail(@"Unsupported direction: %@", direction); + } + return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending]; +} + +NSComparator FSTTestDocComparator(NSString *fieldPath) { + FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"docs" ]]] + queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath) + ascending:YES]]; + return [query comparator]; +} + +FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs) { + FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp]; + for (FSTDocument *doc in docs) { + docSet = [docSet documentSetByAddingDocument:doc]; + } + return docSet; +} + +FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary *values) { + return [[FSTSetMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path] + value:FSTTestObjectValue(values) + precondition:[FSTPrecondition none]]; +} + +FSTPatchMutation *FSTTestPatchMutation(NSString *path, + NSDictionary *values, + NSArray *_Nullable updateMask) { + BOOL merge = updateMask != nil; + + __block FSTObjectValue *objectValue = [FSTObjectValue objectValue]; + NSMutableArray *fieldMaskPaths = [NSMutableArray array]; + [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldPath *path = FSTTestFieldPath(key); + [fieldMaskPaths addObject:path]; + if (![value isEqual:kDeleteSentinel]) { + FSTFieldValue *parsedValue = FSTTestFieldValue(value); + objectValue = [objectValue objectBySettingValue:parsedValue forPath:path]; + } + }]; + + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths]; + return [[FSTPatchMutation alloc] initWithKey:key + fieldMask:mask + value:objectValue + precondition:[FSTPrecondition preconditionWithExists:YES]]; +} + +// For now this only creates TransformMutations with server timestamps. +FSTTransformMutation *FSTTestTransformMutation(NSString *path, + NSArray *serverTimestampFields) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)]; + NSMutableArray *fieldTransforms = [NSMutableArray array]; + for (NSString *field in serverTimestampFields) { + FSTFieldPath *fieldPath = FSTTestFieldPath(field); + id transformOp = [FSTServerTimestampTransform serverTimestampTransform]; + FSTFieldTransform *transform = + [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp]; + [fieldTransforms addObject:transform]; + } + return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms]; +} + +FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) { + return [[FSTDeleteMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path] + precondition:[FSTPrecondition none]]; +} + +FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray *docs) { + FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + for (FSTMaybeDocument *doc in docs) { + updates = [updates dictionaryBySettingObject:doc forKey:doc.key]; + } + return updates; +} + +FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, + NSArray *docs, + FSTTargetChange *_Nullable targetChange) { + return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] + targetChange:targetChange] + .snapshot; +} + +FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + NSArray *updatedInTargets, + NSArray *removedFromTargets) { + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets + removedTargetIDs:removedFromTargets + documentKey:doc.key + document:doc]; + NSMutableDictionary *listens = [NSMutableDictionary dictionary]; + FSTQueryData *dummyQueryData = [FSTQueryData alloc]; + for (NSNumber *targetID in updatedInTargets) { + listens[targetID] = dummyQueryData; + } + for (NSNumber *targetID in removedFromTargets) { + listens[targetID] = dummyQueryData; + } + NSMutableDictionary *pending = [NSMutableDictionary dictionary]; + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version + listenTargets:listens + pendingTargetResponses:pending]; + [aggregator addWatchChange:change]; + return [aggregator remoteEvent]; +} + +/** Creates a resume token to match the given snapshot version. */ +NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) { + if (snapshotVersion == 0) { + return nil; + } + + NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion]; + return [snapshotString dataUsingEncoding:NSUTF8StringEncoding]; +} + +FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, + NSArray *addedKeys, + NSArray *removedKeys) { + FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; + for (NSString *keyPath in addedKeys) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath]; + added = [added setByAddingObject:key]; + } + FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; + for (NSString *keyPath in removedKeys) { + FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath]; + removed = [removed setByAddingObject:key]; + } + return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h new file mode 100644 index 0000000..cefd669 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -0,0 +1,94 @@ +/* + * 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 +#import + +#import "XCTestCase+Await.h" + +@class FIRCollectionReference; +@class FIRDocumentSnapshot; +@class FIRDocumentReference; +@class FIRQuerySnapshot; +@class FIRFirestore; +@class FIRFirestoreSettings; +@class FIRQuery; +@class FSTEventAccumulator; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTIntegrationTestCase : XCTestCase + +/** Returns the default Firestore project ID for testing. */ ++ (NSString *)projectID; + +/** Returns a FirestoreSettings configured to use either hexa or the emulator. */ ++ (FIRFirestoreSettings *)settings; + +/** Returns a new Firestore connected to the "test-db" project. */ +- (FIRFirestore *)firestore; + +/** Returns a new Firestore connected to the project with the given projectID. */ +- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID; + +/** Synchronously shuts down the given firestore. */ +- (void)shutdownFirestore:(FIRFirestore *)firestore; + +- (NSString *)documentPath; + +- (FIRDocumentReference *)documentRef; + +- (FIRCollectionReference *)collectionRef; + +- (FIRCollectionReference *)collectionRefWithDocuments: + (NSDictionary *> *)documents; + +- (void)writeAllDocuments:(NSDictionary *> *)documents + toCollection:(FIRCollectionReference *)collection; + +- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path, + FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef))action; + +- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref; + +- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query; + +- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data; + +- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data; + +- (void)deleteDocumentRef:(FIRDocumentReference *)ref; + +/** + * "Blocks" the current thread/run loop until the block returns YES. + * Should only be called on the main thread. + * The block is invoked frequently and in a loop (every couple of millseconds) to ensure fast + * test progress and make sure actions to be run on main thread are not blocked by this method. + */ +- (void)waitUntil:(BOOL (^)())predicate; + +@property(nonatomic, strong) FIRFirestore *db; +@property(nonatomic, strong) FSTEventAccumulator *eventAccumulator; +@end + +/** Converts the FIRQuerySnapshot to an NSArray containing the data of the documents in order. */ +NSArray *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs); + +/** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */ +NSArray *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m new file mode 100644 index 0000000..87a78c3 --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.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 Firestore; + +#import "FSTIntegrationTestCase.h" + +#import +#import +#import + +#import "API/FIRFirestore+Internal.h" +#import "Auth/FSTEmptyCredentialsProvider.h" +#import "Local/FSTLevelDB.h" +#import "Model/FSTDatabaseID.h" +#import "Util/FSTDispatchQueue.h" +#import "Util/FSTUtil.h" + +#import "FSTEventAccumulator.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTIntegrationTestCase { + NSMutableArray *_firestores; +} + +- (void)setUp { + [super setUp]; + + [self clearPersistence]; + + _firestores = [NSMutableArray array]; + self.db = [self firestore]; + self.eventAccumulator = [FSTEventAccumulator accumulatorForTest:self]; +} + +- (void)tearDown { + @try { + for (FIRFirestore *firestore in _firestores) { + [self shutdownFirestore:firestore]; + } + } @finally { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [GRPCCall closeOpenConnections]; +#pragma clang diagnostic pop + _firestores = nil; + [super tearDown]; + } +} + +- (void)clearPersistence { + NSString *levelDBDir = [FSTLevelDB documentsDirectory]; + NSError *error; + if (![[NSFileManager defaultManager] removeItemAtPath:levelDBDir error:&error]) { + // file not found is okay. + XCTAssertTrue( + [error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError, + @"Failed to clear LevelDB Persistence: %@", error); + } +} + +- (FIRFirestore *)firestore { + return [self firestoreWithProjectID:[FSTIntegrationTestCase projectID]]; +} + ++ (NSString *)projectID { + NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; + if (!project) { + project = @"test-db"; + } + return project; +} + ++ (FIRFirestoreSettings *)settings { + FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init]; + NSString *host = [[NSProcessInfo processInfo] environment][@"DATASTORE_HOST"]; + settings.sslEnabled = YES; + if (!host) { + // If host is nil, there is no GoogleService-Info.plist. Check if a hexa integration test + // configuration is configured. The first bundle location is used by bazel builds. The + // second is used for github clones. + host = @"localhost:8081"; + settings.sslEnabled = YES; + NSString *certsPath = + [[NSBundle mainBundle] pathForResource:@"PlugIns/IntegrationTests.xctest/CAcert" + ofType:@"pem"]; + if (certsPath == nil) { + certsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"CAcert" ofType:@"pem"]; + } + unsigned long long fileSize = + [[[NSFileManager defaultManager] attributesOfItemAtPath:certsPath error:nil] fileSize]; + + if (fileSize == 0) { + NSLog( + @"The cert is not properly configured. Make sure setup_integration_tests.py " + "has been run."); + } + [GRPCCall useTestCertsPath:certsPath testName:@"test_cert_2" forHost:host]; + } + settings.host = host; + settings.persistenceEnabled = YES; + NSLog(@"Configured integration test for %@ with SSL: %@", settings.host, + settings.sslEnabled ? @"YES" : @"NO"); + return settings; +} + +- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID { + NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count]; + + FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; + + FSTEmptyCredentialsProvider *credentialsProvider = [[FSTEmptyCredentialsProvider alloc] init]; + + FIRSetLoggerLevel(FIRLoggerLevelDebug); + // HACK: FIRFirestore expects a non-nil app, but for tests we cheat. + FIRApp *app = nil; + FIRFirestore *firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:kDefaultDatabaseID + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + + firestore.settings = [FSTIntegrationTestCase settings]; + + [_firestores addObject:firestore]; + return firestore; +} + +- (void)shutdownFirestore:(FIRFirestore *)firestore { + XCTestExpectation *shutdownCompletion = [self expectationWithDescription:@"shutdown"]; + [firestore shutdownWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [shutdownCompletion fulfill]; + }]; + [self awaitExpectations]; +} + +- (NSString *)documentPath { + return [@"test-collection/" stringByAppendingString:[FSTUtil autoID]]; +} + +- (FIRDocumentReference *)documentRef { + return [self.db documentWithPath:[self documentPath]]; +} + +- (FIRCollectionReference *)collectionRef { + NSString *collectionName = [@"test-collection-" stringByAppendingString:[FSTUtil autoID]]; + return [self.db collectionWithPath:collectionName]; +} + +- (FIRCollectionReference *)collectionRefWithDocuments: + (NSDictionary *> *)documents { + FIRCollectionReference *collection = [self collectionRef]; + // Use a different instance to write the documents + [self writeAllDocuments:documents + toCollection:[[self firestore] collectionWithPath:collection.path]]; + return collection; +} + +- (void)writeAllDocuments:(NSDictionary *> *)documents + toCollection:(FIRCollectionReference *)collection { + [documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary *value, + BOOL *stop) { + FIRDocumentReference *ref = [collection documentWithPath:key]; + [self writeDocumentRef:ref data:value]; + }]; +} + +- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path, + FIRDocumentReference *readerRef, + FIRDocumentReference *writerRef))action { + FIRFirestore *reader = self.db; // for clarity + FIRFirestore *writer = [self firestore]; + + NSString *path = [self documentPath]; + FIRDocumentReference *readerRef = [reader documentWithPath:path]; + FIRDocumentReference *writerRef = [writer documentWithPath:path]; + action(path, readerRef, writerRef); +} + +- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref { + __block FIRDocumentSnapshot *result; + + XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; + [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) { + XCTAssertNil(error); + result = doc; + [expectation fulfill]; + }]; + [self awaitExpectations]; + + return result; +} + +- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query { + __block FIRQuerySnapshot *result; + + XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; + [query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error) { + XCTAssertNil(error); + result = documentSet; + [expectation fulfill]; + }]; + [self awaitExpectations]; + + return result; +} + +- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { + XCTestExpectation *expectation = [self expectationWithDescription:@"setData"]; + [ref setData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { + XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"]; + [ref updateData:data + completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)deleteDocumentRef:(FIRDocumentReference *)ref { + XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"]; + [ref deleteDocumentWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; +} + +- (void)waitUntil:(BOOL (^)())predicate { + NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; + double waitSeconds = [self defaultExpectationWaitSeconds]; + while (!predicate() && ([NSDate timeIntervalSinceReferenceDate] - start < waitSeconds)) { + // This waits for the next event or until the 100ms timeout is reached + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + if (!predicate()) { + XCTFail(@"Timeout"); + } +} + +NSArray *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs) { + NSMutableArray *> *result = [NSMutableArray array]; + for (FIRDocumentSnapshot *doc in docs.documents) { + [result addObject:doc.data]; + } + return result; +} + +NSArray *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) { + NSMutableArray *result = [NSMutableArray array]; + for (FIRDocumentSnapshot *doc in docs.documents) { + [result addObject:doc.documentID]; + } + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTUtilTests.m b/Firestore/Example/Tests/Util/FSTUtilTests.m new file mode 100644 index 0000000..998832d --- /dev/null +++ b/Firestore/Example/Tests/Util/FSTUtilTests.m @@ -0,0 +1,35 @@ +/* + * 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 "Util/FSTUtil.h" + +#import + +@interface FSTUtilTests : XCTestCase +@end + +@implementation FSTUtilTests + +- (void)testAutoID { + NSString *autoID = [FSTUtil autoID]; + XCTAssertEqual([autoID length], 20); + for (NSUInteger i = 0; i < 20; i++) { + unichar c = [autoID characterAtIndex:i]; + XCTAssert(c >= ' ' && c <= '~', @"Should be printable ascii characters."); + } +} + +@end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.h b/Firestore/Example/Tests/Util/XCTestCase+Await.h new file mode 100644 index 0000000..9d575f9 --- /dev/null +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.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 + +@interface XCTestCase (Await) + +/** + * Await all outstanding expectations with a reasonable timeout, and if any of them fail, XCTFail + * the test. + */ +- (void)awaitExpectations; + +/** + * Returns a reasonable timeout for testing against Firestore. + */ +- (double)defaultExpectationWaitSeconds; + +@end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m new file mode 100644 index 0000000..e200c8c --- /dev/null +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.m @@ -0,0 +1,38 @@ +/* + * 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 "XCTestCase+Await.h" + +#import + +static const double kExpectationWaitSeconds = 10.0; + +@implementation XCTestCase (Await) + +- (void)awaitExpectations { + [self waitForExpectationsWithTimeout:kExpectationWaitSeconds + handler:^(NSError *_Nullable expectationError) { + if (expectationError) { + XCTFail(@"Error waiting for timeout: %@", expectationError); + } + }]; +} + +- (double)defaultExpectationWaitSeconds { + return kExpectationWaitSeconds; +} + +@end diff --git a/Firestore/Example/Tests/en.lproj/InfoPlist.strings b/Firestore/Example/Tests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Firestore/Example/Tests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Firestore/Firestore.podspec b/Firestore/Firestore.podspec new file mode 100644 index 0000000..efd3f64 --- /dev/null +++ b/Firestore/Firestore.podspec @@ -0,0 +1,44 @@ +# +# Be sure to run `pod lib lint Firestore.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'Firestore' + s.version = '0.1.0' + s.summary = 'Google Cloud Firestore for iOS' + + s.description = <<-DESC +Google Cloud Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development. + DESC + + s.homepage = 'https://developers.google.com/' + s.license = { :type => 'Apache', :file => '../LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { :git => 'https://github.com/TBD/Firestore.git', :tag => s.version.to_s } + # s.social_media_url = 'https://twitter.com/' + + s.ios.deployment_target = '8.0' + + s.source_files = 'Source/**/*', 'Port/**/*', 'Protos/objc/**/*.[hm]', 'third_party/**/*.[mh]' + s.requires_arc = 'Source/**/*', 'third_party/**/*.[mh]' + s.exclude_files = 'Port/*test.cc', 'third_party/**/Tests/**' + s.public_header_files = 'Source/Public/*.h' + s.frameworks = 'MobileCoreServices' + s.dependency 'gRPC-ProtoRPC' + s.dependency 'leveldb-library' + s.dependency 'Protobuf' + s.dependency 'FirebaseCommunity/Core' + s.dependency 'FirebaseCommunity/Auth' + s.library = 'c++' + + s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => + '$(inherited) ' + + 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ', + 'OTHER_CFLAGS' => '-DFIRFirestore_VERSION=' + s.version.to_s + } +end diff --git a/Firestore/Port/absl/absl_attributes.h b/Firestore/Port/absl/absl_attributes.h new file mode 100644 index 0000000..d43930c --- /dev/null +++ b/Firestore/Port/absl/absl_attributes.h @@ -0,0 +1,644 @@ +/* + * 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. + */ + +// Various macros for C++ attributes +// Most macros here are exposing GCC or Clang features, and are stubbed out for +// other compilers. +// GCC attributes documentation: +// https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Function-Attributes.html +// https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Variable-Attributes.html +// https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Type-Attributes.html +// +// Most attributes in this file are already supported by GCC 4.7. +// However, some of them are not supported in older version of Clang. +// Thus, we check __has_attribute() first. If the check fails, we check if we +// are on GCC and assume the attribute exists on GCC (which is verified on GCC +// 4.7). +// +// For sanitizer-related attributes, define the following macros +// using -D along with the given value for -fsanitize: +// - ADDRESS_SANITIZER with -fsanitize=address (GCC 4.8+, Clang) +// - MEMORY_SANITIZER with -fsanitize=memory (Clang) +// - THREAD_SANITIZER with -fsanitize=thread (GCC 4.8+, Clang) +// - UNDEFINED_BEHAVIOR_SANITIZER with -fsanitize=undefined (GCC 4.9+, Clang) +// - CONTROL_FLOW_INTEGRITY with -fsanitize=cfi (Clang) +// Since these are only supported by GCC and Clang now, we only check for +// __GNUC__ (GCC or Clang) and the above macros. +#ifndef THIRD_PARTY_ABSL_BASE_ATTRIBUTES_H_ +#define THIRD_PARTY_ABSL_BASE_ATTRIBUTES_H_ + +// ABSL_HAVE_ATTRIBUTE is a function-like feature checking macro. +// It's a wrapper around __has_attribute, which is defined by GCC 5+ and Clang. +// It evaluates to a nonzero constant integer if the attribute is supported +// or 0 if not. +// It evaluates to zero if __has_attribute is not defined by the compiler. +// GCC: https://gcc.gnu.org/gcc-5/changes.html +// Clang: https://clang.llvm.org/docs/LanguageExtensions.html +#ifdef __has_attribute +#define ABSL_HAVE_ATTRIBUTE(x) __has_attribute(x) +#else +#define ABSL_HAVE_ATTRIBUTE(x) 0 +#endif + +// ABSL_HAVE_CPP_ATTRIBUTE is a function-like feature checking macro that +// accepts C++11 style attributes. It's a wrapper around __has_cpp_attribute, +// defined by ISO C++ SD-6 +// (http://en.cppreference.com/w/cpp/experimental/feature_test). If we don't +// find __has_cpp_attribute, will evaluate to 0. +#if defined(__cplusplus) && defined(__has_cpp_attribute) +// NOTE: requiring __cplusplus above should not be necessary, but +// works around https://bugs.llvm.org/show_bug.cgi?id=23435. +#define ABSL_HAVE_CPP_ATTRIBUTE(x) __has_cpp_attribute(x) +#else +#define ABSL_HAVE_CPP_ATTRIBUTE(x) 0 +#endif + +// ----------------------------------------------------------------------------- +// Function Attributes +// ----------------------------------------------------------------------------- +// GCC: https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html +// Clang: https://clang.llvm.org/docs/AttributeReference.html + +// PRINTF_ATTRIBUTE, SCANF_ATTRIBUTE +// Tell the compiler to do printf format std::string checking if the +// compiler supports it; see the 'format' attribute in +// . +// +// N.B.: As the GCC manual states, "[s]ince non-static C++ methods +// have an implicit 'this' argument, the arguments of such methods +// should be counted from two, not one." +#if ABSL_HAVE_ATTRIBUTE(format) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_PRINTF_ATTRIBUTE(string_index, first_to_check) \ + __attribute__((__format__(__printf__, string_index, first_to_check))) +#define ABSL_SCANF_ATTRIBUTE(string_index, first_to_check) \ + __attribute__((__format__(__scanf__, string_index, first_to_check))) +#else +#define ABSL_PRINTF_ATTRIBUTE(string_index, first_to_check) +#define ABSL_SCANF_ATTRIBUTE(string_index, first_to_check) +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(format) || (defined(__GNUC__) && !defined(__clang__)) +#define PRINTF_ATTRIBUTE(string_index, first_to_check) \ + __attribute__((__format__(__printf__, string_index, first_to_check))) +#define SCANF_ATTRIBUTE(string_index, first_to_check) \ + __attribute__((__format__(__scanf__, string_index, first_to_check))) +#else +#define PRINTF_ATTRIBUTE(string_index, first_to_check) +#define SCANF_ATTRIBUTE(string_index, first_to_check) +#endif + +// ATTRIBUTE_ALWAYS_INLINE, ATTRIBUTE_NOINLINE +// For functions we want to force inline or not inline. +// Introduced in gcc 3.1. +#if ABSL_HAVE_ATTRIBUTE(always_inline) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_ALWAYS_INLINE __attribute__((always_inline)) +#define ABSL_HAVE_ATTRIBUTE_ALWAYS_INLINE 1 +#else +#define ABSL_ATTRIBUTE_ALWAYS_INLINE +#endif + +#if ABSL_HAVE_ATTRIBUTE(noinline) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_NOINLINE __attribute__((noinline)) +#define ABSL_HAVE_ATTRIBUTE_NOINLINE 1 +#else +#define ABSL_ATTRIBUTE_NOINLINE +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(always_inline) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_ALWAYS_INLINE __attribute__((always_inline)) +#define HAVE_ATTRIBUTE_ALWAYS_INLINE 1 +#else +#define ATTRIBUTE_ALWAYS_INLINE +#endif + +#if ABSL_HAVE_ATTRIBUTE(noinline) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_NOINLINE __attribute__((noinline)) +#define HAVE_ATTRIBUTE_NOINLINE 1 +#else +#define ATTRIBUTE_NOINLINE +#endif + +// ATTRIBUTE_NO_TAIL_CALL +// Prevent the compiler from optimizing away stack frames for functions which +// end in a call to another function. +#if ABSL_HAVE_ATTRIBUTE(disable_tail_calls) +#define ABSL_HAVE_ATTRIBUTE_NO_TAIL_CALL 1 +#define ABSL_ATTRIBUTE_NO_TAIL_CALL __attribute__((disable_tail_calls)) +#elif defined(__GNUC__) && !defined(__clang__) +#define ABSL_HAVE_ATTRIBUTE_NO_TAIL_CALL 1 +#define ABSL_ATTRIBUTE_NO_TAIL_CALL __attribute__((optimize("no-optimize-sibling-calls"))) +#else +#define ABSL_ATTRIBUTE_NO_TAIL_CALL +#define ABSL_HAVE_ATTRIBUTE_NO_TAIL_CALL 0 +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(disable_tail_calls) +#define HAVE_ATTRIBUTE_NO_TAIL_CALL 1 +#define ATTRIBUTE_NO_TAIL_CALL __attribute__((disable_tail_calls)) +#elif defined(__GNUC__) && !defined(__clang__) +#define HAVE_ATTRIBUTE_NO_TAIL_CALL 1 +#define ATTRIBUTE_NO_TAIL_CALL __attribute__((optimize("no-optimize-sibling-calls"))) +#else +#define ATTRIBUTE_NO_TAIL_CALL +#define HAVE_ATTRIBUTE_NO_TAIL_CALL 0 +#endif + +// ATTRIBUTE_WEAK +// For weak functions +#if ABSL_HAVE_ATTRIBUTE(weak) || (defined(__GNUC__) && !defined(__clang__)) +#undef ABSL_ATTRIBUTE_WEAK +#define ABSL_ATTRIBUTE_WEAK __attribute__((weak)) +#define ABSL_HAVE_ATTRIBUTE_WEAK 1 +#else +#define ABSL_ATTRIBUTE_WEAK +#define ABSL_HAVE_ATTRIBUTE_WEAK 0 +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(weak) || (defined(__GNUC__) && !defined(__clang__)) +#undef ATTRIBUTE_WEAK +#define ATTRIBUTE_WEAK __attribute__((weak)) +#define HAVE_ATTRIBUTE_WEAK 1 +#else +#define ATTRIBUTE_WEAK +#define HAVE_ATTRIBUTE_WEAK 0 +#endif + +// ATTRIBUTE_NONNULL +// Tell the compiler either that a particular function parameter +// should be a non-null pointer, or that all pointer arguments should +// be non-null. +// +// Note: As the GCC manual states, "[s]ince non-static C++ methods +// have an implicit 'this' argument, the arguments of such methods +// should be counted from two, not one." +// +// Args are indexed starting at 1. +// For non-static class member functions, the implicit "this" argument +// is arg 1, and the first explicit argument is arg 2. +// For static class member functions, there is no implicit "this", and +// the first explicit argument is arg 1. +// +// /* arg_a cannot be null, but arg_b can */ +// void Function(void* arg_a, void* arg_b) ATTRIBUTE_NONNULL(1); +// +// class C { +// /* arg_a cannot be null, but arg_b can */ +// void Method(void* arg_a, void* arg_b) ATTRIBUTE_NONNULL(2); +// +// /* arg_a cannot be null, but arg_b can */ +// static void StaticMethod(void* arg_a, void* arg_b) ATTRIBUTE_NONNULL(1); +// }; +// +// If no arguments are provided, then all pointer arguments should be non-null. +// +// /* No pointer arguments may be null. */ +// void Function(void* arg_a, void* arg_b, int arg_c) ATTRIBUTE_NONNULL(); +// +// NOTE: The GCC nonnull attribute actually accepts a list of arguments, but +// ATTRIBUTE_NONNULL does not. +#if ABSL_HAVE_ATTRIBUTE(nonnull) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_NONNULL(arg_index) __attribute__((nonnull(arg_index))) +#else +#define ABSL_ATTRIBUTE_NONNULL(...) +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(nonnull) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_NONNULL(arg_index) __attribute__((nonnull(arg_index))) +#else +#define ATTRIBUTE_NONNULL(...) +#endif + +// ATTRIBUTE_NORETURN +// Tell the compiler that a given function never returns +#if ABSL_HAVE_ATTRIBUTE(noreturn) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_NORETURN __attribute__((noreturn)) +#elif defined(_MSC_VER) +#define ABSL_ATTRIBUTE_NORETURN __declspec(noreturn) +#else +#define ABSL_ATTRIBUTE_NORETURN +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(noreturn) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_NORETURN __attribute__((noreturn)) +#elif defined(_MSC_VER) +#define ATTRIBUTE_NORETURN __declspec(noreturn) +#else +#define ATTRIBUTE_NORETURN +#endif + +// ATTRIBUTE_NO_SANITIZE_ADDRESS +// Tell AddressSanitizer (or other memory testing tools) to ignore a given +// function. Useful for cases when a function reads random locations on stack, +// calls _exit from a cloned subprocess, deliberately accesses buffer +// out of bounds or does other scary things with memory. +// NOTE: GCC supports AddressSanitizer(asan) since 4.8. +// https://gcc.gnu.org/gcc-4.8/changes.html +#if defined(__GNUC__) && defined(ADDRESS_SANITIZER) +#define ABSL_ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address)) +#else +#define ABSL_ATTRIBUTE_NO_SANITIZE_ADDRESS +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if defined(__GNUC__) && defined(ADDRESS_SANITIZER) +#define ATTRIBUTE_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address)) +#else +#define ATTRIBUTE_NO_SANITIZE_ADDRESS +#endif + +// ATTRIBUTE_NO_SANITIZE_MEMORY +// Tell MemorySanitizer to relax the handling of a given function. All "Use of +// uninitialized value" warnings from such functions will be suppressed, and all +// values loaded from memory will be considered fully initialized. +// This is similar to the ADDRESS_SANITIZER attribute above, but deals with +// initializedness rather than addressability issues. +// NOTE: MemorySanitizer(msan) is supported by Clang but not GCC. +#if defined(__GNUC__) && defined(MEMORY_SANITIZER) +#define ABSL_ATTRIBUTE_NO_SANITIZE_MEMORY __attribute__((no_sanitize_memory)) +#else +#define ABSL_ATTRIBUTE_NO_SANITIZE_MEMORY +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if defined(__GNUC__) && defined(MEMORY_SANITIZER) +#define ATTRIBUTE_NO_SANITIZE_MEMORY __attribute__((no_sanitize_memory)) +#else +#define ATTRIBUTE_NO_SANITIZE_MEMORY +#endif + +// ATTRIBUTE_NO_SANITIZE_THREAD +// Tell ThreadSanitizer to not instrument a given function. +// If you are adding this attribute, please cc dynamic-tools@ on the cl. +// NOTE: GCC supports ThreadSanitizer(tsan) since 4.8. +// https://gcc.gnu.org/gcc-4.8/changes.html +#if defined(__GNUC__) && defined(THREAD_SANITIZER) +#define ABSL_ATTRIBUTE_NO_SANITIZE_THREAD __attribute__((no_sanitize_thread)) +#else +#define ABSL_ATTRIBUTE_NO_SANITIZE_THREAD +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if defined(__GNUC__) && defined(THREAD_SANITIZER) +#define ATTRIBUTE_NO_SANITIZE_THREAD __attribute__((no_sanitize_thread)) +#else +#define ATTRIBUTE_NO_SANITIZE_THREAD +#endif + +// ATTRIBUTE_NO_SANITIZE_UNDEFINED +// Tell UndefinedSanitizer to ignore a given function. Useful for cases +// where certain behavior (eg. devision by zero) is being used intentionally. +// NOTE: GCC supports UndefinedBehaviorSanitizer(ubsan) since 4.9. +// https://gcc.gnu.org/gcc-4.9/changes.html +#if defined(__GNUC__) && defined(UNDEFINED_BEHAVIOR_SANITIZER) +#define ABSL_ATTRIBUTE_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize("undefined"))) +#else +#define ABSL_ATTRIBUTE_NO_SANITIZE_UNDEFINED +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if defined(__GNUC__) && defined(UNDEFINED_BEHAVIOR_SANITIZER) +#define ATTRIBUTE_NO_SANITIZE_UNDEFINED __attribute__((no_sanitize("undefined"))) +#else +#define ATTRIBUTE_NO_SANITIZE_UNDEFINED +#endif + +// ATTRIBUTE_NO_SANITIZE_CFI +// Tell ControlFlowIntegrity sanitizer to not instrument a given function. +#if defined(__GNUC__) && defined(CONTROL_FLOW_INTEGRITY) +#define ABSL_ATTRIBUTE_NO_SANITIZE_CFI __attribute__((no_sanitize("cfi"))) +#else +#define ABSL_ATTRIBUTE_NO_SANITIZE_CFI +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if defined(__GNUC__) && defined(CONTROL_FLOW_INTEGRITY) +#define ATTRIBUTE_NO_SANITIZE_CFI __attribute__((no_sanitize("cfi"))) +#else +#define ATTRIBUTE_NO_SANITIZE_CFI +#endif + +// ATTRIBUTE_SECTION +// Labeled sections are not supported on Darwin/iOS. +#ifdef ABSL_HAVE_ATTRIBUTE_SECTION +#error ABSL_HAVE_ATTRIBUTE_SECTION cannot be directly set +#elif (ABSL_HAVE_ATTRIBUTE(section) || (defined(__GNUC__) && !defined(__clang__))) && \ + !(defined(__APPLE__) && defined(__MACH__)) +#define ABSL_HAVE_ATTRIBUTE_SECTION 1 +// +// Tell the compiler/linker to put a given function into a section and define +// "__start_ ## name" and "__stop_ ## name" symbols to bracket the section. +// This functionality is supported by GNU linker. +// Any function with ATTRIBUTE_SECTION must not be inlined, or it will +// be placed into whatever section its caller is placed into. +// +#ifndef ABSL_ATTRIBUTE_SECTION +#define ABSL_ATTRIBUTE_SECTION(name) __attribute__((section(#name))) __attribute__((noinline)) +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#ifndef ATTRIBUTE_SECTION +#define ATTRIBUTE_SECTION(name) __attribute__((section(#name))) __attribute__((noinline)) +#endif + +// Tell the compiler/linker to put a given variable into a section and define +// "__start_ ## name" and "__stop_ ## name" symbols to bracket the section. +// This functionality is supported by GNU linker. +#ifndef ABSL_ATTRIBUTE_SECTION_VARIABLE +#define ABSL_ATTRIBUTE_SECTION_VARIABLE(name) __attribute__((section(#name))) +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#ifndef ATTRIBUTE_SECTION_VARIABLE +#define ATTRIBUTE_SECTION_VARIABLE(name) __attribute__((section(#name))) +#endif + +// +// Weak section declaration to be used as a global declaration +// for ATTRIBUTE_SECTION_START|STOP(name) to compile and link +// even without functions with ATTRIBUTE_SECTION(name). +// DEFINE_ATTRIBUTE_SECTION should be in the exactly one file; it's +// a no-op on ELF but not on Mach-O. +// +#ifndef ABSL_DECLARE_ATTRIBUTE_SECTION_VARS +#define ABSL_DECLARE_ATTRIBUTE_SECTION_VARS(name) \ + extern char __start_##name[] ATTRIBUTE_WEAK; \ + extern char __stop_##name[] ATTRIBUTE_WEAK +#endif +#ifndef ABSL_DEFINE_ATTRIBUTE_SECTION_VARS +#define ABSL_INIT_ATTRIBUTE_SECTION_VARS(name) +#define ABSL_DEFINE_ATTRIBUTE_SECTION_VARS(name) +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#ifndef DECLARE_ATTRIBUTE_SECTION_VARS +#define DECLARE_ATTRIBUTE_SECTION_VARS(name) \ + extern char __start_##name[] ATTRIBUTE_WEAK; \ + extern char __stop_##name[] ATTRIBUTE_WEAK +#endif +#ifndef DEFINE_ATTRIBUTE_SECTION_VARS +#define INIT_ATTRIBUTE_SECTION_VARS(name) +#define DEFINE_ATTRIBUTE_SECTION_VARS(name) +#endif + +// +// Return void* pointers to start/end of a section of code with +// functions having ATTRIBUTE_SECTION(name). +// Returns 0 if no such functions exits. +// One must DECLARE_ATTRIBUTE_SECTION_VARS(name) for this to compile and link. +// +#define ABSL_ATTRIBUTE_SECTION_START(name) (reinterpret_cast(__start_##name)) +#define ABSL_ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast(__stop_##name)) + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#define ATTRIBUTE_SECTION_START(name) (reinterpret_cast(__start_##name)) +#define ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast(__stop_##name)) + +#else // !ABSL_HAVE_ATTRIBUTE_SECTION + +#define ABSL_HAVE_ATTRIBUTE_SECTION 0 + +// provide dummy definitions +#define ABSL_ATTRIBUTE_SECTION(name) +#define ABSL_ATTRIBUTE_SECTION_VARIABLE(name) +#define ABSL_INIT_ATTRIBUTE_SECTION_VARS(name) +#define ABSL_DEFINE_ATTRIBUTE_SECTION_VARS(name) +#define ABSL_DECLARE_ATTRIBUTE_SECTION_VARS(name) +#define ABSL_ATTRIBUTE_SECTION_START(name) (reinterpret_cast(0)) +#define ABSL_ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast(0)) + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#define ATTRIBUTE_SECTION(name) +#define ATTRIBUTE_SECTION_VARIABLE(name) +#define INIT_ATTRIBUTE_SECTION_VARS(name) +#define DEFINE_ATTRIBUTE_SECTION_VARS(name) +#define DECLARE_ATTRIBUTE_SECTION_VARS(name) +#define ATTRIBUTE_SECTION_START(name) (reinterpret_cast(0)) +#define ATTRIBUTE_SECTION_STOP(name) (reinterpret_cast(0)) + +#endif // ATTRIBUTE_SECTION + +// ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +// Support for aligning the stack on 32-bit x86. +#if ABSL_HAVE_ATTRIBUTE(force_align_arg_pointer) || (defined(__GNUC__) && !defined(__clang__)) +#if defined(__i386__) +#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC __attribute__((force_align_arg_pointer)) +#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (0) +#elif defined(__x86_64__) +#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (1) +#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +#else // !__i386__ && !__x86_64 +#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (0) +#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +#endif // __i386__ +#else +#define ABSL_ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +#define ABSL_REQUIRE_STACK_ALIGN_TRAMPOLINE (0) +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(force_align_arg_pointer) || (defined(__GNUC__) && !defined(__clang__)) +#if defined(__i386__) +#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC __attribute__((force_align_arg_pointer)) +#define REQUIRE_STACK_ALIGN_TRAMPOLINE (0) +#elif defined(__x86_64__) +#define REQUIRE_STACK_ALIGN_TRAMPOLINE (1) +#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +#else // !__i386__ && !__x86_64 +#define REQUIRE_STACK_ALIGN_TRAMPOLINE (0) +#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +#endif // __i386__ +#else +#define ATTRIBUTE_STACK_ALIGN_FOR_OLD_LIBC +#define REQUIRE_STACK_ALIGN_TRAMPOLINE (0) +#endif + +// MUST_USE_RESULT +// Tell the compiler to warn about unused return values for functions declared +// with this macro. The macro must appear as the very first part of a function +// declaration or definition: +// +// MUST_USE_RESULT Sprocket* AllocateSprocket(); +// +// This placement has the broadest compatibility with GCC, Clang, and MSVC, with +// both defs and decls, and with GCC-style attributes, MSVC declspec, and C++11 +// attributes. Note: past advice was to place the macro after the argument list. +#if ABSL_HAVE_ATTRIBUTE(warn_unused_result) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_MUST_USE_RESULT __attribute__((warn_unused_result)) +#else +#define ABSL_MUST_USE_RESULT +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(warn_unused_result) || (defined(__GNUC__) && !defined(__clang__)) +#define MUST_USE_RESULT __attribute__((warn_unused_result)) +#else +#define MUST_USE_RESULT +#endif + +// ATTRIBUTE_HOT, ATTRIBUTE_COLD +// Tell GCC that a function is hot or cold. GCC can use this information to +// improve static analysis, i.e. a conditional branch to a cold function +// is likely to be not-taken. +// This annotation is used for function declarations, e.g.: +// int foo() ATTRIBUTE_HOT; +#if ABSL_HAVE_ATTRIBUTE(hot) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_HOT __attribute__((hot)) +#else +#define ABSL_ATTRIBUTE_HOT +#endif + +#if ABSL_HAVE_ATTRIBUTE(cold) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_COLD __attribute__((cold)) +#else +#define ABSL_ATTRIBUTE_COLD +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(hot) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_HOT __attribute__((hot)) +#else +#define ATTRIBUTE_HOT +#endif + +#if ABSL_HAVE_ATTRIBUTE(cold) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_COLD __attribute__((cold)) +#else +#define ATTRIBUTE_COLD +#endif + +// ABSL_XRAY_ALWAYS_INSTRUMENT, ABSL_XRAY_NEVER_INSTRUMENT, ABSL_XRAY_LOG_ARGS +// +// We define the ABSL_XRAY_ALWAYS_INSTRUMENT and ABSL_XRAY_NEVER_INSTRUMENT +// macro used as an attribute to mark functions that must always or never be +// instrumented by XRay. Currently, this is only supported in Clang/LLVM. +// +// For reference on the LLVM XRay instrumentation, see +// http://llvm.org/docs/XRay.html. +// +// A function with the XRAY_ALWAYS_INSTRUMENT macro attribute in its declaration +// will always get the XRay instrumentation sleds. These sleds may introduce +// some binary size and runtime overhead and must be used sparingly. +// +// These attributes only take effect when the following conditions are met: +// +// - The file/target is built in at least C++11 mode, with a Clang compiler +// that supports XRay attributes. +// - The file/target is built with the -fxray-instrument flag set for the +// Clang/LLVM compiler. +// - The function is defined in the translation unit (the compiler honors the +// attribute in either the definition or the declaration, and must match). +// +// There are cases when, even when building with XRay instrumentation, users +// might want to control specifically which functions are instrumented for a +// particular build using special-case lists provided to the compiler. These +// special case lists are provided to Clang via the +// -fxray-always-instrument=... and -fxray-never-instrument=... flags. The +// attributes in source take precedence over these special-case lists. +// +// To disable the XRay attributes at build-time, users may define +// ABSL_NO_XRAY_ATTRIBUTES. Do NOT define ABSL_NO_XRAY_ATTRIBUTES on specific +// packages/targets, as this may lead to conflicting definitions of functions at +// link-time. +// +#if ABSL_HAVE_CPP_ATTRIBUTE(clang::xray_always_instrument) && !defined(ABSL_NO_XRAY_ATTRIBUTES) +#define ABSL_XRAY_ALWAYS_INSTRUMENT [[clang::xray_always_instrument]] +#define ABSL_XRAY_NEVER_INSTRUMENT [[clang::xray_never_instrument]] +#define ABSL_XRAY_LOG_ARGS(N) [[ clang::xray_always_instrument, clang::xray_log_args(N) ]] +#else +#define ABSL_XRAY_ALWAYS_INSTRUMENT +#define ABSL_XRAY_NEVER_INSTRUMENT +#define ABSL_XRAY_LOG_ARGS(N) +#endif + +// ----------------------------------------------------------------------------- +// Variable Attributes +// ----------------------------------------------------------------------------- + +// ATTRIBUTE_UNUSED +// Prevent the compiler from complaining about or optimizing away variables +// that appear unused. +#if ABSL_HAVE_ATTRIBUTE(unused) || (defined(__GNUC__) && !defined(__clang__)) +#undef ABSL_ATTRIBUTE_UNUSED +#define ABSL_ATTRIBUTE_UNUSED __attribute__((__unused__)) +#else +#define ABSL_ATTRIBUTE_UNUSED +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(unused) || (defined(__GNUC__) && !defined(__clang__)) +#undef ATTRIBUTE_UNUSED +#define ATTRIBUTE_UNUSED __attribute__((__unused__)) +#else +#define ATTRIBUTE_UNUSED +#endif + +// ATTRIBUTE_INITIAL_EXEC +// Tell the compiler to use "initial-exec" mode for a thread-local variable. +// See http://people.redhat.com/drepper/tls.pdf for the gory details. +#if ABSL_HAVE_ATTRIBUTE(tls_model) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_INITIAL_EXEC __attribute__((tls_model("initial-exec"))) +#else +#define ABSL_ATTRIBUTE_INITIAL_EXEC +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(tls_model) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_INITIAL_EXEC __attribute__((tls_model("initial-exec"))) +#else +#define ATTRIBUTE_INITIAL_EXEC +#endif + +// ATTRIBUTE_PACKED +// Prevent the compiler from padding a structure to natural alignment +#if ABSL_HAVE_ATTRIBUTE(packed) || (defined(__GNUC__) && !defined(__clang__)) +#define ABSL_ATTRIBUTE_PACKED __attribute__((__packed__)) +#else +#define ABSL_ATTRIBUTE_PACKED +#endif + +// To be deleted macros. All macros are going te be renamed with ABSL_ prefix. +#if ABSL_HAVE_ATTRIBUTE(packed) || (defined(__GNUC__) && !defined(__clang__)) +#define ATTRIBUTE_PACKED __attribute__((__packed__)) +#else +#define ATTRIBUTE_PACKED +#endif + +// ABSL_CONST_INIT +// A variable declaration annotated with the ABSL_CONST_INIT attribute will +// not compile (on supported platforms) unless the variable has a constant +// initializer. This is useful for variables with static and thread storage +// duration, because it guarantees that they will not suffer from the so-called +// "static init order fiasco". +// +// Sample usage: +// +// ABSL_CONST_INIT static MyType my_var = MakeMyType(...); +// +// Note that this attribute is redundant if the variable is declared constexpr. +#if ABSL_HAVE_CPP_ATTRIBUTE(clang::require_constant_initialization) +// NOLINTNEXTLINE(whitespace/braces) (b/36288871) +#define ABSL_CONST_INIT [[clang::require_constant_initialization]] +#else +#define ABSL_CONST_INIT +#endif // ABSL_HAVE_CPP_ATTRIBUTE(clang::require_constant_initialization) + +#endif // THIRD_PARTY_ABSL_BASE_ATTRIBUTES_H_ diff --git a/Firestore/Port/absl/absl_config.h b/Firestore/Port/absl/absl_config.h new file mode 100644 index 0000000..70f4d86 --- /dev/null +++ b/Firestore/Port/absl/absl_config.h @@ -0,0 +1,306 @@ +/* + * 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. + */ + +// Defines preprocessor macros describing the presence of "features" available. +// This facilitates writing portable code by parameterizing the compilation +// based on the presence or lack of a feature. +// +// We define a feature as some interface we wish to program to: for example, +// some library function or system call. +// +// For example, suppose a programmer wants to write a program that uses the +// 'mmap' system call. Then one might write: +// +// #include "absl/base/config.h" +// +// #ifdef ABSL_HAVE_MMAP +// #include "sys/mman.h" +// #endif //ABSL_HAVE_MMAP +// +// ... +// #ifdef ABSL_HAVE_MMAP +// void *ptr = mmap(...); +// ... +// #endif // ABSL_HAVE_MMAP +// +// As a special note, using feature macros from config.h to determine whether +// to include a particular header requires violating the style guide's required +// ordering for headers: this is permitted. + +#ifndef THIRD_PARTY_ABSL_BASE_CONFIG_H_ +#define THIRD_PARTY_ABSL_BASE_CONFIG_H_ + +// Included for the __GLIBC__ macro (or similar macros on other systems). +#include + +#ifdef __cplusplus +// Included for __GLIBCXX__, _LIBCPP_VERSION +#include +#endif // __cplusplus + +// If we're using glibc, make sure we meet a minimum version requirement +// before we proceed much further. +// +// We have chosen glibc 2.12 as the minimum as it was tagged for release +// in May, 2010 and includes some functionality used in Google software +// (for instance pthread_setname_np): +// https://sourceware.org/ml/libc-alpha/2010-05/msg00000.html +#ifdef __GLIBC_PREREQ +#if !__GLIBC_PREREQ(2, 12) +#error "Minimum required version of glibc is 2.12." +#endif +#endif + +// ABSL_HAVE_BUILTIN is a function-like feature checking macro. +// It's a wrapper around __has_builtin, which is defined by only clang now. +// It evaluates to 1 if the builtin is supported or 0 if not. +// Define it to avoid an extra level of #ifdef __has_builtin check. +// http://releases.llvm.org/3.3/tools/clang/docs/LanguageExtensions.html +#ifdef __has_builtin +#define ABSL_HAVE_BUILTIN(x) __has_builtin(x) +#else +#define ABSL_HAVE_BUILTIN(x) 0 +#endif + +// ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE is defined when +// std::is_trivially_destructible is supported. +// +// All supported compilers using libc++ have it, as does gcc >= 4.8 +// using libstdc++, as does Visual Studio. +// https://gcc.gnu.org/onlinedocs/gcc-4.8.1/libstdc++/manual/manual/status.html#status.iso.2011 +// is the first version where std::is_trivially_destructible no longer +// appeared as missing in the Type properties row. +#ifdef ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE +#error ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE cannot be directly set +#elif defined(_LIBCPP_VERSION) || \ + (!defined(__clang__) && defined(__GNUC__) && defined(__GLIBCXX__) && \ + (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8))) || \ + defined(_MSC_VER) +#define ABSL_HAVE_STD_IS_TRIVIALLY_DESTRUCTIBLE 1 +#endif + +// ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE is defined when +// std::is_trivially_default_constructible and +// std::is_trivially_copy_constructible are supported. +// +// ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE is defined when +// std::is_trivially_copy_assignable is supported. +// +// Clang with libc++ supports it, as does gcc >= 5.1 with either +// libc++ or libstdc++, as does Visual Studio. +// https://gcc.gnu.org/gcc-5/changes.html lists as new +// "std::is_trivially_constructible, std::is_trivially_assignable +// etc." +#if defined(ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE) +#error ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE cannot be directly set +#elif defined(ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE) +#error ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE cannot directly set +#elif (defined(__clang__) && defined(_LIBCPP_VERSION)) || \ + (!defined(__clang__) && defined(__GNUC__) && \ + (__GNUC__ > 5 || (__GNUC__ == 5 && __GNUC_MINOR__ >= 1)) && \ + (defined(_LIBCPP_VERSION) || defined(__GLIBCXX__))) || \ + defined(_MSC_VER) +#define ABSL_HAVE_STD_IS_TRIVIALLY_CONSTRUCTIBLE 1 +#define ABSL_HAVE_STD_IS_TRIVIALLY_ASSIGNABLE 1 +#endif + +// ABSL_HAVE_THREAD_LOCAL is defined when C++11's thread_local is available. +// Clang implements thread_local keyword but Xcode did not support the +// implementation until Xcode 8. +#ifdef ABSL_HAVE_THREAD_LOCAL +#error ABSL_HAVE_THREAD_LOCAL cannot be directly set +#elif !defined(__apple_build_version__) || __apple_build_version__ >= 8000042 +#define ABSL_HAVE_THREAD_LOCAL 1 +#endif + +// ABSL_HAVE_INTRINSIC_INT128 is defined when the implementation provides the +// 128 bit integral type: __int128. +// +// __SIZEOF_INT128__ is defined by Clang and GCC when __int128 is supported. +// Clang on ppc64 and aarch64 are exceptions where __int128 exists but has a +// sporadic compiler crashing bug. Nvidia's nvcc also defines __GNUC__ and +// __SIZEOF_INT128__ but not all versions that do this support __int128. Support +// has been tested for versions >= 7. +#ifdef ABSL_HAVE_INTRINSIC_INT128 +#error ABSL_HAVE_INTRINSIC_INT128 cannot be directly set +#elif (defined(__clang__) && defined(__SIZEOF_INT128__) && !defined(__ppc64__) && \ + !defined(__aarch64__)) || \ + (defined(__CUDACC__) && defined(__SIZEOF_INT128__) && __CUDACC_VER__ >= 70000) || \ + (!defined(__clang__) && !defined(__CUDACC__) && defined(__GNUC__) && \ + defined(__SIZEOF_INT128__)) +#define ABSL_HAVE_INTRINSIC_INT128 1 +#endif + +// Operating system-specific features. +// +// Currently supported operating systems and associated preprocessor +// symbols: +// +// Linux and Linux-derived __linux__ +// Android __ANDROID__ (implies __linux__) +// Linux (non-Android) __linux__ && !__ANDROID__ +// Darwin (Mac OS X and iOS) __APPLE__ && __MACH__ +// Akaros (http://akaros.org) __ros__ +// Windows _WIN32 +// NaCL __native_client__ +// AsmJS __asmjs__ +// Fuschia __Fuchsia__ +// +// Note that since Android defines both __ANDROID__ and __linux__, one +// may probe for either Linux or Android by simply testing for __linux__. +// + +// ABSL_HAVE_MMAP is defined when the system has an mmap(2) implementation +// as defined in POSIX.1-2001. +#ifdef ABSL_HAVE_MMAP +#error ABSL_HAVE_MMAP cannot be directly set +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__ros__) || \ + defined(__native_client__) || defined(__asmjs__) || defined(__Fuchsia__) +#define ABSL_HAVE_MMAP 1 +#endif + +// ABSL_HAS_PTHREAD_GETSCHEDPARAM is defined when the system implements the +// pthread_(get|set)schedparam(3) functions as defined in POSIX.1-2001. +#ifdef ABSL_HAVE_PTHREAD_GETSCHEDPARAM +#error ABSL_HAVE_PTHREAD_GETSCHEDPARAM cannot be directly set +#elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__ros__) +#define ABSL_HAVE_PTHREAD_GETSCHEDPARAM 1 +#endif + +// ABSL_HAVE_SCHED_YIELD is defined when the system implements +// sched_yield(2) as defined in POSIX.1-2001. +#ifdef ABSL_HAVE_SCHED_YIELD +#error ABSL_HAVE_SCHED_YIELD cannot be directly set +#elif defined(__linux__) || defined(__ros__) || defined(__native_client__) +#define ABSL_HAVE_SCHED_YIELD 1 +#endif + +// ABSL_HAVE_SEMAPHORE_H is defined when the system supports the +// header and sem_open(3) family of functions as standardized in POSIX.1-2001. +// +// Note: While Apple does have for both iOS and macOS, it is +// explicity deprecated and will cause build failures if enabled for those +// systems. We side-step the issue by not defining it here for Apple platforms. +#ifdef ABSL_HAVE_SEMAPHORE_H +#error ABSL_HAVE_SEMAPHORE_H cannot be directly set +#elif defined(__linux__) || defined(__ros__) +#define ABSL_HAVE_SEMAPHORE_H 1 +#endif + +// Library-specific features. +#ifdef ABSL_HAVE_ALARM +#error ABSL_HAVE_ALARM cannot be directly set +#elif defined(__GOOGLE_GRTE_VERSION__) +// feature tests for Google's GRTE +#define ABSL_HAVE_ALARM 1 +#elif defined(__GLIBC__) +// feature test for glibc +#define ABSL_HAVE_ALARM 1 +#elif defined(_MSC_VER) +// feature tests for Microsoft's library +#elif defined(__native_client__) +#else +// other standard libraries +#define ABSL_HAVE_ALARM 1 +#endif + +#if defined(_STLPORT_VERSION) +#error "STLPort is not supported." +#endif + +// ----------------------------------------------------------------------------- +// Endianness +// ----------------------------------------------------------------------------- +// Define ABSL_IS_LITTLE_ENDIAN, ABSL_IS_BIG_ENDIAN. +// Some compilers or system headers provide macros to specify endianness. +// Unfortunately, there is no standard for the names of the macros or even of +// the header files. +// Reference: https://sourceforge.net/p/predef/wiki/Endianness/ +#if defined(ABSL_IS_BIG_ENDIAN) || defined(ABSL_IS_LITTLE_ENDIAN) +#error "ABSL_IS_(BIG|LITTLE)_ENDIAN cannot be directly set." + +#elif defined(__GLIBC__) || defined(__linux__) +// Operating systems that use the GNU C library generally provide +// containing __BYTE_ORDER, __LITTLE_ENDIAN, __BIG_ENDIAN. +#include + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define ABSL_IS_LITTLE_ENDIAN 1 +#elif __BYTE_ORDER == __BIG_ENDIAN +#define ABSL_IS_BIG_ENDIAN 1 +#else // __BYTE_ORDER != __LITTLE_ENDIAN && __BYTE_ORDER != __BIG_ENDIAN +#error "Unknown endianness" +#endif // __BYTE_ORDER + +#elif defined(__APPLE__) && defined(__MACH__) +// Apple has containing BYTE_ORDER, BIG_ENDIAN, +// LITTLE_ENDIAN. +#include // NOLINT(build/include) + +#if BYTE_ORDER == LITTLE_ENDIAN +#define ABSL_IS_LITTLE_ENDIAN 1 +#elif BYTE_ORDER == BIG_ENDIAN +#define ABSL_IS_BIG_ENDIAN 1 +#else // BYTE_ORDER != LITTLE_ENDIAN && BYTE_ORDER != BIG_ENDIAN +#error "Unknown endianness" +#endif // BYTE_ORDER + +#elif defined(_WIN32) +// Assume Windows is little-endian. +#define ABSL_IS_LITTLE_ENDIAN 1 + +#elif defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || defined(__THUMBEL__) || \ + defined(__AARCH64EL__) || defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) +#define ABSL_IS_LITTLE_ENDIAN 1 + +#elif defined(__BIG_ENDIAN__) || defined(__ARMEB__) || defined(__THUMBEB__) || \ + defined(__AARCH64EB__) || defined(_MIPSEB) || defined(__MIPSEB) || defined(__MIPSEB__) +#define ABSL_IS_BIG_ENDIAN 1 + +#else +#error "absl endian detection needs to be set up on your platform." +#endif + +// ABSL_HAVE_EXCEPTIONS is defined when exceptions are enabled. Many +// compilers support a "no exceptions" mode that disables exceptions. +// +// Generally, when ABSL_HAVE_EXCEPTIONS is not defined: +// +// - Code using `throw` and `try` may not compile. +// - The `noexcept` specifier will still compile and behave as normal. +// - The `noexcept` operator may still return `false`. +// +// For further details, consult the compiler's documentation. +#ifdef ABSL_HAVE_EXCEPTIONS +#error ABSL_HAVE_EXCEPTIONS cannot be directly set. + +#elif defined(__clang__) +// TODO +// Switch to using __cpp_exceptions when we no longer support versions < 3.6. +// For details on this check, see: +// https://goo.gl/PilDrJ +#if defined(__EXCEPTIONS) && __has_feature(cxx_exceptions) +#define ABSL_HAVE_EXCEPTIONS 1 +#endif // defined(__EXCEPTIONS) && __has_feature(cxx_exceptions) + +// Handle remaining special cases and default to exceptions being supported. +#elif !(defined(__GNUC__) && (__GNUC__ < 5) && !defined(__EXCEPTIONS)) && \ + !(defined(__GNUC__) && (__GNUC__ >= 5) && !defined(__cpp_exceptions)) && \ + !(defined(_MSC_VER) && !defined(_CPPUNWIND)) +#define ABSL_HAVE_EXCEPTIONS 1 +#endif + +#endif // THIRD_PARTY_ABSL_BASE_CONFIG_H_ diff --git a/Firestore/Port/absl/absl_endian.h b/Firestore/Port/absl/absl_endian.h new file mode 100644 index 0000000..3b3cf3c --- /dev/null +++ b/Firestore/Port/absl/absl_endian.h @@ -0,0 +1,342 @@ +/* + * 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 ABSL_BASE_INTERNAL_ENDIAN_H_ +#define ABSL_BASE_INTERNAL_ENDIAN_H_ + +// The following guarantees declaration of the byte swap functions +#ifdef _MSC_VER +#include // NOLINT(build/include) +#elif defined(__APPLE__) && defined(__MACH__) +// Mac OS X / Darwin features +#include +#elif defined(__GLIBC__) +#include // IWYU pragma: export +#endif + +#include +#include "absl_port.h" + +namespace absl { + +// Use compiler byte-swapping intrinsics if they are available. 32-bit +// and 64-bit versions are available in Clang and GCC as of GCC 4.3.0. +// The 16-bit version is available in Clang and GCC only as of GCC 4.8.0. +// For simplicity, we enable them all only for GCC 4.8.0 or later. +#if defined(__clang__) || \ + (defined(__GNUC__) && ((__GNUC__ == 4 && __GNUC_MINOR__ >= 8) || __GNUC__ >= 5)) +inline uint64_t gbswap_64(uint64_t host_int) { + return __builtin_bswap64(host_int); +} +inline uint32_t gbswap_32(uint32_t host_int) { + return __builtin_bswap32(host_int); +} +inline uint16 gbswap_16(uint16 host_int) { + return __builtin_bswap16(host_int); +} + +#elif defined(_MSC_VER) +inline uint64_t gbswap_64(uint64_t host_int) { + return _byteswap_uint64(host_int); +} +inline uint32_t gbswap_32(uint32_t host_int) { + return _byteswap_ulong(host_int); +} +inline uint16 gbswap_16(uint16 host_int) { + return _byteswap_ushort(host_int); +} + +#elif defined(__APPLE__) && defined(__MACH__) +inline uint64_t gbswap_64(uint64_t host_int) { + return OSSwapInt16(host_int); +} +inline uint32_t gbswap_32(uint32_t host_int) { + return OSSwapInt32(host_int); +} +inline uint16 gbswap_16(uint16 host_int) { + return OSSwapInt64(host_int); +} + +#else +inline uint64_t gbswap_64(uint64_t host_int) { +#if defined(__GNUC__) && defined(__x86_64__) && !(defined(__APPLE__) && defined(__MACH__)) + // Adapted from /usr/include/byteswap.h. Not available on Mac. + if (__builtin_constant_p(host_int)) { + return __bswap_constant_64(host_int); + } else { + register uint64_t result; + __asm__("bswap %0" : "=r"(result) : "0"(host_int)); + return result; + } +#elif defined(__GLIBC__) + return bswap_64(host_int); +#else + return (((x & GG_ULONGLONG(0xFF)) << 56) | ((x & GG_ULONGLONG(0xFF00)) << 40) | + ((x & GG_ULONGLONG(0xFF0000)) << 24) | ((x & GG_ULONGLONG(0xFF000000)) << 8) | + ((x & GG_ULONGLONG(0xFF00000000)) >> 8) | ((x & GG_ULONGLONG(0xFF0000000000)) >> 24) | + ((x & GG_ULONGLONG(0xFF000000000000)) >> 40) | + ((x & GG_ULONGLONG(0xFF00000000000000)) >> 56)); +#endif // bswap_64 +} + +inline uint32_t gbswap_32(uint32_t host_int) { +#if defined(__GLIBC__) + return bswap_32(host_int); +#else + return (((x & 0xFF) << 24) | ((x & 0xFF00) << 8) | ((x & 0xFF0000) >> 8) | + ((x & 0xFF000000) >> 24)); +#endif +} + +inline uint16 gbswap_16(uint16 host_int) { +#if defined(__GLIBC__) + return bswap_16(host_int); +#else + return (uint16)(((x & 0xFF) << 8) | ((x & 0xFF00) >> 8)); // NOLINT +#endif +} + +#endif // intrinics available + +#ifdef ABSL_IS_LITTLE_ENDIAN + +// Definitions for ntohl etc. that don't require us to include +// netinet/in.h. We wrap gbswap_32 and gbswap_16 in functions rather +// than just #defining them because in debug mode, gcc doesn't +// correctly handle the (rather involved) definitions of bswap_32. +// gcc guarantees that inline functions are as fast as macros, so +// this isn't a performance hit. +inline uint16 ghtons(uint16 x) { + return gbswap_16(x); +} +inline uint32_t ghtonl(uint32_t x) { + return gbswap_32(x); +} +inline uint64_t ghtonll(uint64_t x) { + return gbswap_64(x); +} + +#elif defined ABSL_IS_BIG_ENDIAN + +// These definitions are simpler on big-endian machines +// These are functions instead of macros to avoid self-assignment warnings +// on calls such as "i = ghtnol(i);". This also provides type checking. +inline uint16 ghtons(uint16 x) { + return x; +} +inline uint32_t ghtonl(uint32_t x) { + return x; +} +inline uint64_t ghtonll(uint64_t x) { + return x; +} + +#else +#error \ + "Unsupported byte order: Either ABSL_IS_BIG_ENDIAN or " \ + "ABSL_IS_LITTLE_ENDIAN must be defined" +#endif // byte order + +inline uint16 gntohs(uint16 x) { + return ghtons(x); +} +inline uint32_t gntohl(uint32_t x) { + return ghtonl(x); +} +inline uint64_t gntohll(uint64_t x) { + return ghtonll(x); +} + +// Utilities to convert numbers between the current hosts's native byte +// order and little-endian byte order +// +// Load/Store methods are alignment safe +namespace little_endian { +// Conversion functions. +#ifdef ABSL_IS_LITTLE_ENDIAN + +inline uint16 FromHost16(uint16 x) { + return x; +} +inline uint16 ToHost16(uint16 x) { + return x; +} + +inline uint32_t FromHost32(uint32_t x) { + return x; +} +inline uint32_t ToHost32(uint32_t x) { + return x; +} + +inline uint64_t FromHost64(uint64_t x) { + return x; +} +inline uint64_t ToHost64(uint64_t x) { + return x; +} + +inline constexpr bool IsLittleEndian() { + return true; +} + +#elif defined ABSL_IS_BIG_ENDIAN + +inline uint16 FromHost16(uint16 x) { + return gbswap_16(x); +} +inline uint16 ToHost16(uint16 x) { + return gbswap_16(x); +} + +inline uint32_t FromHost32(uint32_t x) { + return gbswap_32(x); +} +inline uint32_t ToHost32(uint32_t x) { + return gbswap_32(x); +} + +inline uint64_t FromHost64(uint64_t x) { + return gbswap_64(x); +} +inline uint64_t ToHost64(uint64_t x) { + return gbswap_64(x); +} + +inline constexpr bool IsLittleEndian() { + return false; +} + +#endif /* ENDIAN */ + +// Functions to do unaligned loads and stores in little-endian order. +inline uint16 Load16(const void *p) { + return ToHost16(UNALIGNED_LOAD16(p)); +} + +inline void Store16(void *p, uint16 v) { + UNALIGNED_STORE16(p, FromHost16(v)); +} + +inline uint32_t Load32(const void *p) { + return ToHost32(UNALIGNED_LOAD32(p)); +} + +inline void Store32(void *p, uint32_t v) { + UNALIGNED_STORE32(p, FromHost32(v)); +} + +inline uint64_t Load64(const void *p) { + return ToHost64(UNALIGNED_LOAD64(p)); +} + +inline void Store64(void *p, uint64_t v) { + UNALIGNED_STORE64(p, FromHost64(v)); +} + +} // namespace little_endian + +// Utilities to convert numbers between the current hosts's native byte +// order and big-endian byte order (same as network byte order) +// +// Load/Store methods are alignment safe +namespace big_endian { +#ifdef ABSL_IS_LITTLE_ENDIAN + +inline uint16 FromHost16(uint16 x) { + return gbswap_16(x); +} +inline uint16 ToHost16(uint16 x) { + return gbswap_16(x); +} + +inline uint32_t FromHost32(uint32_t x) { + return gbswap_32(x); +} +inline uint32_t ToHost32(uint32_t x) { + return gbswap_32(x); +} + +inline uint64_t FromHost64(uint64_t x) { + return gbswap_64(x); +} +inline uint64_t ToHost64(uint64_t x) { + return gbswap_64(x); +} + +inline constexpr bool IsLittleEndian() { + return true; +} + +#elif defined ABSL_IS_BIG_ENDIAN + +inline uint16 FromHost16(uint16 x) { + return x; +} +inline uint16 ToHost16(uint16 x) { + return x; +} + +inline uint32_t FromHost32(uint32_t x) { + return x; +} +inline uint32_t ToHost32(uint32_t x) { + return x; +} + +inline uint64_t FromHost64(uint64_t x) { + return x; +} +inline uint64_t ToHost64(uint64_t x) { + return x; +} + +inline constexpr bool IsLittleEndian() { + return false; +} + +#endif /* ENDIAN */ + +// Functions to do unaligned loads and stores in big-endian order. +inline uint16 Load16(const void *p) { + return ToHost16(UNALIGNED_LOAD16(p)); +} + +inline void Store16(void *p, uint16 v) { + UNALIGNED_STORE16(p, FromHost16(v)); +} + +inline uint32_t Load32(const void *p) { + return ToHost32(UNALIGNED_LOAD32(p)); +} + +inline void Store32(void *p, uint32_t v) { + UNALIGNED_STORE32(p, FromHost32(v)); +} + +inline uint64_t Load64(const void *p) { + return ToHost64(UNALIGNED_LOAD64(p)); +} + +inline void Store64(void *p, uint64_t v) { + UNALIGNED_STORE64(p, FromHost64(v)); +} + +} // namespace big_endian + +} // namespace absl + +#endif // ABSL_BASE_INTERNAL_ENDIAN_H_ diff --git a/Firestore/Port/absl/absl_integral_types.h b/Firestore/Port/absl/absl_integral_types.h new file mode 100644 index 0000000..47da9c1 --- /dev/null +++ b/Firestore/Port/absl/absl_integral_types.h @@ -0,0 +1,148 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Basic integer type definitions for various platforms +// +// This code is compiled directly on many platforms, including client +// platforms like Windows, Mac, and embedded systems. Before making +// any changes here, make sure that you're not breaking any platforms. +// + +#ifndef THIRD_PARTY_ABSL_BASE_INTEGRAL_TYPES_H_ +#define THIRD_PARTY_ABSL_BASE_INTEGRAL_TYPES_H_ + +// These typedefs are also defined in base/swig/google.swig. In the +// SWIG environment, we use those definitions and avoid duplicate +// definitions here with an ifdef. The definitions should be the +// same in both files, and ideally be only defined in this file. +#ifndef SWIG +// Standard typedefs +// Signed integer types with width of exactly 8, 16, 32, or 64 bits +// respectively, for use when exact sizes are required. +typedef signed char schar; +typedef signed char int8; +typedef short int16; +typedef int int32; +#ifdef COMPILER_MSVC +typedef __int64 int64; +#else +typedef long long int64; +#endif /* COMPILER_MSVC */ + +// NOTE: unsigned types are DANGEROUS in loops and other arithmetical +// places. Use the signed types unless your variable represents a bit +// pattern (eg a hash value) or you really need the extra bit. Do NOT +// use 'unsigned' to express "this value should always be positive"; +// use assertions for this. + +// Unsigned integer types with width of exactly 8, 16, 32, or 64 bits +// respectively, for use when exact sizes are required. +typedef unsigned char uint8; +typedef unsigned short uint16; +typedef unsigned int uint32; +#ifdef COMPILER_MSVC +typedef unsigned __int64 uint64; +#else +typedef unsigned long long uint64; +#endif /* COMPILER_MSVC */ + +// A type to represent a Unicode code-point value. As of Unicode 4.0, +// such values require up to 21 bits. +// (For type-checking on pointers, make this explicitly signed, +// and it should always be the signed version of whatever int32 is.) +typedef signed int char32; + +// A type to represent a natural machine word (for e.g. efficiently +// scanning through memory for checksums or index searching). Don't use +// this for storing normal integers. Ideally this would be just +// unsigned int, but our 64-bit architectures use the LP64 model +// (http://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_models), hence +// their ints are only 32 bits. We want to use the same fundamental +// type on all archs if possible to preserve *printf() compatability. +typedef unsigned long uword_t; + +#endif /* SWIG */ + +// long long macros to be used because gcc and vc++ use different suffixes, +// and different size specifiers in format strings +#undef GG_LONGLONG +#undef GG_ULONGLONG +#undef GG_LL_FORMAT + +#ifdef COMPILER_MSVC /* if Visual C++ */ + +// VC++ long long suffixes +#define GG_LONGLONG(x) x##I64 +#define GG_ULONGLONG(x) x##UI64 + +// Length modifier in printf format std::string for int64's (e.g. within %d) +#define GG_LL_FORMAT "I64" // As in printf("%I64d", ...) +#define GG_LL_FORMAT_W L"I64" + +#else /* not Visual C++ */ + +#define GG_LONGLONG(x) x##LL +#define GG_ULONGLONG(x) x##ULL +#define GG_LL_FORMAT "ll" // As in "%lld". Note that "q" is poor form also. +#define GG_LL_FORMAT_W L"ll" + +#endif // COMPILER_MSVC + +// There are still some requirements that we build these headers in +// C-compatibility mode. Unfortunately, -Wall doesn't like c-style +// casts, and C doesn't know how to read braced-initialization for +// integers. +#if defined(__cplusplus) +const uint8 kuint8max{0xFF}; +const uint16 kuint16max{0xFFFF}; +const uint32 kuint32max{0xFFFFFFFF}; +const uint64 kuint64max{GG_ULONGLONG(0xFFFFFFFFFFFFFFFF)}; +const int8 kint8min{~0x7F}; +const int8 kint8max{0x7F}; +const int16 kint16min{~0x7FFF}; +const int16 kint16max{0x7FFF}; +const int32 kint32min{~0x7FFFFFFF}; +const int32 kint32max{0x7FFFFFFF}; +const int64 kint64min{GG_LONGLONG(~0x7FFFFFFFFFFFFFFF)}; +const int64 kint64max{GG_LONGLONG(0x7FFFFFFFFFFFFFFF)}; +#else // not __cplusplus, this branch exists only for C-compat +static const uint8 kuint8max = ((uint8)0xFF); +static const uint16 kuint16max = ((uint16)0xFFFF); +static const uint32 kuint32max = ((uint32)0xFFFFFFFF); +static const uint64 kuint64max = ((uint64)GG_LONGLONG(0xFFFFFFFFFFFFFFFF)); +static const int8 kint8min = ((int8)~0x7F); +static const int8 kint8max = ((int8)0x7F); +static const int16 kint16min = ((int16)~0x7FFF); +static const int16 kint16max = ((int16)0x7FFF); +static const int32 kint32min = ((int32)~0x7FFFFFFF); +static const int32 kint32max = ((int32)0x7FFFFFFF); +static const int64 kint64min = ((int64)GG_LONGLONG(~0x7FFFFFFFFFFFFFFF)); +static const int64 kint64max = ((int64)GG_LONGLONG(0x7FFFFFFFFFFFFFFF)); +#endif // __cplusplus + +// The following are not real constants, but we list them so CodeSearch and +// other tools find them, in case people are looking for the above constants +// under different names: +// kMaxUint8, kMaxUint16, kMaxUint32, kMaxUint64 +// kMinInt8, kMaxInt8, kMinInt16, kMaxInt16, kMinInt32, kMaxInt32, +// kMinInt64, kMaxInt64 + +// No object has kIllegalFprint as its Fingerprint. +typedef uint64 Fprint; +static const Fprint kIllegalFprint = 0; +static const Fprint kMaxFprint = GG_ULONGLONG(0xFFFFFFFFFFFFFFFF); + +#endif // THIRD_PARTY_ABSL_BASE_INTEGRAL_TYPES_H_ diff --git a/Firestore/Port/absl/absl_port.h b/Firestore/Port/absl/absl_port.h new file mode 100644 index 0000000..3a123a2 --- /dev/null +++ b/Firestore/Port/absl/absl_port.h @@ -0,0 +1,535 @@ +/* + * 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. + */ + +// Various portability macros, type definitions, and inline functions +// This file is used for both C and C++! +// +// These are weird things we need to do to get this compiling on +// random systems (and on SWIG). +// +// This files is structured into the following high-level categories: +// - Platform checks (OS, Compiler, C++, Library) +// - Feature macros +// - Utility macros +// - Utility functions +// - Type alias +// - Predefined system/language macros +// - Predefined system/language functions +// - Compiler attributes (__attribute__) +// - Performance optimization (alignment, branch prediction) +// - Obsolete +// + +#ifndef THIRD_PARTY_ABSL_BASE_PORT_H_ +#define THIRD_PARTY_ABSL_BASE_PORT_H_ + +#include +#include // So we can set the bounds of our types +#include // for free() +#include // for memcpy() + +#include "absl_attributes.h" +#include "absl_config.h" +#include "absl_integral_types.h" + +#ifdef SWIG +%include "attributes.h" +#endif + +// ----------------------------------------------------------------------------- +// Operating System Check +// ----------------------------------------------------------------------------- + +#if defined(__CYGWIN__) +#error "Cygwin is not supported." +#endif + +// ----------------------------------------------------------------------------- +// Compiler Check +// ----------------------------------------------------------------------------- + +// We support MSVC++ 14.0 update 2 and later. +#if defined(_MSC_FULL_VER) && _MSC_FULL_VER < 190023918 +#error "This package requires Visual Studio 2015 Update 2 or higher" +#endif + +// We support gcc 4.7 and later. +#if defined(__GNUC__) && !defined(__clang__) +#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 7) +#error "This package requires gcc 4.7 or higher" +#endif +#endif + +// We support Apple Xcode clang 4.2.1 (version 421.11.65) and later. +// This corresponds to Apple Xcode version 4.5. +#if defined(__apple_build_version__) && __apple_build_version__ < 4211165 +#error "This package requires __apple_build_version__ of 4211165 or higher" +#endif + +// ----------------------------------------------------------------------------- +// C++ Version Check +// ----------------------------------------------------------------------------- + +// Enforce C++11 as the minimum. Note that Visual Studio has not +// advanced __cplusplus despite being good enough for our purposes, so +// so we exempt it from the check. +#if defined(__cplusplus) && !defined(_MSC_VER) && !defined(SWIG) +#if __cplusplus < 201103L +#error "C++ versions less than C++11 are not supported." +#endif +#endif + +// ----------------------------------------------------------------------------- +// C++ Standard Library Check +// ----------------------------------------------------------------------------- + +#if defined(__cplusplus) +#include +#if defined(_STLPORT_VERSION) +#error "STLPort is not supported." +#endif +#endif + +// ----------------------------------------------------------------------------- +// Feature Macros +// ----------------------------------------------------------------------------- + +// ABSL_HAVE_TLS is defined to 1 when __thread should be supported. +// We assume __thread is supported on Linux when compiled with Clang or compiled +// against libstdc++ with _GLIBCXX_HAVE_TLS defined. +#ifdef ABSL_HAVE_TLS +#error ABSL_HAVE_TLS cannot be directly set +#elif defined(__linux__) && (defined(__clang__) || defined(_GLIBCXX_HAVE_TLS)) +#define ABSL_HAVE_TLS 1 +#endif + +// ----------------------------------------------------------------------------- +// Utility Macros +// ----------------------------------------------------------------------------- + +// ABSL_FUNC_PTR_TO_CHAR_PTR +// On some platforms, a "function pointer" points to a function descriptor +// rather than directly to the function itself. +// Use ABSL_FUNC_PTR_TO_CHAR_PTR(func) to get a char-pointer to the first +// instruction of the function func. +#if defined(__cplusplus) +#if (defined(__powerpc__) && !(_CALL_ELF > 1)) || defined(__ia64) +// use opd section for function descriptors on these platforms, the function +// address is the first word of the descriptor +namespace absl { +enum { kPlatformUsesOPDSections = 1 }; +} // namespace absl +#define ABSL_FUNC_PTR_TO_CHAR_PTR(func) (reinterpret_cast(func)[0]) +#else // not PPC or IA64 +namespace absl { +enum { kPlatformUsesOPDSections = 0 }; +} // namespace absl +#define ABSL_FUNC_PTR_TO_CHAR_PTR(func) (reinterpret_cast(func)) +#endif // PPC or IA64 +#endif // __cplusplus + +// ----------------------------------------------------------------------------- +// Utility Functions +// ----------------------------------------------------------------------------- + +#if defined(__cplusplus) +namespace absl { +constexpr char PathSeparator() { +#ifdef _WIN32 + return '\\'; +#else + return '/'; +#endif +} +} // namespace absl +#endif // __cplusplus + +// ----------------------------------------------------------------------------- +// Type Alias +// ----------------------------------------------------------------------------- + +#ifdef _MSC_VER +// uid_t +// MSVC doesn't have uid_t +typedef int uid_t; + +// pid_t +// Defined all over the place. +typedef int pid_t; + +// ssize_t +// VC++ doesn't understand "ssize_t". SSIZE_T is defined in . +#include +typedef SSIZE_T ssize_t; +#endif // _MSC_VER + +// ----------------------------------------------------------------------------- +// Predefined System/Language Macros +// ----------------------------------------------------------------------------- + +// MAP_ANONYMOUS +#if defined(__APPLE__) && defined(__MACH__) +// For mmap, Linux defines both MAP_ANONYMOUS and MAP_ANON and says MAP_ANON is +// deprecated. In Darwin, MAP_ANON is all there is. +#if !defined MAP_ANONYMOUS +#define MAP_ANONYMOUS MAP_ANON +#endif // !MAP_ANONYMOUS +#endif // __APPLE__ && __MACH__ + +// PATH_MAX +// You say tomato, I say atotom +#ifdef _MSC_VER +#define PATH_MAX MAX_PATH +#endif + +// ----------------------------------------------------------------------------- +// Performance Optimization +// ----------------------------------------------------------------------------- + +// Alignment + +// CACHELINE_SIZE, CACHELINE_ALIGNED +// Deprecated: Use ABSL_CACHELINE_SIZE, ABSL_CACHELINE_ALIGNED. +// Note: When C++17 is available, consider using the following: +// - std::hardware_constructive_interference_size +// - std::hardware_destructive_interference_size +// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0154r1.html +#if defined(__GNUC__) +#if defined(__i386__) || defined(__x86_64__) +#define CACHELINE_SIZE 64 +#define ABSL_CACHELINE_SIZE 64 +#elif defined(__powerpc64__) +#define CACHELINE_SIZE 128 +#define ABSL_CACHELINE_SIZE 128 +#elif defined(__aarch64__) +// We would need to read special register ctr_el0 to find out L1 dcache size. +// This value is a good estimate based on a real aarch64 machine. +#define CACHELINE_SIZE 64 +#define ABSL_CACHELINE_SIZE 64 +#elif defined(__arm__) +// Cache line sizes for ARM: These values are not strictly correct since +// cache line sizes depend on implementations, not architectures. There +// are even implementations with cache line sizes configurable at boot +// time. +#if defined(__ARM_ARCH_5T__) +#define CACHELINE_SIZE 32 +#define ABSL_CACHELINE_SIZE 32 +#elif defined(__ARM_ARCH_7A__) +#define CACHELINE_SIZE 64 +#define ABSL_CACHELINE_SIZE 64 +#endif +#endif + +#ifndef CACHELINE_SIZE +// A reasonable default guess. Note that overestimates tend to waste more +// space, while underestimates tend to waste more time. +#define CACHELINE_SIZE 64 +#define ABSL_CACHELINE_SIZE 64 +#endif + +// On some compilers, expands to __attribute__((aligned(CACHELINE_SIZE))). +// For compilers where this is not known to work, expands to nothing. +// +// No further guarantees are made here. The result of applying the macro +// to variables and types is always implementation defined. +// +// WARNING: It is easy to use this attribute incorrectly, even to the point +// of causing bugs that are difficult to diagnose, crash, etc. It does not +// guarantee that objects are aligned to a cache line. +// +// Recommendations: +// +// 1) Consult compiler documentation; this comment is not kept in sync as +// toolchains evolve. +// 2) Verify your use has the intended effect. This often requires inspecting +// the generated machine code. +// 3) Prefer applying this attribute to individual variables. Avoid +// applying it to types. This tends to localize the effect. +#define CACHELINE_ALIGNED __attribute__((aligned(CACHELINE_SIZE))) +#define ABSL_CACHELINE_ALIGNED __attribute__((aligned(ABSL_CACHELINE_SIZE))) + +#else // not GCC +#define CACHELINE_SIZE 64 +#define ABSL_CACHELINE_SIZE 64 +#define CACHELINE_ALIGNED +#define ABSL_CACHELINE_ALIGNED +#endif + +// unaligned APIs + +// Portable handling of unaligned loads, stores, and copies. +// On some platforms, like ARM, the copy functions can be more efficient +// then a load and a store. +// +// It is possible to implement all of these these using constant-length memcpy +// calls, which is portable and will usually be inlined into simple loads and +// stores if the architecture supports it. However, such inlining usually +// happens in a pass that's quite late in compilation, which means the resulting +// loads and stores cannot participate in many other optimizations, leading to +// overall worse code. + +// The unaligned API is C++ only. The declarations use C++ features +// (namespaces, inline) which are absent or incompatible in C. +#if defined(__cplusplus) + +#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER) +// Consider we have an unaligned load/store of 4 bytes from address 0x...05. +// AddressSanitizer will treat it as a 3-byte access to the range 05:07 and +// will miss a bug if 08 is the first unaddressable byte. +// ThreadSanitizer will also treat this as a 3-byte access to 05:07 and will +// miss a race between this access and some other accesses to 08. +// MemorySanitizer will correctly propagate the shadow on unaligned stores +// and correctly report bugs on unaligned loads, but it may not properly +// update and report the origin of the uninitialized memory. +// For all three tools, replacing an unaligned access with a tool-specific +// callback solves the problem. + +// Make sure uint16_t/uint32_t/uint64_t are defined. +#include + +extern "C" { +uint16_t __sanitizer_unaligned_load16(const void *p); +uint32_t __sanitizer_unaligned_load32(const void *p); +uint64_t __sanitizer_unaligned_load64(const void *p); +void __sanitizer_unaligned_store16(void *p, uint16_t v); +void __sanitizer_unaligned_store32(void *p, uint32_t v); +void __sanitizer_unaligned_store64(void *p, uint64_t v); +} // extern "C" + +inline uint16 UNALIGNED_LOAD16(const void *p) { + return __sanitizer_unaligned_load16(p); +} + +inline uint32 UNALIGNED_LOAD32(const void *p) { + return __sanitizer_unaligned_load32(p); +} + +inline uint64 UNALIGNED_LOAD64(const void *p) { + return __sanitizer_unaligned_load64(p); +} + +inline void UNALIGNED_STORE16(void *p, uint16 v) { + __sanitizer_unaligned_store16(p, v); +} + +inline void UNALIGNED_STORE32(void *p, uint32 v) { + __sanitizer_unaligned_store32(p, v); +} + +inline void UNALIGNED_STORE64(void *p, uint64 v) { + __sanitizer_unaligned_store64(p, v); +} + +#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) || \ + defined(__ppc__) || defined(__PPC__) || defined(__ppc64__) || defined(__PPC64__) + +// x86 and x86-64 can perform unaligned loads/stores directly; +// modern PowerPC hardware can also do unaligned integer loads and stores; +// but note: the FPU still sends unaligned loads and stores to a trap handler! + +#define UNALIGNED_LOAD16(_p) (*reinterpret_cast(_p)) +#define UNALIGNED_LOAD32(_p) (*reinterpret_cast(_p)) +#define UNALIGNED_LOAD64(_p) (*reinterpret_cast(_p)) + +#define UNALIGNED_STORE16(_p, _val) (*reinterpret_cast(_p) = (_val)) +#define UNALIGNED_STORE32(_p, _val) (*reinterpret_cast(_p) = (_val)) +#define UNALIGNED_STORE64(_p, _val) (*reinterpret_cast(_p) = (_val)) + +#elif defined(__arm__) && !defined(__ARM_ARCH_5__) && !defined(__ARM_ARCH_5T__) && \ + !defined(__ARM_ARCH_5TE__) && !defined(__ARM_ARCH_5TEJ__) && !defined(__ARM_ARCH_6__) && \ + !defined(__ARM_ARCH_6J__) && !defined(__ARM_ARCH_6K__) && !defined(__ARM_ARCH_6Z__) && \ + !defined(__ARM_ARCH_6ZK__) && !defined(__ARM_ARCH_6T2__) + +// ARMv7 and newer support native unaligned accesses, but only of 16-bit +// and 32-bit values (not 64-bit); older versions either raise a fatal signal, +// do an unaligned read and rotate the words around a bit, or do the reads very +// slowly (trip through kernel mode). There's no simple #define that says just +// “ARMv7 or higher”, so we have to filter away all ARMv5 and ARMv6 +// sub-architectures. Newer gcc (>= 4.6) set an __ARM_FEATURE_ALIGNED #define, +// so in time, maybe we can move on to that. +// +// This is a mess, but there's not much we can do about it. +// +// To further complicate matters, only LDR instructions (single reads) are +// allowed to be unaligned, not LDRD (two reads) or LDM (many reads). Unless we +// explicitly tell the compiler that these accesses can be unaligned, it can and +// will combine accesses. On armcc, the way to signal this is done by accessing +// through the type (uint32 __packed *), but GCC has no such attribute +// (it ignores __attribute__((packed)) on individual variables). However, +// we can tell it that a _struct_ is unaligned, which has the same effect, +// so we do that. + +namespace base { +namespace internal { + +struct Unaligned16Struct { + uint16 value; + uint8 dummy; // To make the size non-power-of-two. +} ATTRIBUTE_PACKED; + +struct Unaligned32Struct { + uint32 value; + uint8 dummy; // To make the size non-power-of-two. +} ATTRIBUTE_PACKED; + +} // namespace internal +} // namespace base + +#define UNALIGNED_LOAD16(_p) \ + ((reinterpret_cast(_p))->value) +#define UNALIGNED_LOAD32(_p) \ + ((reinterpret_cast(_p))->value) + +#define UNALIGNED_STORE16(_p, _val) \ + ((reinterpret_cast< ::base::internal::Unaligned16Struct *>(_p))->value = (_val)) +#define UNALIGNED_STORE32(_p, _val) \ + ((reinterpret_cast< ::base::internal::Unaligned32Struct *>(_p))->value = (_val)) + +inline uint64 UNALIGNED_LOAD64(const void *p) { + uint64 t; + memcpy(&t, p, sizeof t); + return t; +} + +inline void UNALIGNED_STORE64(void *p, uint64 v) { + memcpy(p, &v, sizeof v); +} + +#else + +#define NEED_ALIGNED_LOADS + +// These functions are provided for architectures that don't support +// unaligned loads and stores. + +inline uint16 UNALIGNED_LOAD16(const void *p) { + uint16 t; + memcpy(&t, p, sizeof t); + return t; +} + +inline uint32 UNALIGNED_LOAD32(const void *p) { + uint32 t; + memcpy(&t, p, sizeof t); + return t; +} + +inline uint64 UNALIGNED_LOAD64(const void *p) { + uint64 t; + memcpy(&t, p, sizeof t); + return t; +} + +inline void UNALIGNED_STORE16(void *p, uint16 v) { + memcpy(p, &v, sizeof v); +} + +inline void UNALIGNED_STORE32(void *p, uint32 v) { + memcpy(p, &v, sizeof v); +} + +inline void UNALIGNED_STORE64(void *p, uint64 v) { + memcpy(p, &v, sizeof v); +} + +#endif + +// The UNALIGNED_LOADW and UNALIGNED_STOREW macros load and store values +// of type uword_t. +#ifdef _LP64 +#define UNALIGNED_LOADW(_p) UNALIGNED_LOAD64(_p) +#define UNALIGNED_STOREW(_p, _val) UNALIGNED_STORE64(_p, _val) +#else +#define UNALIGNED_LOADW(_p) UNALIGNED_LOAD32(_p) +#define UNALIGNED_STOREW(_p, _val) UNALIGNED_STORE32(_p, _val) +#endif + +inline void UnalignedCopy16(const void *src, void *dst) { + UNALIGNED_STORE16(dst, UNALIGNED_LOAD16(src)); +} + +inline void UnalignedCopy32(const void *src, void *dst) { + UNALIGNED_STORE32(dst, UNALIGNED_LOAD32(src)); +} + +inline void UnalignedCopy64(const void *src, void *dst) { + if (sizeof(void *) == 8) { + UNALIGNED_STORE64(dst, UNALIGNED_LOAD64(src)); + } else { + const char *src_char = reinterpret_cast(src); + char *dst_char = reinterpret_cast(dst); + + UNALIGNED_STORE32(dst_char, UNALIGNED_LOAD32(src_char)); + UNALIGNED_STORE32(dst_char + 4, UNALIGNED_LOAD32(src_char + 4)); + } +} + +#endif // defined(__cplusplus), end of unaligned API + +// PREDICT_TRUE, PREDICT_FALSE +// +// GCC can be told that a certain branch is not likely to be taken (for +// instance, a CHECK failure), and use that information in static analysis. +// Giving it this information can help it optimize for the common case in +// the absence of better information (ie. -fprofile-arcs). +#if ABSL_HAVE_BUILTIN(__builtin_expect) || (defined(__GNUC__) && !defined(__clang__)) +#define PREDICT_FALSE(x) (__builtin_expect(x, 0)) +#define PREDICT_TRUE(x) (__builtin_expect(!!(x), 1)) +#define ABSL_PREDICT_FALSE(x) (__builtin_expect(x, 0)) +#define ABSL_PREDICT_TRUE(x) (__builtin_expect(!!(x), 1)) +#else +#define PREDICT_FALSE(x) x +#define PREDICT_TRUE(x) x +#define ABSL_PREDICT_FALSE(x) x +#define ABSL_PREDICT_TRUE(x) x +#endif + +// ABSL_ASSERT +// +// In C++11, `assert` can't be used portably within constexpr functions. +// ABSL_ASSERT functions as a runtime assert but works in C++11 constexpr +// functions. Example: +// +// constexpr double Divide(double a, double b) { +// return ABSL_ASSERT(b != 0), a / b; +// } +// +// This macro is inspired by +// https://akrzemi1.wordpress.com/2017/05/18/asserts-in-constexpr-functions/ +#if defined(NDEBUG) +#define ABSL_ASSERT(expr) (false ? (void)(expr) : (void)0) +#else +#define ABSL_ASSERT(expr) \ + (PREDICT_TRUE((expr)) ? (void)0 : [] { assert(false && #expr); }()) // NOLINT +#endif + +// ----------------------------------------------------------------------------- +// Obsolete (to be removed) +// ----------------------------------------------------------------------------- + +// HAS_GLOBAL_STRING +// Some platforms have a std::string class that is different from ::std::string +// (although the interface is the same, of course). On other platforms, +// std::string is the same as ::std::string. +#if defined(__cplusplus) && !defined(SWIG) +#include +#ifndef HAS_GLOBAL_STRING +using std::basic_string; +using std::string; +#endif // HAS_GLOBAL_STRING +#endif // SWIG, __cplusplus + +#endif // THIRD_PARTY_ABSL_BASE_PORT_H_ diff --git a/Firestore/Port/bits.cc b/Firestore/Port/bits.cc new file mode 100644 index 0000000..40af964 --- /dev/null +++ b/Firestore/Port/bits.cc @@ -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. + */ + +#include "bits.h" + +#include + +namespace Firestore { + +int Bits::Log2Floor_Portable(uint32_t n) { + if (n == 0) return -1; + int log = 0; + uint32_t value = n; + for (int i = 4; i >= 0; --i) { + int shift = (1 << i); + uint32_t x = value >> shift; + if (x != 0) { + value = x; + log += shift; + } + } + assert(value == 1); + return log; +} + +} // namespace Firestore diff --git a/Firestore/Port/bits.h b/Firestore/Port/bits.h new file mode 100644 index 0000000..d212bf8 --- /dev/null +++ b/Firestore/Port/bits.h @@ -0,0 +1,160 @@ +/* + * 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 IPHONE_FIRESTORE_PORT_BITS_H_ +#define IPHONE_FIRESTORE_PORT_BITS_H_ + +// Various bit-twiddling functions, all of which are static members of the Bits +// class (making it effectively a namespace). Operands are unsigned integers. +// Munging bits in _signed_ integers is fraught with peril! For example, +// -5 << n has undefined behavior (for some values of n). + +#include + +class Bits_Port32_Test; +class Bits_Port64_Test; + +namespace Firestore { + +class Bits { + public: + // Return floor(log2(n)) for positive integer n. Returns -1 iff n == 0. + static int Log2Floor(uint32_t n); + static int Log2Floor64(uint64_t n); + + // Potentially faster version of Log2Floor() that returns an + // undefined value if n == 0 + static int Log2FloorNonZero(uint32_t n); + static int Log2FloorNonZero64(uint64_t n); + + private: + // Portable implementations. + static int Log2Floor_Portable(uint32_t n); + static int Log2Floor64_Portable(uint64_t n); + static int Log2FloorNonZero_Portable(uint32_t n); + static int Log2FloorNonZero64_Portable(uint64_t n); + + Bits(Bits const&) = delete; + void operator=(Bits const&) = delete; + + // Allow tests to call _Portable variants directly. + friend class ::Bits_Port32_Test; + friend class ::Bits_Port64_Test; +}; + +// ------------------------------------------------------------------------ +// Implementation details follow +// ------------------------------------------------------------------------ + +#if defined(__GNUC__) + +inline int Bits::Log2Floor(uint32_t n) { + return n == 0 ? -1 : 31 ^ __builtin_clz(n); +} + +inline int Bits::Log2FloorNonZero(uint32_t n) { + return 31 ^ __builtin_clz(n); +} + +inline int Bits::Log2Floor64(uint64_t n) { + return n == 0 ? -1 : 63 ^ __builtin_clzll(n); +} + +inline int Bits::Log2FloorNonZero64(uint64_t n) { + return 63 ^ __builtin_clzll(n); +} + +#elif defined(COMPILER_MSVC) + +inline int Bits::Log2FloorNonZero(uint32 n) { +#ifdef _M_IX86 + _asm { + bsr ebx, n + mov n, ebx + } + return n; +#else + return Bits::Log2FloorNonZero_Portable(n); +#endif +} + +inline int Bits::Log2Floor(uint32 n) { +#ifdef _M_IX86 + _asm { + xor ebx, ebx + mov eax, n + and eax, eax + jz return_ebx + bsr ebx, eax + return_ebx: + mov n, ebx + } + return n; +#else + return Bits::Log2Floor_Portable(n); +#endif +} + +inline int Bits::Log2Floor64(uint64_t n) { + return Bits::Log2Floor64_Portable(n); +} + +inline int Bits::Log2FloorNonZero64(uint64_t n) { + return Bits::Log2FloorNonZero64_Portable(n); +} + +#else // !__GNUC__ && !COMPILER_MSVC + +inline int Bits::Log2Floor64(uint64_t n) { + return Bits::Log2Floor64_Portable(n); +} + +inline int Bits::Log2FloorNonZero64(uint64_t n) { + return Bits::Log2FloorNonZero64_Portable(n); +} + +#endif + +inline int Bits::Log2FloorNonZero_Portable(uint32_t n) { + // Just use the common routine + return Log2Floor(n); +} + +// Log2Floor64() is defined in terms of Log2Floor32(), Log2FloorNonZero32() +inline int Bits::Log2Floor64_Portable(uint64_t n) { + const uint32_t topbits = static_cast(n >> 32); + if (topbits == 0) { + // Top bits are zero, so scan in bottom bits + return Log2Floor(static_cast(n)); + } else { + return 32 + Log2FloorNonZero(topbits); + } +} + +// Log2FloorNonZero64() is defined in terms of Log2FloorNonZero32() +inline int Bits::Log2FloorNonZero64_Portable(uint64_t n) { + const uint32_t topbits = static_cast(n >> 32); + if (topbits == 0) { + // Top bits are zero, so scan in bottom bits + return Log2FloorNonZero(static_cast(n)); + } else { + return 32 + Log2FloorNonZero(topbits); + } +} + +} // namespace Firestore + +#endif // IPHONE_FIRESTORE_PORT_BITS_H_ diff --git a/Firestore/Port/bits_test.cc b/Firestore/Port/bits_test.cc new file mode 100644 index 0000000..18c3b1d --- /dev/null +++ b/Firestore/Port/bits_test.cc @@ -0,0 +1,138 @@ +/* + * 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. + */ + +#include "bits.h" + +#include + +#include "base/commandlineflags.h" +#include "testing/base/public/gunit.h" +#include "util/random/mt_random.h" + +using Firestore::Bits; + +DEFINE_int32(num_iterations, 10000, "Number of test iterations to run."); + +class BitsTest : public testing::Test { + public: + BitsTest() : random_(testing::FLAGS_gunit_random_seed) {} + + protected: + MTRandom random_; +}; + +TEST_F(BitsTest, Log2EdgeCases) { + std::cout << "TestLog2EdgeCases" << std::endl; + + EXPECT_EQ(-1, Bits::Log2Floor(0)); + EXPECT_EQ(-1, Bits::Log2Floor64(0)); + + for (int i = 0; i < 32; i++) { + uint32 n = 1U << i; + EXPECT_EQ(i, Bits::Log2Floor(n)); + EXPECT_EQ(i, Bits::Log2FloorNonZero(n)); + if (n > 2) { + EXPECT_EQ(i - 1, Bits::Log2Floor(n - 1)); + EXPECT_EQ(i, Bits::Log2Floor(n + 1)); + EXPECT_EQ(i - 1, Bits::Log2FloorNonZero(n - 1)); + EXPECT_EQ(i, Bits::Log2FloorNonZero(n + 1)); + } + } + + for (int i = 0; i < 64; i++) { + uint64 n = 1ULL << i; + EXPECT_EQ(i, Bits::Log2Floor64(n)); + EXPECT_EQ(i, Bits::Log2FloorNonZero64(n)); + if (n > 2) { + EXPECT_EQ(i - 1, Bits::Log2Floor64(n - 1)); + EXPECT_EQ(i, Bits::Log2Floor64(n + 1)); + EXPECT_EQ(i - 1, Bits::Log2FloorNonZero64(n - 1)); + EXPECT_EQ(i, Bits::Log2FloorNonZero64(n + 1)); + } + } +} + +TEST_F(BitsTest, Log2Random) { + std::cout << "TestLog2Random" << std::endl; + + for (int i = 0; i < FLAGS_num_iterations; i++) { + int maxbit = -1; + uint32 n = 0; + while (!random_.OneIn(32)) { + int bit = random_.Uniform(32); + n |= (1U << bit); + maxbit = std::max(bit, maxbit); + } + EXPECT_EQ(maxbit, Bits::Log2Floor(n)); + if (n != 0) { + EXPECT_EQ(maxbit, Bits::Log2FloorNonZero(n)); + } + } +} + +TEST_F(BitsTest, Log2Random64) { + std::cout << "TestLog2Random64" << std::endl; + + for (int i = 0; i < FLAGS_num_iterations; i++) { + int maxbit = -1; + uint64 n = 0; + while (!random_.OneIn(64)) { + int bit = random_.Uniform(64); + n |= (1ULL << bit); + maxbit = std::max(bit, maxbit); + } + EXPECT_EQ(maxbit, Bits::Log2Floor64(n)); + if (n != 0) { + EXPECT_EQ(maxbit, Bits::Log2FloorNonZero64(n)); + } + } +} + +TEST(Bits, Port32) { + for (int shift = 0; shift < 32; shift++) { + for (int delta = -1; delta <= +1; delta++) { + const uint32 v = (static_cast(1) << shift) + delta; + EXPECT_EQ(Bits::Log2Floor_Portable(v), Bits::Log2Floor(v)) << v; + if (v != 0) { + EXPECT_EQ(Bits::Log2FloorNonZero_Portable(v), Bits::Log2FloorNonZero(v)) + << v; + } + } + } + static const uint32 M32 = kuint32max; + EXPECT_EQ(Bits::Log2Floor_Portable(M32), Bits::Log2Floor(M32)) << M32; + EXPECT_EQ(Bits::Log2FloorNonZero_Portable(M32), Bits::Log2FloorNonZero(M32)) + << M32; +} + +TEST(Bits, Port64) { + for (int shift = 0; shift < 64; shift++) { + for (int delta = -1; delta <= +1; delta++) { + const uint64 v = (static_cast(1) << shift) + delta; + EXPECT_EQ(Bits::Log2Floor64_Portable(v), Bits::Log2Floor64(v)) << v; + if (v != 0) { + EXPECT_EQ(Bits::Log2FloorNonZero64_Portable(v), + Bits::Log2FloorNonZero64(v)) + << v; + } + } + } + static const uint64 M64 = kuint64max; + EXPECT_EQ(Bits::Log2Floor64_Portable(M64), Bits::Log2Floor64(M64)) << M64; + EXPECT_EQ(Bits::Log2FloorNonZero64_Portable(M64), + Bits::Log2FloorNonZero64(M64)) + << M64; +} diff --git a/Firestore/Port/ordered_code.cc b/Firestore/Port/ordered_code.cc new file mode 100644 index 0000000..ec5733c --- /dev/null +++ b/Firestore/Port/ordered_code.cc @@ -0,0 +1,579 @@ +/* + * 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. + */ + +#include "ordered_code.h" + +#include + +#include "bits.h" + +#include "absl_endian.h" +#include "absl_port.h" +#include // For Slice + +// We encode a string in different ways depending on whether the item +// should be in lexicographically increasing or decreasing order. +// +// +// Lexicographically increasing order +// +// We want a string-to-string mapping F(x) such that for any two strings +// +// x < y => F(x) < F(y) +// +// In addition to the normal characters '\x00' through '\xff', we want to +// encode a few extra symbols in strings: +// +// Separator between items +// Infinite string +// +// Therefore we need an alphabet with at least 258 symbols. Each +// character '\1' through '\xfe' is mapped to itself. The other four are +// encoded into two-letter sequences starting with '\0' and '\xff': +// +// encoded as => \0\1 +// \0 encoded as => \0\xff +// \xff encoded as => \xff\x00 +// encoded as => \xff\xff +// +// The remaining two-letter sequences starting with '\0' and '\xff' are +// currently unused. +// +// F() is defined above. For any finite string x, F(x) is the +// the encodings of x's characters followed by the encoding for . The +// ordering of two finite strings is the same as the ordering of the +// respective characters at the first position where they differ, which in +// turn is the same as the ordering of the encodings of those two +// characters. Moreover, for every finite string x, F(x) < F(). + +namespace Firestore { + +using leveldb::Slice; + +static const char kEscape1 = '\000'; +static const char kNullCharacter = '\xff'; // Combined with kEscape1 +static const char kSeparator = '\001'; // Combined with kEscape1 + +static const char kEscape2 = '\xff'; +static const char kInfinity = '\xff'; // Combined with kEscape2 +static const char kFFCharacter = '\000'; // Combined with kEscape2 + +static const char kEscape1_Separator[2] = {kEscape1, kSeparator}; + +// Append to "*dest" the "len" bytes starting from "*src". +inline static void AppendBytes(std::string* dest, const char* src, size_t len) { + dest->append(src, len); +} + +inline bool IsSpecialByte(char c) { return ((unsigned char)(c + 1)) < 2; } + +// Returns 0 if one or more of the bytes in the specified uint32 value +// are the special values 0 or 255, and returns 4 otherwise. The +// result of this routine can be added to "p" to either advance past +// the next 4 bytes if they do not contain a special byte, or to +// remain on this set of four bytes if they contain the next special +// byte occurrence. +// +// REQUIRES: v is the value of loading the next 4 bytes from "*p" (we +// pass in v rather than loading it because in some cases, the client +// may already have the value in a register: "p" is just used for +// assertion checking). +inline int AdvanceIfNoSpecialBytes(uint32_t v_32, const char* p) { + assert(UNALIGNED_LOAD32(p) == v_32); + // See comments in SkipToNextSpecialByte if you wish to + // understand this expression (which checks for the occurrence + // of the special byte values 0 or 255 in any of the bytes of v_32). + if ((v_32 - 0x01010101u) & ~(v_32 + 0x01010101u) & 0x80808080u) { + // Special byte is in p[0..3] + assert(IsSpecialByte(p[0]) || IsSpecialByte(p[1]) || IsSpecialByte(p[2]) || + IsSpecialByte(p[3])); + return 0; + } else { + assert(!IsSpecialByte(p[0])); + assert(!IsSpecialByte(p[1])); + assert(!IsSpecialByte(p[2])); + assert(!IsSpecialByte(p[3])); + return 4; + } +} + +// Return a pointer to the first byte in the range "[start..limit)" +// whose value is 0 or 255 (kEscape1 or kEscape2). If no such byte +// exists in the range, returns "limit". +inline const char* SkipToNextSpecialByte(const char* start, const char* limit) { + // If these constants were ever changed, this routine needs to change + assert(kEscape1 == 0); + assert((kEscape2 & 0xffu) == 255u); + const char* p = start; + while (p + 8 <= limit) { + // Find out if any of the next 8 bytes are either 0 or 255 (our + // two characters that require special handling). We do this using + // the technique described in: + // + // http://graphics.stanford.edu/~seander/bithacks.html#HasLessInWord + // + // We use the test (x + 1) < 2 to check x = 0 or -1(255) + // + // If x is a byte value (0x00..0xff): + // (x - 0x01) & 0x80 is true only when x = 0x81..0xff, 0x00 + // ~(x + 0x01) & 0x80 is true only when x = 0x00..0x7e, 0xff + // The intersection of the above two sets is x = 0x00 or 0xff. + // We can ignore carry between bytes because only x = 0x00 or 0xff + // can cause carry in the expression -- and such x already makes the + // result value non-zero. + uint64_t v = UNALIGNED_LOAD64(p); + bool hasZeroOr255Byte = (v - 0x0101010101010101ull) & + ~(v + 0x0101010101010101ull) & + 0x8080808080808080ull; + if (!hasZeroOr255Byte) { + // No special values in the next 8 bytes + p += 8; + } else { +// We know the next 8 bytes have a special byte: find it +#ifdef IS_LITTLE_ENDIAN + uint32_t v_32 = static_cast(v); // Low 32 bits of v +#else + uint32_t v_32 = UNALIGNED_LOAD32(p); +#endif + // Test 32 bits at once to see if special byte is in next 4 bytes + // or the following 4 bytes + p += AdvanceIfNoSpecialBytes(v_32, p); + if (IsSpecialByte(p[0])) return p; + if (IsSpecialByte(p[1])) return p + 1; + if (IsSpecialByte(p[2])) return p + 2; + assert(IsSpecialByte(p[3])); // Last byte must be the special one + return p + 3; + } + } + if (p + 4 <= limit) { + uint32_t v_32 = UNALIGNED_LOAD32(p); + p += AdvanceIfNoSpecialBytes(v_32, p); + } + while (p < limit && !IsSpecialByte(*p)) { + p++; + } + return p; +} + +// Expose SkipToNextSpecialByte for testing purposes +const char* OrderedCode::TEST_SkipToNextSpecialByte(const char* start, + const char* limit) { + return SkipToNextSpecialByte(start, limit); +} + +// Helper routine to encode "s" and append to "*dest", escaping special +// characters. +inline static void EncodeStringFragment(std::string* dest, Slice s) { + const char* p = s.data(); + const char* limit = p + s.size(); + const char* copy_start = p; + while (true) { + p = SkipToNextSpecialByte(p, limit); + if (p >= limit) break; // No more special characters that need escaping + char c = *(p++); + assert(IsSpecialByte(c)); + if (c == kEscape1) { + AppendBytes(dest, copy_start, p - copy_start - 1); + dest->push_back(kEscape1); + dest->push_back(kNullCharacter); + copy_start = p; + } else { + assert(c == kEscape2); + AppendBytes(dest, copy_start, p - copy_start - 1); + dest->push_back(kEscape2); + dest->push_back(kFFCharacter); + copy_start = p; + } + } + if (p > copy_start) { + AppendBytes(dest, copy_start, p - copy_start); + } +} + +void OrderedCode::WriteString(std::string* dest, Slice s) { + EncodeStringFragment(dest, s); + AppendBytes(dest, kEscape1_Separator, 2); +} + +// Return number of bytes needed to encode the non-length portion +// of val in ordered coding. Returns number in range [0,8]. +static inline unsigned int OrderedNumLength(uint64_t val) { + const int lg = Bits::Log2Floor64(val); // -1 if val==0 + return static_cast(lg + 1 + 7) / 8; +} + +// Append n bytes from src to *dst. +// REQUIRES: n <= 9 +// REQUIRES: src[0..8] are readable bytes (even if n is smaller) +// +// If we use string::append() instead of this routine, it increases the +// runtime of WriteNumIncreasing from ~9ns to ~13ns. +static inline void AppendUpto9(std::string* dst, const char* src, + unsigned int n) { + dst->append(src, 9); // Fixed-length append + const size_t extra = 9 - n; // How many extra bytes we added + dst->erase(dst->size() - extra, extra); +} + +void OrderedCode::WriteNumIncreasing(std::string* dest, uint64_t val) { + // Values are encoded with a single byte length prefix, followed + // by the actual value in big-endian format with leading 0 bytes + // dropped. + + // 8 bytes for value plus one byte for length. In addition, we have + // 8 extra bytes at the end so that we can have a fixed-length append + // call on *dest. + char buf[17]; + + UNALIGNED_STORE64(buf + 1, absl::ghtonll(val)); // buf[0] may be needed for length + const unsigned int length = OrderedNumLength(val); + char* start = buf + 9 - length - 1; + *start = length; + AppendUpto9(dest, start, length + 1); +} + +inline static void WriteInfinityInternal(std::string* dest) { + // Make an array so that we can just do one string operation for performance + static const char buf[2] = {kEscape2, kInfinity}; + dest->append(buf, 2); +} + +void OrderedCode::WriteInfinity(std::string* dest) { + WriteInfinityInternal(dest); +} + +void OrderedCode::WriteTrailingString(std::string* dest, Slice str) { + dest->append(str.data(), str.size()); +} + +// Parse the encoding of a string previously encoded with or without +// inversion. If parse succeeds, return true, consume encoding from +// "*src", and if result != NULL append the decoded string to "*result". +// Otherwise, return false and leave both undefined. +inline static bool ReadStringInternal(Slice* src, std::string* result) { + const char* start = src->data(); + const char* string_limit = src->data() + src->size(); + + // We only scan up to "limit-2" since a valid string must end with + // a two character terminator: 'kEscape1 kSeparator' + const char* limit = string_limit - 1; + const char* copy_start = start; + while (true) { + start = SkipToNextSpecialByte(start, limit); + if (start >= limit) break; // No terminator sequence found + const char c = *(start++); + // If inversion is required, instead of inverting 'c', we invert the + // character constants to which 'c' is compared. We get the same + // behavior but save the runtime cost of inverting 'c'. + assert(IsSpecialByte(c)); + if (c == kEscape1) { + if (result) { + AppendBytes(result, copy_start, start - copy_start - 1); + } + // kEscape1 kSeparator ends component + // kEscape1 kNullCharacter represents '\0' + const char next = *(start++); + if (next == kSeparator) { + src->remove_prefix(start - src->data()); + return true; + } else if (next == kNullCharacter) { + if (result) { + *result += '\0'; + } + } else { + return false; + } + copy_start = start; + } else { + assert(c == kEscape2); + if (result) { + AppendBytes(result, copy_start, start - copy_start - 1); + } + // kEscape2 kFFCharacter represents '\xff' + // kEscape2 kInfinity is an error + const char next = *(start++); + if (next == kFFCharacter) { + if (result) { + *result += '\xff'; + } + } else { + return false; + } + copy_start = start; + } + } + return false; +} + +bool OrderedCode::ReadString(Slice* src, std::string* result) { + return ReadStringInternal(src, result); +} + +bool OrderedCode::ReadNumIncreasing(Slice* src, uint64_t* result) { + if (src->empty()) { + return false; // Not enough bytes + } + + // Decode length byte + const int len = static_cast((*src)[0]); + + // If len > 0 and src is longer than 1, the first byte of "payload" + // must be non-zero (otherwise the encoding is not minimal). + // In opt mode, we don't enforce that encodings must be minimal. + assert(0 == len || src->size() == 1 || (*src)[1] != '\0'); + + if (len + 1 > src->size() || len > 8) { + return false; // Not enough bytes or too many bytes + } + + if (result) { + uint64_t tmp = 0; + for (int i = 0; i < len; i++) { + tmp <<= 8; + tmp |= static_cast((*src)[1 + i]); + } + *result = tmp; + } + src->remove_prefix(len + 1); + return true; +} + +inline static bool ReadInfinityInternal(Slice* src) { + if (src->size() >= 2 && ((*src)[0] == kEscape2) && ((*src)[1] == kInfinity)) { + src->remove_prefix(2); + return true; + } else { + return false; + } +} + +bool OrderedCode::ReadInfinity(Slice* src) { return ReadInfinityInternal(src); } + +inline static bool ReadStringOrInfinityInternal(Slice* src, std::string* result, + bool* inf) { + if (ReadInfinityInternal(src)) { + if (inf) *inf = true; + return true; + } + + // We don't use ReadStringInternal here because that would inline + // the whole encoded string parsing code here. Depending on INVERT, only + // one of the following two calls will be generated at compile time. + bool success = OrderedCode::ReadString(src, result); + if (success) { + if (inf) *inf = false; + return true; + } else { + return false; + } +} + +bool OrderedCode::ReadStringOrInfinity(Slice* src, std::string* result, + bool* inf) { + return ReadStringOrInfinityInternal(src, result, inf); +} + +bool OrderedCode::ReadTrailingString(Slice* src, std::string* result) { + if (result) result->assign(src->data(), src->size()); + src->remove_prefix(src->size()); + return true; +} + +void OrderedCode::TEST_Corrupt(std::string* str, int k) { + int seen_seps = 0; + for (int i = 0; i < str->size() - 1; i++) { + if ((*str)[i] == kEscape1 && (*str)[i + 1] == kSeparator) { + seen_seps++; + if (seen_seps == k) { + (*str)[i + 1] = kSeparator + 1; + return; + } + } + } +} + +// Signed number encoding/decoding ///////////////////////////////////// +// +// The format is as follows: +// +// The first bit (the most significant bit of the first byte) +// represents the sign, 0 if the number is negative and +// 1 if the number is >= 0. +// +// Any unbroken sequence of successive bits with the same value as the sign +// bit, up to 9 (the 8th and 9th are the most significant bits of the next +// byte), are size bits that count the number of bytes after the first byte. +// That is, the total length is between 1 and 10 bytes. +// +// The value occupies the bits after the sign bit and the "size bits" +// till the end of the string, in network byte order. If the number +// is negative, the bits are in 2-complement. +// +// +// Example 1: number 0x424242 -> 4 byte big-endian hex string 0xf0424242: +// +// +---------------+---------------+---------------+---------------+ +// 1 1 1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 1 0 +// +---------------+---------------+---------------+---------------+ +// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +// | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +// | | | | payload: the remaining bits after the sign and size bits +// | | | | and the delimiter bit, the value is 0x424242 +// | | | | +// | size bits: 3 successive bits with the same value as the sign bit +// | (followed by a delimiter bit with the opposite value) +// | mean that there are 3 bytes after the first byte, 4 total +// | +// sign bit: 1 means that the number is non-negative +// +// Example 2: negative number -0x800 -> 2 byte big-endian hex string 0x3800: +// +// +---------------+---------------+ +// 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 +// +---------------+---------------+ +// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +// | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +// | | payload: the remaining bits after the sign and size bits and the +// | | delimiter bit, 2-complement because of the negative sign, +// | | value is ~0x7ff, represents the value -0x800 +// | | +// | size bits: 1 bit with the same value as the sign bit +// | (followed by a delimiter bit with the opposite value) +// | means that there is 1 byte after the first byte, 2 total +// | +// sign bit: 0 means that the number is negative +// +// +// Compared with the simpler unsigned format used for uint64_t numbers, +// this format is more compact for small numbers, namely one byte encodes +// numbers in the range [-64,64), two bytes cover the range [-2^13,2^13), etc. +// In general, n bytes encode numbers in the range [-2^(n*7-1),2^(n*7-1)). +// (The cross-over point for compactness of representation is 8 bytes, +// where this format only covers the range [-2^55,2^55), +// whereas an encoding with sign bit and length in the first byte and +// payload in all following bytes would cover [-2^56,2^56).) + +static const int kMaxSigned64Length = 10; + +// This array maps encoding length to header bits in the first two bytes. +static const char kLengthToHeaderBits[1 + kMaxSigned64Length][2] = { + {0, 0}, {'\x80', 0}, {'\xc0', 0}, {'\xe0', 0}, + {'\xf0', 0}, {'\xf8', 0}, {'\xfc', 0}, {'\xfe', 0}, + {'\xff', 0}, {'\xff', '\x80'}, {'\xff', '\xc0'}}; + +// This array maps encoding lengths to the header bits that overlap with +// the payload and need fixing when reading. +static const uint64_t kLengthToMask[1 + kMaxSigned64Length] = { + 0ULL, + 0x80ULL, + 0xc000ULL, + 0xe00000ULL, + 0xf0000000ULL, + 0xf800000000ULL, + 0xfc0000000000ULL, + 0xfe000000000000ULL, + 0xff00000000000000ULL, + 0x8000000000000000ULL, + 0ULL}; + +// This array maps the number of bits in a number to the encoding +// length produced by WriteSignedNumIncreasing. +// For positive numbers, the number of bits is 1 plus the most significant +// bit position (the highest bit position in a positive int64_t is 63). +// For a negative number n, we count the bits in ~n. +// That is, length = kBitsToLength[Bits::Log2Floor64(n < 0 ? ~n : n) + 1]. +static const int8_t kBitsToLength[1 + 63] = { + 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, + 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 7, + 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 10}; + +// Calculates the encoding length in bytes of the signed number n. +static inline int SignedEncodingLength(int64_t n) { + return kBitsToLength[Bits::Log2Floor64(n < 0 ? ~n : n) + 1]; +} + +// Slightly faster version for n > 0. +static inline int SignedEncodingLengthPositive(int64_t n) { + return kBitsToLength[Bits::Log2FloorNonZero64(n) + 1]; +} + +void OrderedCode::WriteSignedNumIncreasing(std::string* dest, int64_t val) { + const uint64_t x = val < 0 ? ~val : val; + if (x < 64) { // fast path for encoding length == 1 + *dest += kLengthToHeaderBits[1][0] ^ val; + return; + } + // buf = val in network byte order, sign extended to 10 bytes + const char sign_byte = val < 0 ? '\xff' : '\0'; + char buf[10] = { + sign_byte, sign_byte, + }; + UNALIGNED_STORE64(buf + 2, absl::ghtonll(val)); + + static_assert(sizeof(buf) == kMaxSigned64Length, "max length size mismatch"); + const int len = SignedEncodingLengthPositive(x); + assert(len >= 2); + char* const begin = buf + sizeof(buf) - len; + begin[0] ^= kLengthToHeaderBits[len][0]; + begin[1] ^= kLengthToHeaderBits[len][1]; // ok because len >= 2 + dest->append(begin, len); +} + +bool OrderedCode::ReadSignedNumIncreasing(Slice* src, int64_t* result) { + if (src->empty()) return false; + const uint64_t xor_mask = (!((*src)[0] & 0x80)) ? ~0ULL : 0ULL; + const unsigned char first_byte = (*src)[0] ^ (xor_mask & 0xff); + + // now calculate and test length, and set x to raw (unmasked) result + int len; + uint64_t x; + if (first_byte != 0xff) { + len = 7 - Bits::Log2FloorNonZero(first_byte ^ 0xff); + if (src->size() < len) return false; + x = xor_mask; // sign extend using xor_mask + for (int i = 0; i < len; ++i) + x = (x << 8) | static_cast((*src)[i]); + } else { + len = 8; + if (src->size() < len) return false; + const unsigned char second_byte = (*src)[1] ^ (xor_mask & 0xff); + if (second_byte >= 0x80) { + if (second_byte < 0xc0) { + len = 9; + } else { + const unsigned char third_byte = (*src)[2] ^ (xor_mask & 0xff); + if (second_byte == 0xc0 && third_byte < 0x80) { + len = 10; + } else { + return false; // either len > 10 or len == 10 and #bits > 63 + } + } + if (src->size() < len) return false; + } + x = absl::gntohll(UNALIGNED_LOAD64(src->data() + len - 8)); + } + + x ^= kLengthToMask[len]; // remove spurious header bits + + assert(len == SignedEncodingLength(x)); + + if (result) *result = x; + src->remove_prefix(len); + return true; +} + +} // namespace Firestore + diff --git a/Firestore/Port/ordered_code.h b/Firestore/Port/ordered_code.h new file mode 100644 index 0000000..7c390a5 --- /dev/null +++ b/Firestore/Port/ordered_code.h @@ -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. + */ + +// This module provides routines for encoding a sequence of typed +// entities into a string. The resulting strings can be +// lexicographically compared to yield the same comparison value that +// would have been generated if the encoded items had been compared +// one by one according to their type. +// +// More precisely, suppose: +// 1. string A is generated by encoding the sequence of items [A_1..A_n] +// 2. string B is generated by encoding the sequence of items [B_1..B_n] +// 3. The types match; i.e., for all i: A_i was encoded using +// the same routine as B_i +// Then: +// Comparing A vs. B lexicographically is the same as comparing +// the vectors [A_1..A_n] and [B_1..B_n] lexicographically. +// +// Furthermore, if n < m, the encoding of [A_1..A_n] is a strict prefix of +// [A_1..A_m] (unless m = n+1 and A_m is the empty string encoded with +// WriteTrailingString, in which case the encodings are equal). +// +// This module is often useful when generating multi-part sstable +// keys that have to be ordered in a particular fashion. + +#ifndef IPHONE_FIRESTORE_PORT_ORDERED_CODE_H_ +#define IPHONE_FIRESTORE_PORT_ORDERED_CODE_H_ + +#include + +namespace leveldb { +class Slice; +} + +namespace Firestore { + +class OrderedCode { + public: + // ------------------------------------------------------------------- + // Encoding routines: each one of the following routines append + // one item to "*dest". The Write(Signed)NumIncreasing() and + // Write(Signed)NumDecreasing() routines differ in whether the resulting + // encoding is ordered by increasing number or decreasing number. + // Similarly, WriteString() and WriteStringDecreasing() differ in whether + // the resulting encoding is ordered by the original string in + // lexicographically increasing or decreasing order. WriteString() + // is not called WriteStringIncreasing() for convenience and backward + // compatibility. + + static void WriteString(std::string* dest, leveldb::Slice str); + static void WriteNumIncreasing(std::string* dest, uint64_t num); + static void WriteSignedNumIncreasing(std::string* dest, int64_t num); + + // Creates an encoding for the "infinite string", a value considered to + // be lexicographically after any real string. Note that in the case of + // WriteInfinityDecreasing(), this would come before any real string as + // the ordering puts lexicographically greater values first. + static void WriteInfinity(std::string* dest); + + // Special string append that can only be used at the tail end of + // an encoded string -- blindly appends "str" to "*dest". + static void WriteTrailingString(std::string* dest, leveldb::Slice str); + + // ------------------------------------------------------------------- + // Decoding routines: these extract an item earlier encoded using + // the corresponding WriteXXX() routines above. The item is read + // from "*src"; "*src" is modified to point past the decoded item; + // and if "result" is non-NULL, "*result" is modified to contain the + // result. In case of string result, the decoded string is appended to + // "*result". Returns true if the next item was read successfully, false + // otherwise. + + static bool ReadString(leveldb::Slice* src, std::string* result); + static bool ReadNumIncreasing(leveldb::Slice* src, uint64_t* result); + static bool ReadSignedNumIncreasing(leveldb::Slice* src, int64_t* result); + + static bool ReadInfinity(leveldb::Slice* src); + static bool ReadTrailingString(leveldb::Slice* src, std::string* result); + + // REQUIRES: next item was encoded by WriteInfinity() or WriteString() + static bool ReadStringOrInfinity(leveldb::Slice* src, std::string* result, bool* inf); + + // Helper for testing: corrupt "*str" by changing the kth item separator + // in the string. + static void TEST_Corrupt(std::string* str, int k); + + // Helper for testing. + // SkipToNextSpecialByte is an internal routine defined in the .cc file + // with the following semantics. Return a pointer to the first byte + // in the range "[start..limit)" whose value is 0 or 255. If no such + // byte exists in the range, returns "limit". + static const char* TEST_SkipToNextSpecialByte(const char* start, const char* limit); + + // Not an instantiable class, but the class exists to make it easy to + // use with a single using statement. + OrderedCode() = delete; + OrderedCode(const OrderedCode&) = delete; + OrderedCode& operator=(const OrderedCode&) = delete; +}; + +} // namespace Firestore + +#endif // IPHONE_FIRESTORE_PORT_ORDERED_CODE_H_ diff --git a/Firestore/Port/ordered_code_test.cc b/Firestore/Port/ordered_code_test.cc new file mode 100644 index 0000000..21b0bd1 --- /dev/null +++ b/Firestore/Port/ordered_code_test.cc @@ -0,0 +1,528 @@ +/* + * 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. + */ + +#include "ordered_code.h" + +// #include +// #include +#include +#include + +#include "base/logging.h" +#include "testing/base/public/gunit.h" +#include +#include "util/random/acmrandom.h" + +using Firestore::OrderedCode; +using leveldb::Slice; + +// Make Slices writeable to ostream, making all the CHECKs happy below. +namespace { +void WritePadding(std::ostream& o, size_t pad) { + char fill_buf[32]; + memset(fill_buf, o.fill(), sizeof(fill_buf)); + while (pad) { + size_t n = std::min(pad, sizeof(fill_buf)); + o.write(fill_buf, n); + pad -= n; + } +} +} // namespace + +namespace leveldb { + +std::ostream& operator<<(std::ostream& o, const Slice slice) { + std::ostream::sentry sentry(o); + if (sentry) { + size_t lpad = 0; + size_t rpad = 0; + if (o.width() > slice.size()) { + size_t pad = o.width() - slice.size(); + if ((o.flags() & o.adjustfield) == o.left) { + rpad = pad; + } else { + lpad = pad; + } + } + if (lpad) WritePadding(o, lpad); + o.write(slice.data(), slice.size()); + if (rpad) WritePadding(o, rpad); + o.width(0); + } + return o; +} + +} // namespace leveldb + +static std::string RandomString(ACMRandom* rnd, int len) { + std::string x; + for (int i = 0; i < len; i++) { + x += rnd->Uniform(256); + } + return x; +} + +// --------------------------------------------------------------------- +// Utility template functions (they help templatize the tests below) + +// Read/WriteIncreasing are defined for string, uint64_t, int64_t below. +template +static void OCWriteIncreasing(std::string* dest, const T& val); +template +static bool OCReadIncreasing(Slice* src, T* result); + +// Read/WriteIncreasing +template <> +void OCWriteIncreasing(std::string* dest, const std::string& val) { + OrderedCode::WriteString(dest, val); +} +template <> +bool OCReadIncreasing(Slice* src, std::string* result) { + return OrderedCode::ReadString(src, result); +} + +// Read/WriteIncreasing +template <> +void OCWriteIncreasing(std::string* dest, const uint64_t& val) { + OrderedCode::WriteNumIncreasing(dest, val); +} +template <> +bool OCReadIncreasing(Slice* src, uint64_t* result) { + return OrderedCode::ReadNumIncreasing(src, result); +} + +enum Direction { INCREASING = 0 }; + +// Read/WriteIncreasing +template <> +void OCWriteIncreasing(std::string* dest, const int64_t& val) { + OrderedCode::WriteSignedNumIncreasing(dest, val); +} +template <> +bool OCReadIncreasing(Slice* src, int64_t* result) { + return OrderedCode::ReadSignedNumIncreasing(src, result); +} + +template +std::string OCWrite(T val, Direction direction) { + std::string result; + OCWriteIncreasing(&result, val); + return result; +} + +template +void OCWriteToString(std::string* result, T val, Direction direction) { + OCWriteIncreasing(result, val); +} + +template +bool OCRead(Slice* s, T* val, Direction direction) { + return OCReadIncreasing(s, val); +} + +// --------------------------------------------------------------------- +// Numbers + +template +static T TestRead(Direction d, const std::string& a) { + // gracefully reject any proper prefix of an encoding + for (int i = 0; i < a.size() - 1; ++i) { + Slice s(a.data(), i); + CHECK(!OCRead(&s, NULL, d)); + CHECK_EQ(s, a.substr(0, i)); + } + + Slice s(a); + T v; + CHECK(OCRead(&s, &v, d)); + CHECK(s.empty()); + return v; +} + +template +static void TestWriteRead(Direction d, T expected) { + EXPECT_EQ(expected, TestRead(d, OCWrite(expected, d))); +} + +// Verifies that the second Write* call appends a non-empty std::string to its +// output. +template +static void TestWriteAppends(Direction d, T first, U second) { + std::string encoded; + OCWriteToString(&encoded, first, d); + std::string encoded_first_only = encoded; + OCWriteToString(&encoded, second, d); + EXPECT_NE(encoded, encoded_first_only); + EXPECT_TRUE(Slice(encoded).starts_with(encoded_first_only)); +} + +template +static void TestNumbers(T multiplier) { + for (int j = 0; j < 2; ++j) { + const Direction d = static_cast(j); + + // first test powers of 2 (and nearby numbers) + for (T x = std::numeric_limits().max(); x != 0; x /= 2) { + TestWriteRead(d, multiplier * (x - 1)); + TestWriteRead(d, multiplier * x); + if (x != std::numeric_limits::max()) { + TestWriteRead(d, multiplier * (x + 1)); + } else if (multiplier < 0 && multiplier == -1) { + TestWriteRead(d, -x - 1); + } + } + + ACMRandom rnd(301); + for (int bits = 1; bits <= std::numeric_limits().digits; ++bits) { + // test random non-negative numbers with given number of significant bits + const uint64_t mask = (~0ULL) >> (64 - bits); + for (int i = 0; i < 1000; i++) { + T x = rnd.Next64() & mask; + TestWriteRead(d, multiplier * x); + T y = rnd.Next64() & mask; + TestWriteAppends(d, multiplier * x, multiplier * y); + } + } + } +} + +// Return true iff 'a' is "before" 'b' according to 'direction' +static bool CompareStrings(const std::string& a, const std::string& b, + Direction d) { + return (INCREASING == d) ? (a < b) : (b < a); +} + +template +static void TestNumberOrdering() { + const Direction d = INCREASING; + + // first the negative numbers (if T is signed, otherwise no-op) + std::string laststr = OCWrite(std::numeric_limits().min(), d); + for (T num = std::numeric_limits().min() / 2; num != 0; num /= 2) { + std::string strminus1 = OCWrite(num - 1, d); + std::string str = OCWrite(num, d); + std::string strplus1 = OCWrite(num + 1, d); + + CHECK(CompareStrings(strminus1, str, d)); + CHECK(CompareStrings(str, strplus1, d)); + + // Compare 'str' with 'laststr'. When we approach 0, 'laststr' is + // not necessarily before 'strminus1'. + CHECK(CompareStrings(laststr, str, d)); + laststr = str; + } + + // then the positive numbers + laststr = OCWrite(0, d); + T num = 1; + while (num < std::numeric_limits().max() / 2) { + num *= 2; + std::string strminus1 = OCWrite(num - 1, d); + std::string str = OCWrite(num, d); + std::string strplus1 = OCWrite(num + 1, d); + + CHECK(CompareStrings(strminus1, str, d)); + CHECK(CompareStrings(str, strplus1, d)); + + // Compare 'str' with 'laststr'. + CHECK(CompareStrings(laststr, str, d)); + laststr = str; + } +} + +// Helper routine for testing TEST_SkipToNextSpecialByte +static int FindSpecial(const std::string& x) { + const char* p = x.data(); + const char* limit = p + x.size(); + const char* result = OrderedCode::TEST_SkipToNextSpecialByte(p, limit); + return result - p; +} + +TEST(OrderedCode, SkipToNextSpecialByte) { + for (int len = 0; len < 256; len++) { + ACMRandom rnd(301); + std::string x; + while (x.size() < len) { + char c = 1 + rnd.Uniform(254); + ASSERT_NE(c, 0); + ASSERT_NE(c, 255); + x += c; // No 0 bytes, no 255 bytes + } + EXPECT_EQ(FindSpecial(x), x.size()); + for (int special_pos = 0; special_pos < len; special_pos++) { + for (int special_test = 0; special_test < 2; special_test++) { + const char special_byte = (special_test == 0) ? 0 : 255; + std::string y = x; + y[special_pos] = special_byte; + EXPECT_EQ(FindSpecial(y), special_pos); + if (special_pos < 16) { + // Add some special bytes after the one at special_pos to make sure + // we still return the earliest special byte in the string + for (int rest = special_pos + 1; rest < len; rest++) { + if (rnd.OneIn(3)) { + y[rest] = rnd.OneIn(2) ? 0 : 255; + EXPECT_EQ(FindSpecial(y), special_pos); + } + } + } + } + } + } +} + +TEST(OrderedCode, ExhaustiveFindSpecial) { + char buf[16]; + char* limit = buf + sizeof(buf); + int count = 0; + for (int start_offset = 0; start_offset <= 5; start_offset += 5) { + // We test exhaustively with all combinations of 3 bytes starting + // at offset 0 and offset 5 (so as to test with the bytes at both + // ends of a 64-bit word). + for (char& c : buf) { + c = 'a'; // Not a special byte + } + for (int b0 = 0; b0 < 256; b0++) { + for (int b1 = 0; b1 < 256; b1++) { + for (int b2 = 0; b2 < 256; b2++) { + buf[start_offset + 0] = b0; + buf[start_offset + 1] = b1; + buf[start_offset + 2] = b2; + char* expected; + if (b0 == 0 || b0 == 255) { + expected = &buf[start_offset]; + } else if (b1 == 0 || b1 == 255) { + expected = &buf[start_offset + 1]; + } else if (b2 == 0 || b2 == 255) { + expected = &buf[start_offset + 2]; + } else { + expected = limit; + } + count++; + EXPECT_EQ(expected, + OrderedCode::TEST_SkipToNextSpecialByte(buf, limit)); + } + } + } + } + EXPECT_EQ(count, 256 * 256 * 256 * 2); +} + +TEST(Uint64, EncodeDecode) { TestNumbers(1); } + +TEST(Uint64, Ordering) { TestNumberOrdering(); } + +TEST(Int64, EncodeDecode) { + TestNumbers(1); + TestNumbers(-1); +} + +TEST(Int64, Ordering) { TestNumberOrdering(); } + +// Returns the bitwise complement of s. +static inline std::string StrNot(const std::string& s) { + std::string result; + for (const char c : s) result.push_back(~c); + return result; +} + +template +static void TestInvalidEncoding(Direction d, const std::string& s) { + Slice p(s); + EXPECT_FALSE(OCRead(&p, static_cast(NULL), d)); + EXPECT_EQ(s, p); +} + +TEST(OrderedCodeInvalidEncodingsTest, Overflow) { + // 1U << 64, increasing + const std::string k2xx64U = "\x09\x01" + std::string(8, 0); + TestInvalidEncoding(INCREASING, k2xx64U); + + // 1 << 63 and ~(1 << 63), increasing + const std::string k2xx63 = "\xff\xc0\x80" + std::string(7, 0); + TestInvalidEncoding(INCREASING, k2xx63); + TestInvalidEncoding(INCREASING, StrNot(k2xx63)); +} + +TEST(OrderedCodeInvalidEncodingsTest, NonCanonical) { + // Test DCHECK failures of "ambiguous"/"non-canonical" encodings. + // These are non-minimal (but otherwise "valid") encodings that + // differ from the minimal encoding chosen by OrderedCode::WriteXXX + // and thus should be avoided to not mess up the string ordering of + // encodings. + + ACMRandom rnd(301); + + for (int n = 2; n <= 9; ++n) { + // The zero in non_minimal[1] is "redundant". + std::string non_minimal = + std::string(1, n - 1) + std::string(1, 0) + RandomString(&rnd, n - 2); + EXPECT_EQ(n, non_minimal.length()); + + EXPECT_NE(OCWrite(0, INCREASING), non_minimal); + if (DEBUG_MODE) { + Slice s(non_minimal); + EXPECT_DEATH_IF_SUPPORTED(OrderedCode::ReadNumIncreasing(&s, NULL), + "ssertion failed"); + } else { + TestRead(INCREASING, non_minimal); + } + } + + for (int n = 2; n <= 10; ++n) { + // Header with 1 sign bit and n-1 size bits. + std::string header = + std::string(n / 8, 0xff) + std::string(1, 0xff << (8 - (n % 8))); + // There are more than 7 zero bits between header bits and "payload". + std::string non_minimal = + header + std::string(1, rnd.Uniform(256) & ~*header.rbegin()) + + RandomString(&rnd, n - header.length() - 1); + EXPECT_EQ(n, non_minimal.length()); + + EXPECT_NE(OCWrite(0, INCREASING), non_minimal); + if (DEBUG_MODE) { + Slice s(non_minimal); + EXPECT_DEATH_IF_SUPPORTED(OrderedCode::ReadSignedNumIncreasing(&s, NULL), + "ssertion failed") + << n; + s = non_minimal; + } else { + TestRead(INCREASING, non_minimal); + } + } +} + +// --------------------------------------------------------------------- +// Strings + +TEST(String, Infinity) { + const std::string value("\xff\xff foo"); + bool is_inf; + std::string encoding, parsed; + Slice s; + + // Check encoding/decoding of "infinity" for ascending order + encoding.clear(); + OrderedCode::WriteInfinity(&encoding); + encoding.push_back('a'); + s = encoding; + EXPECT_TRUE(OrderedCode::ReadInfinity(&s)); + EXPECT_EQ(1, s.size()); + s = encoding; + is_inf = false; + EXPECT_TRUE(OrderedCode::ReadStringOrInfinity(&s, NULL, &is_inf)); + EXPECT_EQ(1, s.size()); + EXPECT_TRUE(is_inf); + + // Check ReadStringOrInfinity() can parse ordinary strings + encoding.clear(); + OrderedCode::WriteString(&encoding, value); + encoding.push_back('a'); + s = encoding; + is_inf = false; + parsed.clear(); + EXPECT_TRUE(OrderedCode::ReadStringOrInfinity(&s, &parsed, &is_inf)); + EXPECT_EQ(1, s.size()); + EXPECT_FALSE(is_inf); + EXPECT_EQ(value, parsed); +} + +TEST(String, EncodeDecode) { + ACMRandom rnd(301); + for (int i = 0; i < 2; ++i) { + const Direction d = static_cast(i); + + for (int len = 0; len < 256; len++) { + const std::string a = RandomString(&rnd, len); + TestWriteRead(d, a); + for (int len2 = 0; len2 < 64; len2++) { + const std::string b = RandomString(&rnd, len2); + + TestWriteAppends(d, a, b); + + std::string out; + OCWriteToString(&out, a, d); + OCWriteToString(&out, b, d); + + std::string a2, b2, dummy; + Slice s = out; + Slice s2 = out; + CHECK(OCRead(&s, &a2, d)); + CHECK(OCRead(&s2, NULL, d)); + CHECK_EQ(s, s2); + + CHECK(OCRead(&s, &b2, d)); + CHECK(OCRead(&s2, NULL, d)); + CHECK_EQ(s, s2); + + CHECK(!OCRead(&s, &dummy, d)); + CHECK(!OCRead(&s2, NULL, d)); + CHECK_EQ(a, a2); + CHECK_EQ(b, b2); + CHECK(s.empty()); + CHECK(s2.empty()); + } + } + } +} + +// 'str' is a static C-style string that may contain '\0' +#define STATIC_STR(str) Slice((str), sizeof(str) - 1) + +static std::string EncodeStringIncreasing(Slice value) { + std::string encoded; + OrderedCode::WriteString(&encoded, value); + return encoded; +} + +TEST(String, Increasing) { + // Here are a series of strings in non-decreasing order, including + // consecutive strings such that the second one is equal to, a proper + // prefix of, or has the same length as the first one. Most also contain + // the special escaping characters '\x00' and '\xff'. + ASSERT_EQ(EncodeStringIncreasing(STATIC_STR("")), + EncodeStringIncreasing(STATIC_STR(""))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("")), + EncodeStringIncreasing(STATIC_STR("\x00"))); + + ASSERT_EQ(EncodeStringIncreasing(STATIC_STR("\x00")), + EncodeStringIncreasing(STATIC_STR("\x00"))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\x00")), + EncodeStringIncreasing(STATIC_STR("\x01"))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\x01")), + EncodeStringIncreasing(STATIC_STR("a"))); + + ASSERT_EQ(EncodeStringIncreasing(STATIC_STR("a")), + EncodeStringIncreasing(STATIC_STR("a"))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("a")), + EncodeStringIncreasing(STATIC_STR("aa"))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("aa")), + EncodeStringIncreasing(STATIC_STR("\xff"))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\xff")), + EncodeStringIncreasing(STATIC_STR("\xff\x00"))); + + ASSERT_LT(EncodeStringIncreasing(STATIC_STR("\xff\x00")), + EncodeStringIncreasing(STATIC_STR("\xff\x01"))); + + std::string infinity; + OrderedCode::WriteInfinity(&infinity); + ASSERT_LT(EncodeStringIncreasing(std::string(1 << 20, '\xff')), infinity); +} diff --git a/Firestore/Port/string_util.cc b/Firestore/Port/string_util.cc new file mode 100644 index 0000000..5e87fff --- /dev/null +++ b/Firestore/Port/string_util.cc @@ -0,0 +1,51 @@ +/* + * 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. + */ + +#include "string_util.h" + +#include + +namespace Firestore { + +std::string PrefixSuccessor(leveldb::Slice prefix) { + // We can increment the last character in the string and be done + // unless that character is 255 (0xff), in which case we have to erase the + // last character and increment the previous character, unless that + // is 255, etc. If the string is empty or consists entirely of + // 255's, we just return the empty string. + std::string limit(prefix.data(), prefix.size()); + while (!limit.empty()) { + size_t index = limit.length() - 1; + if (limit[index] == '\xff') { // char literal avoids signed/unsigned. + limit.erase(index); + } else { + limit[index]++; + break; + } + } + return limit; +} + +std::string ImmediateSuccessor(leveldb::Slice s) { + // Return the input string, with an additional NUL byte appended. + std::string out; + out.reserve(s.size() + 1); + out.append(s.data(), s.size()); + out.push_back('\0'); + return out; +} + +} // namespace Firestore diff --git a/Firestore/Port/string_util.h b/Firestore/Port/string_util.h new file mode 100644 index 0000000..6e85ba9 --- /dev/null +++ b/Firestore/Port/string_util.h @@ -0,0 +1,66 @@ +/* + * 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. + */ + +// Useful string functions and so forth. This is a grab-bag file. +// +// These functions work fine for UTF-8 strings as long as you can +// consider them to be just byte strings. For example, due to the +// design of UTF-8 you do not need to worry about accidental matches, +// as long as all your inputs are valid UTF-8 (use \uHHHH, not \xHH or \oOOO). + +#ifndef IPHONE_FIRESTORE_PORT_STRING_UTIL_H_ +#define IPHONE_FIRESTORE_PORT_STRING_UTIL_H_ + +#include + +namespace leveldb { +class Slice; +} + +namespace Firestore { + +// Returns the smallest lexicographically larger string of equal or smaller +// length. Returns an empty string if there is no such successor (if the input +// is empty or consists entirely of 0xff bytes). +// Useful for calculating the smallest lexicographically larger string +// that will not be prefixed by the input string. +// +// Examples: +// "a" -> "b", "aaa" -> "aab", "aa\xff" -> "ab", "\xff" -> "", "" -> "" +std::string PrefixSuccessor(leveldb::Slice prefix); + +// Returns the immediate lexicographically-following string. This is useful to +// turn an inclusive range into something that can be used with Bigtable's +// SetLimitRow(): +// +// // Inclusive range [min_element, max_element]. +// string min_element = ...; +// string max_element = ...; +// +// // Equivalent range [range_start, range_end). +// string range_start = min_element; +// string range_end = ImmediateSuccessor(max_element); +// +// WARNING: Returns the input string with a '\0' appended; if you call c_str() +// on the result, it will compare equal to s. +// +// WARNING: Transforms "" -> "\0"; this doesn't account for Bigtable's special +// treatment of "" as infinity. +std::string ImmediateSuccessor(leveldb::Slice s); + +} // namespace Firestore + +#endif // IPHONE_FIRESTORE_PORT_STRING_UTIL_H_ diff --git a/Firestore/Port/string_util_test.cc b/Firestore/Port/string_util_test.cc new file mode 100644 index 0000000..ecdfb8f --- /dev/null +++ b/Firestore/Port/string_util_test.cc @@ -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. + */ + +#include "string_util.h" + +#include "testing/base/public/gunit.h" +#include + +using Firestore::PrefixSuccessor; +using Firestore::ImmediateSuccessor; +using leveldb::Slice; + +TEST(Util, PrefixSuccessor) { + EXPECT_EQ(PrefixSuccessor("a"), "b"); + EXPECT_EQ(PrefixSuccessor("aaAA"), "aaAB"); + EXPECT_EQ(PrefixSuccessor("aaa\xff"), "aab"); + EXPECT_EQ(PrefixSuccessor(string("\x00", 1)), "\x01"); + EXPECT_EQ(PrefixSuccessor("az\xe0"), "az\xe1"); + EXPECT_EQ(PrefixSuccessor("\xff\xff\xff"), ""); + EXPECT_EQ(PrefixSuccessor(""), ""); +} + +TEST(Util, ImmediateSuccessor) { + EXPECT_EQ(ImmediateSuccessor("hello"), Slice("hello\0", 6)); + EXPECT_EQ(ImmediateSuccessor(""), Slice("\0", 1)); +} diff --git a/Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj b/Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj new file mode 100644 index 0000000..51a61b8 --- /dev/null +++ b/Firestore/Protos/FrameworkMaker.xcodeproj/project.pbxproj @@ -0,0 +1,428 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 621808027FC20B1A1B769E50 /* libPods-FrameworkMaker_iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AB2E4F8834D5EA87A8F7124C /* libPods-FrameworkMaker_iOS.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker.release.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker.release.xcconfig"; sourceTree = ""; }; + 04058317A2F1A863FB91F84F /* Pods-FrameworkMaker_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS.release.xcconfig"; sourceTree = ""; }; + 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BDF11E206B3015647181AB8 /* Pods-FrameworkMaker_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS.debug.xcconfig"; sourceTree = ""; }; + 93482F41CCA683759459AC1E /* libPods-FrameworkMaker_macOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker_macOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB2E4F8834D5EA87A8F7124C /* libPods-FrameworkMaker_iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FrameworkMaker_iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FrameworkMaker.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FrameworkMaker/Pods-FrameworkMaker.debug.xcconfig"; sourceTree = ""; }; + D013F9FF1ED9EB9900FD68A9 /* FrameworkMaker_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D013FA131ED9EC0B00FD68A9 /* iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS-Info.plist"; sourceTree = ""; }; + D013FA141ED9EC1500FD68A9 /* macOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "macOS-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05A46BD41CC9B2BE007BDB33 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 621808027FC20B1A1B769E50 /* libPods-FrameworkMaker_iOS.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D013F9FC1ED9EB9900FD68A9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05A46BCE1CC9B2BE007BDB33 = { + isa = PBXGroup; + children = ( + D013FA131ED9EC0B00FD68A9 /* iOS-Info.plist */, + D013FA141ED9EC1500FD68A9 /* macOS-Info.plist */, + 05A46BD81CC9B2BE007BDB33 /* Products */, + AA03828B8B59297B5A3389B0 /* Pods */, + D3884AD1918E82D7FD21433D /* Frameworks */, + ); + sourceTree = ""; + }; + 05A46BD81CC9B2BE007BDB33 /* Products */ = { + isa = PBXGroup; + children = ( + 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker_iOS.app */, + D013F9FF1ED9EB9900FD68A9 /* FrameworkMaker_macOS.app */, + ); + name = Products; + sourceTree = ""; + }; + AA03828B8B59297B5A3389B0 /* Pods */ = { + isa = PBXGroup; + children = ( + C8DA4EE8A169B227B0576C02 /* Pods-FrameworkMaker.debug.xcconfig */, + 01F29B956E7F6E45EF34DE72 /* Pods-FrameworkMaker.release.xcconfig */, + 5BDF11E206B3015647181AB8 /* Pods-FrameworkMaker_iOS.debug.xcconfig */, + 04058317A2F1A863FB91F84F /* Pods-FrameworkMaker_iOS.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + D3884AD1918E82D7FD21433D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1D25AC01A0F56F8BC5375DD2 /* libPods-FrameworkMaker.a */, + AB2E4F8834D5EA87A8F7124C /* libPods-FrameworkMaker_iOS.a */, + 93482F41CCA683759459AC1E /* libPods-FrameworkMaker_macOS.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker_iOS" */; + buildPhases = ( + AC1C2B143A86214CE77C9932 /* [CP] Check Pods Manifest.lock */, + 05A46BD31CC9B2BE007BDB33 /* Sources */, + 05A46BD41CC9B2BE007BDB33 /* Frameworks */, + 05A46BD51CC9B2BE007BDB33 /* Resources */, + 11182BBE1E5DB1C0F58623BB /* [CP] Embed Pods Frameworks */, + 5040608D1004852F08A22A14 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FrameworkMaker_iOS; + productName = FrameworkMaker; + productReference = 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker_iOS.app */; + productType = "com.apple.product-type.application"; + }; + D013F9FE1ED9EB9900FD68A9 /* FrameworkMaker_macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D013FA121ED9EB9900FD68A9 /* Build configuration list for PBXNativeTarget "FrameworkMaker_macOS" */; + buildPhases = ( + D013F9FB1ED9EB9900FD68A9 /* Sources */, + D013F9FC1ED9EB9900FD68A9 /* Frameworks */, + D013F9FD1ED9EB9900FD68A9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FrameworkMaker_macOS; + productName = FrameworkMaker_macOS; + productReference = D013F9FF1ED9EB9900FD68A9 /* FrameworkMaker_macOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05A46BCF1CC9B2BE007BDB33 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = "Google, Inc."; + TargetAttributes = { + 05A46BD61CC9B2BE007BDB33 = { + CreatedOnToolsVersion = 7.3; + }; + D013F9FE1ED9EB9900FD68A9 = { + CreatedOnToolsVersion = 8.3.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 05A46BD21CC9B2BE007BDB33 /* Build configuration list for PBXProject "FrameworkMaker" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05A46BCE1CC9B2BE007BDB33; + productRefGroup = 05A46BD81CC9B2BE007BDB33 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker_iOS */, + D013F9FE1ED9EB9900FD68A9 /* FrameworkMaker_macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05A46BD51CC9B2BE007BDB33 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D013F9FD1ED9EB9900FD68A9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 11182BBE1E5DB1C0F58623BB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 5040608D1004852F08A22A14 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS-resources.sh", + "$PODS_CONFIGURATION_BUILD_DIR/gRPC/gRPCCertificates.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FrameworkMaker_iOS/Pods-FrameworkMaker_iOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + AC1C2B143A86214CE77C9932 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FrameworkMaker_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05A46BD31CC9B2BE007BDB33 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D013F9FB1ED9EB9900FD68A9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05A46BEC1CC9B2BE007BDB33 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05A46BED1CC9B2BE007BDB33 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05A46BEF1CC9B2BE007BDB33 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5BDF11E206B3015647181AB8 /* Pods-FrameworkMaker_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/iOS-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "google.FrameworkMaker-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05A46BF01CC9B2BE007BDB33 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 04058317A2F1A863FB91F84F /* Pods-FrameworkMaker_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/iOS-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "google.FrameworkMaker-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + D013FA101ED9EB9900FD68A9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "$(SRCROOT)/macOS-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.FrameworkMaker-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Debug; + }; + D013FA111ED9EB9900FD68A9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "$(SRCROOT)/macOS-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.FrameworkMaker-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05A46BD21CC9B2BE007BDB33 /* Build configuration list for PBXProject "FrameworkMaker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05A46BEC1CC9B2BE007BDB33 /* Debug */, + 05A46BED1CC9B2BE007BDB33 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05A46BEF1CC9B2BE007BDB33 /* Debug */, + 05A46BF01CC9B2BE007BDB33 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D013FA121ED9EB9900FD68A9 /* Build configuration list for PBXNativeTarget "FrameworkMaker_macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D013FA101ED9EB9900FD68A9 /* Debug */, + D013FA111ED9EB9900FD68A9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05A46BCF1CC9B2BE007BDB33 /* Project object */; +} diff --git a/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme new file mode 100644 index 0000000..2994deb --- /dev/null +++ b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_iOS.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme new file mode 100644 index 0000000..dbe6579 --- /dev/null +++ b/Firestore/Protos/FrameworkMaker.xcodeproj/xcshareddata/xcschemes/FrameworkMaker_macOS.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firestore/Protos/Podfile b/Firestore/Protos/Podfile new file mode 100644 index 0000000..988d7f8 --- /dev/null +++ b/Firestore/Protos/Podfile @@ -0,0 +1,11 @@ +# This Podfile and FrameworkMaker.xcodeproj is only here to access the +# ProtoCompiler plugin. + +project 'FrameworkMaker.xcodeproj' + +target 'FrameworkMaker_iOS' do + platform :ios, '7.0' + + # This should be versioned along with 'gRPC-ProtoRPC' in Firestore.podspec + pod '!ProtoCompiler-gRPCPlugin' +end diff --git a/Firestore/Protos/README.md b/Firestore/Protos/README.md new file mode 100644 index 0000000..cb6d90e --- /dev/null +++ b/Firestore/Protos/README.md @@ -0,0 +1,20 @@ +## Usage + +``` +cd firebase-ios-sdk/Firestore/Protos +./build-protos.sh +``` + +Verify diffs, tests and make PR + +### Script Details + +Get the protoc and the gRPC plugin. See +[here](https://github.com/grpc/grpc/tree/master/src/objective-c). The +easiest way I found was to add +`pod '!ProtoCompiler-gRPCPlugin'` to a Podfile and do `pod update`. + +After running the protoc, shell commands run to fix up the generated code: + * Flatten import paths for CocoaPods library build. + * Remove unneeded extensionRegistry functions. + * Remove non-buildable code from Annotations.pbobjc.*. diff --git a/Firestore/Protos/build-protos.sh b/Firestore/Protos/build-protos.sh new file mode 100755 index 0000000..4cfb12e --- /dev/null +++ b/Firestore/Protos/build-protos.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# 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. + +# Run this script from firebase-ios-sdk/Firestore/Protos to regnenerate the +# Objective C files from the protos. + +# pod update to install protoc and the gRPC plugin compiler. +rm -rf Pods +rm Podfile.lock +pod update + +# Generate the objective C files from the protos. +./Pods/!ProtoCompiler/protoc --plugin=protoc-gen-grpc=Pods/\!ProtoCompiler-gRPCPlugin/grpc_objective_c_plugin -I protos --objc_out=objc --grpc_out=objc `find protos -name *.proto -print | xargs` + +# CocoaPods does not like paths in library imports, flatten them. + +for i in `find objc -name "*.[mh]"` ; do + perl -i -pe 's#import ".*/#import "#' $i; +done + +# Remove the unnecessary extensionRegistry functions. + +for i in `find objc -name "*.[m]" ` ; do + ./strip-registry.py $i +done + +# Remove non-buildable code from Annotations.pbobjc.*. + +echo "static int annotations_stub __attribute__((unused,used)) = 0;" > objc/google/api/Annotations.pbobjc.m +echo "// Empty stub file" > objc/google/api/Annotations.pbobjc.h diff --git a/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h new file mode 100644 index 0000000..d34090a --- /dev/null +++ b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h @@ -0,0 +1,132 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: firestore/local/maybe_document.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class FSTPBNoDocument; +@class GCFSDocument; +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTPBMaybeDocumentRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface FSTPBMaybeDocumentRoot : GPBRootObject +@end + +#pragma mark - FSTPBNoDocument + +typedef GPB_ENUM(FSTPBNoDocument_FieldNumber) { + FSTPBNoDocument_FieldNumber_Name = 1, + FSTPBNoDocument_FieldNumber_ReadTime = 2, +}; + +/** + * A message indicating that the document is known to not exist. + **/ +@interface FSTPBNoDocument : GPBMessage + +/** + * The name of the document that does not exist, in the standard format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}` + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *name; + +/** The time at which we observed that it does not exist. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; +/** Test to see if @c readTime has been set. */ +@property(nonatomic, readwrite) BOOL hasReadTime; + +@end + +#pragma mark - FSTPBMaybeDocument + +typedef GPB_ENUM(FSTPBMaybeDocument_FieldNumber) { + FSTPBMaybeDocument_FieldNumber_NoDocument = 1, + FSTPBMaybeDocument_FieldNumber_Document = 2, +}; + +typedef GPB_ENUM(FSTPBMaybeDocument_DocumentType_OneOfCase) { + FSTPBMaybeDocument_DocumentType_OneOfCase_GPBUnsetOneOfCase = 0, + FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument = 1, + FSTPBMaybeDocument_DocumentType_OneOfCase_Document = 2, +}; + +/** + * Represents either an existing document or the explicitly known absence of a + * document. + **/ +@interface FSTPBMaybeDocument : GPBMessage + +@property(nonatomic, readonly) FSTPBMaybeDocument_DocumentType_OneOfCase documentTypeOneOfCase; + +/** Used if the document is known to not exist. */ +@property(nonatomic, readwrite, strong, null_resettable) FSTPBNoDocument *noDocument; + +/** The document (if it exists). */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document; + +@end + +/** + * Clears whatever value was set for the oneof 'documentType'. + **/ +void FSTPBMaybeDocument_ClearDocumentTypeOneOfCase(FSTPBMaybeDocument *message); + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m new file mode 100644 index 0000000..1d4404d --- /dev/null +++ b/Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.m @@ -0,0 +1,192 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: firestore/local/maybe_document.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Timestamp.pbobjc.h" +#endif + + #import "MaybeDocument.pbobjc.h" + #import "Document.pbobjc.h" + #import "Annotations.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - FSTPBMaybeDocumentRoot + +@implementation FSTPBMaybeDocumentRoot + + +@end + +#pragma mark - FSTPBMaybeDocumentRoot_FileDescriptor + +static GPBFileDescriptor *FSTPBMaybeDocumentRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"firestore.client" + objcPrefix:@"FSTPB" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - FSTPBNoDocument + +@implementation FSTPBNoDocument + +@dynamic name; +@dynamic hasReadTime, readTime; + +typedef struct FSTPBNoDocument__storage_ { + uint32_t _has_storage_[1]; + NSString *name; + GPBTimestamp *readTime; +} FSTPBNoDocument__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "name", + .dataTypeSpecific.className = NULL, + .number = FSTPBNoDocument_FieldNumber_Name, + .hasIndex = 0, + .offset = (uint32_t)offsetof(FSTPBNoDocument__storage_, name), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = FSTPBNoDocument_FieldNumber_ReadTime, + .hasIndex = 1, + .offset = (uint32_t)offsetof(FSTPBNoDocument__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[FSTPBNoDocument class] + rootClass:[FSTPBMaybeDocumentRoot class] + file:FSTPBMaybeDocumentRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(FSTPBNoDocument__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - FSTPBMaybeDocument + +@implementation FSTPBMaybeDocument + +@dynamic documentTypeOneOfCase; +@dynamic noDocument; +@dynamic document; + +typedef struct FSTPBMaybeDocument__storage_ { + uint32_t _has_storage_[2]; + FSTPBNoDocument *noDocument; + GCFSDocument *document; +} FSTPBMaybeDocument__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "noDocument", + .dataTypeSpecific.className = GPBStringifySymbol(FSTPBNoDocument), + .number = FSTPBMaybeDocument_FieldNumber_NoDocument, + .hasIndex = -1, + .offset = (uint32_t)offsetof(FSTPBMaybeDocument__storage_, noDocument), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "document", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = FSTPBMaybeDocument_FieldNumber_Document, + .hasIndex = -1, + .offset = (uint32_t)offsetof(FSTPBMaybeDocument__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[FSTPBMaybeDocument class] + rootClass:[FSTPBMaybeDocumentRoot class] + file:FSTPBMaybeDocumentRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(FSTPBMaybeDocument__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "documentType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void FSTPBMaybeDocument_ClearDocumentTypeOneOfCase(FSTPBMaybeDocument *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h new file mode 100644 index 0000000..0089632 --- /dev/null +++ b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h @@ -0,0 +1,138 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: firestore/local/mutation.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSWrite; +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTPBMutationRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface FSTPBMutationRoot : GPBRootObject +@end + +#pragma mark - FSTPBMutationQueue + +typedef GPB_ENUM(FSTPBMutationQueue_FieldNumber) { + FSTPBMutationQueue_FieldNumber_LastAcknowledgedBatchId = 1, + FSTPBMutationQueue_FieldNumber_LastStreamToken = 2, +}; + +/** + * Each user gets a single queue of WriteBatches to apply to the server. + * MutationQueue tracks the metadata about the queue. + **/ +@interface FSTPBMutationQueue : GPBMessage + +/** + * An identifier for the highest numbered batch that has been acknowledged by + * the server. All WriteBatches in this queue with batch_ids less than or + * equal to this value are considered to have been acknowledged by the + * server. + **/ +@property(nonatomic, readwrite) int32_t lastAcknowledgedBatchId; + +/** + * A stream token that was previously sent by the server. + * + * See StreamingWriteRequest in datastore.proto for more details about usage. + * + * After sending this token, earlier tokens may not be used anymore so only a + * single stream token is retained. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *lastStreamToken; + +@end + +#pragma mark - FSTPBWriteBatch + +typedef GPB_ENUM(FSTPBWriteBatch_FieldNumber) { + FSTPBWriteBatch_FieldNumber_BatchId = 1, + FSTPBWriteBatch_FieldNumber_WritesArray = 2, + FSTPBWriteBatch_FieldNumber_LocalWriteTime = 3, +}; + +/** + * Message containing a batch of user-level writes intended to be sent to + * the server in a single call. Each user-level batch gets a separate + * WriteBatch with a new batch_id. + **/ +@interface FSTPBWriteBatch : GPBMessage + +/** + * An identifier for this batch, allocated by the mutation queue in a + * monotonically increasing manner. + **/ +@property(nonatomic, readwrite) int32_t batchId; + +/** A list of writes to apply. All writes will be applied atomically. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *writesArray; +/** The number of items in @c writesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger writesArray_Count; + +/** The local time at which the write batch was initiated. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *localWriteTime; +/** Test to see if @c localWriteTime has been set. */ +@property(nonatomic, readwrite) BOOL hasLocalWriteTime; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m new file mode 100644 index 0000000..8034143 --- /dev/null +++ b/Firestore/Protos/objc/firestore/local/Mutation.pbobjc.m @@ -0,0 +1,190 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: firestore/local/mutation.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Timestamp.pbobjc.h" +#endif + + #import "Mutation.pbobjc.h" + #import "Write.pbobjc.h" + #import "Annotations.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#pragma mark - FSTPBMutationRoot + +@implementation FSTPBMutationRoot + + +@end + +#pragma mark - FSTPBMutationRoot_FileDescriptor + +static GPBFileDescriptor *FSTPBMutationRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"firestore.client" + objcPrefix:@"FSTPB" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - FSTPBMutationQueue + +@implementation FSTPBMutationQueue + +@dynamic lastAcknowledgedBatchId; +@dynamic lastStreamToken; + +typedef struct FSTPBMutationQueue__storage_ { + uint32_t _has_storage_[1]; + int32_t lastAcknowledgedBatchId; + NSData *lastStreamToken; +} FSTPBMutationQueue__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "lastAcknowledgedBatchId", + .dataTypeSpecific.className = NULL, + .number = FSTPBMutationQueue_FieldNumber_LastAcknowledgedBatchId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(FSTPBMutationQueue__storage_, lastAcknowledgedBatchId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "lastStreamToken", + .dataTypeSpecific.className = NULL, + .number = FSTPBMutationQueue_FieldNumber_LastStreamToken, + .hasIndex = 1, + .offset = (uint32_t)offsetof(FSTPBMutationQueue__storage_, lastStreamToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[FSTPBMutationQueue class] + rootClass:[FSTPBMutationRoot class] + file:FSTPBMutationRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(FSTPBMutationQueue__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - FSTPBWriteBatch + +@implementation FSTPBWriteBatch + +@dynamic batchId; +@dynamic writesArray, writesArray_Count; +@dynamic hasLocalWriteTime, localWriteTime; + +typedef struct FSTPBWriteBatch__storage_ { + uint32_t _has_storage_[1]; + int32_t batchId; + NSMutableArray *writesArray; + GPBTimestamp *localWriteTime; +} FSTPBWriteBatch__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "batchId", + .dataTypeSpecific.className = NULL, + .number = FSTPBWriteBatch_FieldNumber_BatchId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(FSTPBWriteBatch__storage_, batchId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "writesArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSWrite), + .number = FSTPBWriteBatch_FieldNumber_WritesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(FSTPBWriteBatch__storage_, writesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "localWriteTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = FSTPBWriteBatch_FieldNumber_LocalWriteTime, + .hasIndex = 1, + .offset = (uint32_t)offsetof(FSTPBWriteBatch__storage_, localWriteTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[FSTPBWriteBatch class] + rootClass:[FSTPBMutationRoot class] + file:FSTPBMutationRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(FSTPBWriteBatch__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/firestore/local/Target.pbobjc.h b/Firestore/Protos/objc/firestore/local/Target.pbobjc.h new file mode 100644 index 0000000..d8bf49c --- /dev/null +++ b/Firestore/Protos/objc/firestore/local/Target.pbobjc.h @@ -0,0 +1,208 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: firestore/local/target.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSTarget_DocumentsTarget; +@class GCFSTarget_QueryTarget; +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTPBTargetRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface FSTPBTargetRoot : GPBRootObject +@end + +#pragma mark - FSTPBTarget + +typedef GPB_ENUM(FSTPBTarget_FieldNumber) { + FSTPBTarget_FieldNumber_TargetId = 1, + FSTPBTarget_FieldNumber_SnapshotVersion = 2, + FSTPBTarget_FieldNumber_ResumeToken = 3, + FSTPBTarget_FieldNumber_LastListenSequenceNumber = 4, + FSTPBTarget_FieldNumber_Query = 5, + FSTPBTarget_FieldNumber_Documents = 6, +}; + +typedef GPB_ENUM(FSTPBTarget_TargetType_OneOfCase) { + FSTPBTarget_TargetType_OneOfCase_GPBUnsetOneOfCase = 0, + FSTPBTarget_TargetType_OneOfCase_Query = 5, + FSTPBTarget_TargetType_OneOfCase_Documents = 6, +}; + +/** + * A Target is a long-lived data structure representing a resumable listen on a + * particular user query. While the query describes what to listen to, the + * Target records data about when the results were last updated and enough + * information to be able to resume listening later. + **/ +@interface FSTPBTarget : GPBMessage + +/** + * An auto-generated sequential numeric identifier for the target. This + * serves as the identity of the target, and once assigned never changes. + **/ +@property(nonatomic, readwrite) int32_t targetId; + +/** + * The last snapshot version received from the Watch Service for this target. + * + * This is the same value as TargetChange.read_time + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *snapshotVersion; +/** Test to see if @c snapshotVersion has been set. */ +@property(nonatomic, readwrite) BOOL hasSnapshotVersion; + +/** + * An opaque, server-assigned token that allows watching a query to be + * resumed after disconnecting without retransmitting all the data that + * matches the query. The resume token essentially identifies a point in + * time from which the server should resume sending results. + * + * This is related to the snapshot_version in that the resume_token + * effectively also encodes that value, but the resume_token is opaque and + * sometimes encodes additional information. + * + * A consequence of this is that the resume_token should be used when asking + * the server to reason about where this client is in the watch stream, but + * the client should use the snapshot_version for its own purposes. + * + * This is the same value as TargetChange.resume_token + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *resumeToken; + +/** + * A sequence number representing the last time this query was listened to, + * used for garbage collection purposes. + * + * Conventionally this would be a timestamp value, but device-local clocks + * are unreliable and they must be able to create new listens even while + * disconnected. Instead this should be a monotonically increasing number + * that's incremented on each listen call. + * + * This is different from the target_id since the target_id is an immutable + * identifier assigned to the Target on first use while + * last_listen_sequence_number is updated every time the query is listened + * to. + **/ +@property(nonatomic, readwrite) int64_t lastListenSequenceNumber; + +/** The server-side type of target to listen to. */ +@property(nonatomic, readonly) FSTPBTarget_TargetType_OneOfCase targetTypeOneOfCase; + +/** A target specified by a query. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_QueryTarget *query; + +/** A target specified by a set of document names. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_DocumentsTarget *documents; + +@end + +/** + * Clears whatever value was set for the oneof 'targetType'. + **/ +void FSTPBTarget_ClearTargetTypeOneOfCase(FSTPBTarget *message); + +#pragma mark - FSTPBTargetGlobal + +typedef GPB_ENUM(FSTPBTargetGlobal_FieldNumber) { + FSTPBTargetGlobal_FieldNumber_HighestTargetId = 1, + FSTPBTargetGlobal_FieldNumber_HighestListenSequenceNumber = 2, + FSTPBTargetGlobal_FieldNumber_LastRemoteSnapshotVersion = 3, +}; + +/** + * Global state tracked across all Targets, tracked separately to avoid the + * need for extra indexes. + **/ +@interface FSTPBTargetGlobal : GPBMessage + +/** + * The highest numbered target id across all Targets. + * + * See Target.target_id. + **/ +@property(nonatomic, readwrite) int32_t highestTargetId; + +/** + * The highest numbered last_listen_sequence_number across all Targets. + * + * See Target.last_listen_sequence_number. + **/ +@property(nonatomic, readwrite) int64_t highestListenSequenceNumber; + +/** + * A global snapshot version representing the last consistent snapshot we + * received from the backend. This is monotonically increasing and any + * snapshots received from the backend prior to this version (e.g. for + * targets resumed with a resume_token) should be suppressed (buffered) until + * the backend has caught up to this snapshot_version again. This prevents + * our cache from ever going backwards in time. + * + * This is updated whenever our we get a TargetChange with a read_time and + * empty target_ids. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *lastRemoteSnapshotVersion; +/** Test to see if @c lastRemoteSnapshotVersion has been set. */ +@property(nonatomic, readwrite) BOOL hasLastRemoteSnapshotVersion; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/firestore/local/Target.pbobjc.m b/Firestore/Protos/objc/firestore/local/Target.pbobjc.m new file mode 100644 index 0000000..6f6ccf2 --- /dev/null +++ b/Firestore/Protos/objc/firestore/local/Target.pbobjc.m @@ -0,0 +1,247 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: firestore/local/target.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Timestamp.pbobjc.h" +#endif + + #import "Target.pbobjc.h" + #import "Firestore.pbobjc.h" + #import "Annotations.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - FSTPBTargetRoot + +@implementation FSTPBTargetRoot + + +@end + +#pragma mark - FSTPBTargetRoot_FileDescriptor + +static GPBFileDescriptor *FSTPBTargetRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"firestore.client" + objcPrefix:@"FSTPB" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - FSTPBTarget + +@implementation FSTPBTarget + +@dynamic targetTypeOneOfCase; +@dynamic targetId; +@dynamic hasSnapshotVersion, snapshotVersion; +@dynamic resumeToken; +@dynamic lastListenSequenceNumber; +@dynamic query; +@dynamic documents; + +typedef struct FSTPBTarget__storage_ { + uint32_t _has_storage_[2]; + int32_t targetId; + GPBTimestamp *snapshotVersion; + NSData *resumeToken; + GCFSTarget_QueryTarget *query; + GCFSTarget_DocumentsTarget *documents; + int64_t lastListenSequenceNumber; +} FSTPBTarget__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "targetId", + .dataTypeSpecific.className = NULL, + .number = FSTPBTarget_FieldNumber_TargetId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(FSTPBTarget__storage_, targetId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "snapshotVersion", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = FSTPBTarget_FieldNumber_SnapshotVersion, + .hasIndex = 1, + .offset = (uint32_t)offsetof(FSTPBTarget__storage_, snapshotVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "resumeToken", + .dataTypeSpecific.className = NULL, + .number = FSTPBTarget_FieldNumber_ResumeToken, + .hasIndex = 2, + .offset = (uint32_t)offsetof(FSTPBTarget__storage_, resumeToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "lastListenSequenceNumber", + .dataTypeSpecific.className = NULL, + .number = FSTPBTarget_FieldNumber_LastListenSequenceNumber, + .hasIndex = 3, + .offset = (uint32_t)offsetof(FSTPBTarget__storage_, lastListenSequenceNumber), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt64, + }, + { + .name = "query", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_QueryTarget), + .number = FSTPBTarget_FieldNumber_Query, + .hasIndex = -1, + .offset = (uint32_t)offsetof(FSTPBTarget__storage_, query), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "documents", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_DocumentsTarget), + .number = FSTPBTarget_FieldNumber_Documents, + .hasIndex = -1, + .offset = (uint32_t)offsetof(FSTPBTarget__storage_, documents), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[FSTPBTarget class] + rootClass:[FSTPBTargetRoot class] + file:FSTPBTargetRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(FSTPBTarget__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "targetType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void FSTPBTarget_ClearTargetTypeOneOfCase(FSTPBTarget *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - FSTPBTargetGlobal + +@implementation FSTPBTargetGlobal + +@dynamic highestTargetId; +@dynamic highestListenSequenceNumber; +@dynamic hasLastRemoteSnapshotVersion, lastRemoteSnapshotVersion; + +typedef struct FSTPBTargetGlobal__storage_ { + uint32_t _has_storage_[1]; + int32_t highestTargetId; + GPBTimestamp *lastRemoteSnapshotVersion; + int64_t highestListenSequenceNumber; +} FSTPBTargetGlobal__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "highestTargetId", + .dataTypeSpecific.className = NULL, + .number = FSTPBTargetGlobal_FieldNumber_HighestTargetId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(FSTPBTargetGlobal__storage_, highestTargetId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "highestListenSequenceNumber", + .dataTypeSpecific.className = NULL, + .number = FSTPBTargetGlobal_FieldNumber_HighestListenSequenceNumber, + .hasIndex = 1, + .offset = (uint32_t)offsetof(FSTPBTargetGlobal__storage_, highestListenSequenceNumber), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt64, + }, + { + .name = "lastRemoteSnapshotVersion", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = FSTPBTargetGlobal_FieldNumber_LastRemoteSnapshotVersion, + .hasIndex = 2, + .offset = (uint32_t)offsetof(FSTPBTargetGlobal__storage_, lastRemoteSnapshotVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[FSTPBTargetGlobal class] + rootClass:[FSTPBTargetRoot class] + file:FSTPBTargetRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(FSTPBTargetGlobal__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/api/Annotations.pbobjc.h b/Firestore/Protos/objc/google/api/Annotations.pbobjc.h new file mode 100644 index 0000000..b7bee2d --- /dev/null +++ b/Firestore/Protos/objc/google/api/Annotations.pbobjc.h @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Empty stub file diff --git a/Firestore/Protos/objc/google/api/Annotations.pbobjc.m b/Firestore/Protos/objc/google/api/Annotations.pbobjc.m new file mode 100644 index 0000000..ef0558d --- /dev/null +++ b/Firestore/Protos/objc/google/api/Annotations.pbobjc.m @@ -0,0 +1,17 @@ +/* + * 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. + */ + +static int annotations_stub __attribute__((unused,used)) = 0; diff --git a/Firestore/Protos/objc/google/api/HTTP.pbobjc.h b/Firestore/Protos/objc/google/api/HTTP.pbobjc.h new file mode 100644 index 0000000..9cc00dc --- /dev/null +++ b/Firestore/Protos/objc/google/api/HTTP.pbobjc.h @@ -0,0 +1,406 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/api/http.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GAPICustomHttpPattern; +@class GAPIHttpRule; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - GAPIHTTPRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GAPIHTTPRoot : GPBRootObject +@end + +#pragma mark - GAPIHttp + +typedef GPB_ENUM(GAPIHttp_FieldNumber) { + GAPIHttp_FieldNumber_RulesArray = 1, +}; + +/** + * Defines the HTTP configuration for a service. It contains a list of + * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method + * to one or more HTTP REST API methods. + **/ +@interface GAPIHttp : GPBMessage + +/** + * A list of HTTP configuration rules that apply to individual API methods. + * + * **NOTE:** All service configuration rules follow "last one wins" order. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *rulesArray; +/** The number of items in @c rulesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger rulesArray_Count; + +@end + +#pragma mark - GAPIHttpRule + +typedef GPB_ENUM(GAPIHttpRule_FieldNumber) { + GAPIHttpRule_FieldNumber_Selector = 1, + GAPIHttpRule_FieldNumber_Get = 2, + GAPIHttpRule_FieldNumber_Put = 3, + GAPIHttpRule_FieldNumber_Post = 4, + GAPIHttpRule_FieldNumber_Delete_p = 5, + GAPIHttpRule_FieldNumber_Patch = 6, + GAPIHttpRule_FieldNumber_Body = 7, + GAPIHttpRule_FieldNumber_Custom = 8, + GAPIHttpRule_FieldNumber_AdditionalBindingsArray = 11, +}; + +typedef GPB_ENUM(GAPIHttpRule_Pattern_OneOfCase) { + GAPIHttpRule_Pattern_OneOfCase_GPBUnsetOneOfCase = 0, + GAPIHttpRule_Pattern_OneOfCase_Get = 2, + GAPIHttpRule_Pattern_OneOfCase_Put = 3, + GAPIHttpRule_Pattern_OneOfCase_Post = 4, + GAPIHttpRule_Pattern_OneOfCase_Delete_p = 5, + GAPIHttpRule_Pattern_OneOfCase_Patch = 6, + GAPIHttpRule_Pattern_OneOfCase_Custom = 8, +}; + +/** + * `HttpRule` defines the mapping of an RPC method to one or more HTTP + * REST APIs. The mapping determines what portions of the request + * message are populated from the path, query parameters, or body of + * the HTTP request. The mapping is typically specified as an + * `google.api.http` annotation, see "google/api/annotations.proto" + * for details. + * + * The mapping consists of a field specifying the path template and + * method kind. The path template can refer to fields in the request + * message, as in the example below which describes a REST GET + * operation on a resource collection of messages: + * + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; + * } + * } + * message GetMessageRequest { + * message SubMessage { + * string subfield = 1; + * } + * string message_id = 1; // mapped to the URL + * SubMessage sub = 2; // `sub.subfield` is url-mapped + * } + * message Message { + * string text = 1; // content of the resource + * } + * + * The same http annotation can alternatively be expressed inside the + * `GRPC API Configuration` YAML file. + * + * http: + * rules: + * - selector: .Messaging.GetMessage + * get: /v1/messages/{message_id}/{sub.subfield} + * + * This definition enables an automatic, bidrectional mapping of HTTP + * JSON to RPC. Example: + * + * HTTP | RPC + * -----|----- + * `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` + * + * In general, not only fields but also field paths can be referenced + * from a path pattern. Fields mapped to the path pattern cannot be + * repeated and must have a primitive (non-message) type. + * + * Any fields in the request message which are not bound by the path + * pattern automatically become (optional) HTTP query + * parameters. Assume the following definition of the request message: + * + * + * message GetMessageRequest { + * message SubMessage { + * string subfield = 1; + * } + * string message_id = 1; // mapped to the URL + * int64 revision = 2; // becomes a parameter + * SubMessage sub = 3; // `sub.subfield` becomes a parameter + * } + * + * + * This enables a HTTP JSON to RPC mapping as below: + * + * HTTP | RPC + * -----|----- + * `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` + * + * Note that fields which are mapped to HTTP parameters must have a + * primitive type or a repeated primitive type. Message types are not + * allowed. In the case of a repeated type, the parameter can be + * repeated in the URL, as in `...?param=A¶m=B`. + * + * For HTTP method kinds which allow a request body, the `body` field + * specifies the mapping. Consider a REST update method on the + * message resource collection: + * + * + * service Messaging { + * rpc UpdateMessage(UpdateMessageRequest) returns (Message) { + * option (google.api.http) = { + * put: "/v1/messages/{message_id}" + * body: "message" + * }; + * } + * } + * message UpdateMessageRequest { + * string message_id = 1; // mapped to the URL + * Message message = 2; // mapped to the body + * } + * + * + * The following HTTP JSON to RPC mapping is enabled, where the + * representation of the JSON in the request body is determined by + * protos JSON encoding: + * + * HTTP | RPC + * -----|----- + * `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` + * + * The special name `*` can be used in the body mapping to define that + * every field not bound by the path template should be mapped to the + * request body. This enables the following alternative definition of + * the update method: + * + * service Messaging { + * rpc UpdateMessage(Message) returns (Message) { + * option (google.api.http) = { + * put: "/v1/messages/{message_id}" + * body: "*" + * }; + * } + * } + * message Message { + * string message_id = 1; + * string text = 2; + * } + * + * + * The following HTTP JSON to RPC mapping is enabled: + * + * HTTP | RPC + * -----|----- + * `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` + * + * Note that when using `*` in the body mapping, it is not possible to + * have HTTP parameters, as all fields not bound by the path end in + * the body. This makes this option more rarely used in practice of + * defining REST APIs. The common usage of `*` is in custom methods + * which don't use the URL at all for transferring data. + * + * It is possible to define multiple HTTP methods for one RPC by using + * the `additional_bindings` option. Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/messages/{message_id}" + * additional_bindings { + * get: "/v1/users/{user_id}/messages/{message_id}" + * } + * }; + * } + * } + * message GetMessageRequest { + * string message_id = 1; + * string user_id = 2; + * } + * + * + * This enables the following two alternative HTTP JSON to RPC + * mappings: + * + * HTTP | RPC + * -----|----- + * `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` + * `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` + * + * # Rules for HTTP mapping + * + * The rules for mapping HTTP path, query parameters, and body fields + * to the request message are as follows: + * + * 1. The `body` field specifies either `*` or a field path, or is + * omitted. If omitted, it assumes there is no HTTP body. + * 2. Leaf fields (recursive expansion of nested messages in the + * request) can be classified into three types: + * (a) Matched in the URL template. + * (b) Covered by body (if body is `*`, everything except (a) fields; + * else everything under the body field) + * (c) All other fields. + * 3. URL query parameters found in the HTTP request are mapped to (c) fields. + * 4. Any body sent with an HTTP request can contain only (b) fields. + * + * The syntax of the path template is as follows: + * + * Template = "/" Segments [ Verb ] ; + * Segments = Segment { "/" Segment } ; + * Segment = "*" | "**" | LITERAL | Variable ; + * Variable = "{" FieldPath [ "=" Segments ] "}" ; + * FieldPath = IDENT { "." IDENT } ; + * Verb = ":" LITERAL ; + * + * The syntax `*` matches a single path segment. It follows the semantics of + * [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String + * Expansion. + * + * The syntax `**` matches zero or more path segments. It follows the semantics + * of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved + * Expansion. NOTE: it must be the last segment in the path except the Verb. + * + * The syntax `LITERAL` matches literal text in the URL path. + * + * The syntax `Variable` matches the entire path as specified by its template; + * this nested template must not contain further variables. If a variable + * matches a single path segment, its template may be omitted, e.g. `{var}` + * is equivalent to `{var=*}`. + * + * NOTE: the field paths in variables and in the `body` must not refer to + * repeated fields or map fields. + * + * Use CustomHttpPattern to specify any HTTP method that is not included in the + * `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for + * a given URL path rule. The wild-card rule is useful for services that provide + * content to Web (HTML) clients. + **/ +@interface GAPIHttpRule : GPBMessage + +/** + * Selects methods to which this rule applies. + * + * Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *selector; + +/** + * Determines the URL pattern is matched by this rules. This pattern can be + * used with any of the {get|put|post|delete|patch} methods. A custom method + * can be defined using the 'custom' field. + **/ +@property(nonatomic, readonly) GAPIHttpRule_Pattern_OneOfCase patternOneOfCase; + +/** Used for listing and getting information about resources. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *get; + +/** Used for updating a resource. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *put; + +/** Used for creating a resource. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *post; + +/** Used for deleting a resource. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *delete_p; + +/** Used for updating a resource. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *patch; + +/** Custom pattern is used for defining custom verbs. */ +@property(nonatomic, readwrite, strong, null_resettable) GAPICustomHttpPattern *custom; + +/** + * The name of the request field whose value is mapped to the HTTP body, or + * `*` for mapping all fields not captured by the path pattern to the HTTP + * body. NOTE: the referred field must not be a repeated field and must be + * present at the top-level of request message type. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *body; + +/** + * Additional HTTP bindings for the selector. Nested bindings must + * not contain an `additional_bindings` field themselves (that is, + * the nesting may only be one level deep). + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *additionalBindingsArray; +/** The number of items in @c additionalBindingsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger additionalBindingsArray_Count; + +@end + +/** + * Clears whatever value was set for the oneof 'pattern'. + **/ +void GAPIHttpRule_ClearPatternOneOfCase(GAPIHttpRule *message); + +#pragma mark - GAPICustomHttpPattern + +typedef GPB_ENUM(GAPICustomHttpPattern_FieldNumber) { + GAPICustomHttpPattern_FieldNumber_Kind = 1, + GAPICustomHttpPattern_FieldNumber_Path = 2, +}; + +/** + * A custom pattern is used for defining custom HTTP verb. + **/ +@interface GAPICustomHttpPattern : GPBMessage + +/** The name of this custom HTTP verb. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *kind; + +/** The path matched by this custom verb. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *path; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/api/HTTP.pbobjc.m b/Firestore/Protos/objc/google/api/HTTP.pbobjc.m new file mode 100644 index 0000000..5adf41c --- /dev/null +++ b/Firestore/Protos/objc/google/api/HTTP.pbobjc.m @@ -0,0 +1,306 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/api/http.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + + #import "HTTP.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - GAPIHTTPRoot + +@implementation GAPIHTTPRoot + +// No extensions in the file and no imports, so no need to generate +// +extensionRegistry. + +@end + +#pragma mark - GAPIHTTPRoot_FileDescriptor + +static GPBFileDescriptor *GAPIHTTPRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.api" + objcPrefix:@"GAPI" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GAPIHttp + +@implementation GAPIHttp + +@dynamic rulesArray, rulesArray_Count; + +typedef struct GAPIHttp__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *rulesArray; +} GAPIHttp__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "rulesArray", + .dataTypeSpecific.className = GPBStringifySymbol(GAPIHttpRule), + .number = GAPIHttp_FieldNumber_RulesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GAPIHttp__storage_, rulesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GAPIHttp class] + rootClass:[GAPIHTTPRoot class] + file:GAPIHTTPRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GAPIHttp__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GAPIHttpRule + +@implementation GAPIHttpRule + +@dynamic patternOneOfCase; +@dynamic selector; +@dynamic get; +@dynamic put; +@dynamic post; +@dynamic delete_p; +@dynamic patch; +@dynamic custom; +@dynamic body; +@dynamic additionalBindingsArray, additionalBindingsArray_Count; + +typedef struct GAPIHttpRule__storage_ { + uint32_t _has_storage_[2]; + NSString *selector; + NSString *get; + NSString *put; + NSString *post; + NSString *delete_p; + NSString *patch; + NSString *body; + GAPICustomHttpPattern *custom; + NSMutableArray *additionalBindingsArray; +} GAPIHttpRule__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "selector", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Selector, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, selector), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "get", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Get, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, get), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "put", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Put, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, put), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "post", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Post, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, post), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "delete_p", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Delete_p, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, delete_p), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "patch", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Patch, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, patch), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "body", + .dataTypeSpecific.className = NULL, + .number = GAPIHttpRule_FieldNumber_Body, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, body), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "custom", + .dataTypeSpecific.className = GPBStringifySymbol(GAPICustomHttpPattern), + .number = GAPIHttpRule_FieldNumber_Custom, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, custom), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "additionalBindingsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GAPIHttpRule), + .number = GAPIHttpRule_FieldNumber_AdditionalBindingsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GAPIHttpRule__storage_, additionalBindingsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GAPIHttpRule class] + rootClass:[GAPIHTTPRoot class] + file:GAPIHTTPRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GAPIHttpRule__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "pattern", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GAPIHttpRule_ClearPatternOneOfCase(GAPIHttpRule *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GAPICustomHttpPattern + +@implementation GAPICustomHttpPattern + +@dynamic kind; +@dynamic path; + +typedef struct GAPICustomHttpPattern__storage_ { + uint32_t _has_storage_[1]; + NSString *kind; + NSString *path; +} GAPICustomHttpPattern__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "kind", + .dataTypeSpecific.className = NULL, + .number = GAPICustomHttpPattern_FieldNumber_Kind, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GAPICustomHttpPattern__storage_, kind), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "path", + .dataTypeSpecific.className = NULL, + .number = GAPICustomHttpPattern_FieldNumber_Path, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GAPICustomHttpPattern__storage_, path), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GAPICustomHttpPattern class] + rootClass:[GAPIHTTPRoot class] + file:GAPIHTTPRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GAPICustomHttpPattern__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h new file mode 100644 index 0000000..6215e82 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h @@ -0,0 +1,223 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/common.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSTransactionOptions_ReadOnly; +@class GCFSTransactionOptions_ReadWrite; +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - GCFSCommonRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GCFSCommonRoot : GPBRootObject +@end + +#pragma mark - GCFSDocumentMask + +typedef GPB_ENUM(GCFSDocumentMask_FieldNumber) { + GCFSDocumentMask_FieldNumber_FieldPathsArray = 1, +}; + +/** + * A set of field paths on a document. + * Used to restrict a get or update operation on a document to a subset of its + * fields. + * This is different from standard field masks, as this is always scoped to a + * [Document][google.firestore.v1beta1.Document], and takes in account the dynamic nature of [Value][google.firestore.v1beta1.Value]. + **/ +@interface GCFSDocumentMask : GPBMessage + +/** + * The list of field paths in the mask. See [Document.fields][google.firestore.v1beta1.Document.fields] for a field + * path syntax reference. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *fieldPathsArray; +/** The number of items in @c fieldPathsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger fieldPathsArray_Count; + +@end + +#pragma mark - GCFSPrecondition + +typedef GPB_ENUM(GCFSPrecondition_FieldNumber) { + GCFSPrecondition_FieldNumber_Exists = 1, + GCFSPrecondition_FieldNumber_UpdateTime = 2, +}; + +typedef GPB_ENUM(GCFSPrecondition_ConditionType_OneOfCase) { + GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSPrecondition_ConditionType_OneOfCase_Exists = 1, + GCFSPrecondition_ConditionType_OneOfCase_UpdateTime = 2, +}; + +/** + * A precondition on a document, used for conditional operations. + **/ +@interface GCFSPrecondition : GPBMessage + +/** The type of precondition. */ +@property(nonatomic, readonly) GCFSPrecondition_ConditionType_OneOfCase conditionTypeOneOfCase; + +/** + * When set to `true`, the target document must exist. + * When set to `false`, the target document must not exist. + **/ +@property(nonatomic, readwrite) BOOL exists; + +/** + * When set, the target document must exist and have been last updated at + * that time. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *updateTime; + +@end + +/** + * Clears whatever value was set for the oneof 'conditionType'. + **/ +void GCFSPrecondition_ClearConditionTypeOneOfCase(GCFSPrecondition *message); + +#pragma mark - GCFSTransactionOptions + +typedef GPB_ENUM(GCFSTransactionOptions_FieldNumber) { + GCFSTransactionOptions_FieldNumber_ReadOnly = 2, + GCFSTransactionOptions_FieldNumber_ReadWrite = 3, +}; + +typedef GPB_ENUM(GCFSTransactionOptions_Mode_OneOfCase) { + GCFSTransactionOptions_Mode_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSTransactionOptions_Mode_OneOfCase_ReadOnly = 2, + GCFSTransactionOptions_Mode_OneOfCase_ReadWrite = 3, +}; + +/** + * Options for creating a new transaction. + **/ +@interface GCFSTransactionOptions : GPBMessage + +/** The mode of the transaction. */ +@property(nonatomic, readonly) GCFSTransactionOptions_Mode_OneOfCase modeOneOfCase; + +/** The transaction can only be used for read operations. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions_ReadOnly *readOnly; + +/** The transaction can be used for both read and write operations. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions_ReadWrite *readWrite; + +@end + +/** + * Clears whatever value was set for the oneof 'mode'. + **/ +void GCFSTransactionOptions_ClearModeOneOfCase(GCFSTransactionOptions *message); + +#pragma mark - GCFSTransactionOptions_ReadWrite + +typedef GPB_ENUM(GCFSTransactionOptions_ReadWrite_FieldNumber) { + GCFSTransactionOptions_ReadWrite_FieldNumber_RetryTransaction = 1, +}; + +/** + * Options for a transaction that can be used to read and write documents. + **/ +@interface GCFSTransactionOptions_ReadWrite : GPBMessage + +/** An optional transaction to retry. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *retryTransaction; + +@end + +#pragma mark - GCFSTransactionOptions_ReadOnly + +typedef GPB_ENUM(GCFSTransactionOptions_ReadOnly_FieldNumber) { + GCFSTransactionOptions_ReadOnly_FieldNumber_ReadTime = 2, +}; + +typedef GPB_ENUM(GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase) { + GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase_ReadTime = 2, +}; + +/** + * Options for a transaction that can only be used to read documents. + **/ +@interface GCFSTransactionOptions_ReadOnly : GPBMessage + +/** + * The consistency mode for this transaction. If not set, defaults to strong + * consistency. + **/ +@property(nonatomic, readonly) GCFSTransactionOptions_ReadOnly_ConsistencySelector_OneOfCase consistencySelectorOneOfCase; + +/** + * Reads documents at the given time. + * This may not be older than 60 seconds. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; + +@end + +/** + * Clears whatever value was set for the oneof 'consistencySelector'. + **/ +void GCFSTransactionOptions_ReadOnly_ClearConsistencySelectorOneOfCase(GCFSTransactionOptions_ReadOnly *message); + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m new file mode 100644 index 0000000..118f56e --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.m @@ -0,0 +1,345 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/common.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Timestamp.pbobjc.h" +#endif + + #import "Common.pbobjc.h" + #import "Annotations.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - GCFSCommonRoot + +@implementation GCFSCommonRoot + + +@end + +#pragma mark - GCFSCommonRoot_FileDescriptor + +static GPBFileDescriptor *GCFSCommonRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1" + objcPrefix:@"GCFS" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GCFSDocumentMask + +@implementation GCFSDocumentMask + +@dynamic fieldPathsArray, fieldPathsArray_Count; + +typedef struct GCFSDocumentMask__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *fieldPathsArray; +} GCFSDocumentMask__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "fieldPathsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentMask_FieldNumber_FieldPathsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocumentMask__storage_, fieldPathsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocumentMask class] + rootClass:[GCFSCommonRoot class] + file:GCFSCommonRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocumentMask__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSPrecondition + +@implementation GCFSPrecondition + +@dynamic conditionTypeOneOfCase; +@dynamic exists; +@dynamic updateTime; + +typedef struct GCFSPrecondition__storage_ { + uint32_t _has_storage_[2]; + GPBTimestamp *updateTime; +} GCFSPrecondition__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "exists", + .dataTypeSpecific.className = NULL, + .number = GCFSPrecondition_FieldNumber_Exists, + .hasIndex = -1, + .offset = 0, // Stored in _has_storage_ to save space. + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBool, + }, + { + .name = "updateTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSPrecondition_FieldNumber_UpdateTime, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSPrecondition__storage_, updateTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSPrecondition class] + rootClass:[GCFSCommonRoot class] + file:GCFSCommonRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSPrecondition__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "conditionType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSPrecondition_ClearConditionTypeOneOfCase(GCFSPrecondition *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSTransactionOptions + +@implementation GCFSTransactionOptions + +@dynamic modeOneOfCase; +@dynamic readOnly; +@dynamic readWrite; + +typedef struct GCFSTransactionOptions__storage_ { + uint32_t _has_storage_[2]; + GCFSTransactionOptions_ReadOnly *readOnly; + GCFSTransactionOptions_ReadWrite *readWrite; +} GCFSTransactionOptions__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "readOnly", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions_ReadOnly), + .number = GCFSTransactionOptions_FieldNumber_ReadOnly, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSTransactionOptions__storage_, readOnly), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "readWrite", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions_ReadWrite), + .number = GCFSTransactionOptions_FieldNumber_ReadWrite, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSTransactionOptions__storage_, readWrite), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTransactionOptions class] + rootClass:[GCFSCommonRoot class] + file:GCFSCommonRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTransactionOptions__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "mode", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSTransactionOptions_ClearModeOneOfCase(GCFSTransactionOptions *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSTransactionOptions_ReadWrite + +@implementation GCFSTransactionOptions_ReadWrite + +@dynamic retryTransaction; + +typedef struct GCFSTransactionOptions_ReadWrite__storage_ { + uint32_t _has_storage_[1]; + NSData *retryTransaction; +} GCFSTransactionOptions_ReadWrite__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "retryTransaction", + .dataTypeSpecific.className = NULL, + .number = GCFSTransactionOptions_ReadWrite_FieldNumber_RetryTransaction, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSTransactionOptions_ReadWrite__storage_, retryTransaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTransactionOptions_ReadWrite class] + rootClass:[GCFSCommonRoot class] + file:GCFSCommonRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTransactionOptions_ReadWrite__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTransactionOptions)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSTransactionOptions_ReadOnly + +@implementation GCFSTransactionOptions_ReadOnly + +@dynamic consistencySelectorOneOfCase; +@dynamic readTime; + +typedef struct GCFSTransactionOptions_ReadOnly__storage_ { + uint32_t _has_storage_[2]; + GPBTimestamp *readTime; +} GCFSTransactionOptions_ReadOnly__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSTransactionOptions_ReadOnly_FieldNumber_ReadTime, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSTransactionOptions_ReadOnly__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTransactionOptions_ReadOnly class] + rootClass:[GCFSCommonRoot class] + file:GCFSCommonRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTransactionOptions_ReadOnly__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "consistencySelector", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTransactionOptions)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSTransactionOptions_ReadOnly_ClearConsistencySelectorOneOfCase(GCFSTransactionOptions_ReadOnly *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h new file mode 100644 index 0000000..3c5bfb1 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h @@ -0,0 +1,309 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/document.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSArrayValue; +@class GCFSMapValue; +@class GCFSValue; +@class GPBTimestamp; +@class GTPLatLng; +GPB_ENUM_FWD_DECLARE(GPBNullValue); + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - GCFSDocumentRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GCFSDocumentRoot : GPBRootObject +@end + +#pragma mark - GCFSDocument + +typedef GPB_ENUM(GCFSDocument_FieldNumber) { + GCFSDocument_FieldNumber_Name = 1, + GCFSDocument_FieldNumber_Fields = 2, + GCFSDocument_FieldNumber_CreateTime = 3, + GCFSDocument_FieldNumber_UpdateTime = 4, +}; + +/** + * A Firestore document. + * + * Must not exceed 1 MiB - 4 bytes. + **/ +@interface GCFSDocument : GPBMessage + +/** + * The resource name of the document, for example + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *name; + +/** + * The document's fields. + * + * The map keys represent field names. + * + * A simple field name contains only characters `a` to `z`, `A` to `Z`, + * `0` to `9`, or `_`, and must not start with `0` to `9` or `_`. For example, + * `foo_bar_17`. + * + * Field names matching the regular expression `__.*__` are reserved. Reserved + * field names are forbidden except in certain documented contexts. The map + * keys, represented as UTF-8, must not exceed 1,500 bytes and cannot be + * empty. + * + * Field paths may be used in other contexts to refer to structured fields + * defined here. For `map_value`, the field path is represented by the simple + * or quoted field names of the containing fields, delimited by `.`. For + * example, the structured field + * `"foo" : { map_value: { "x&y" : { string_value: "hello" }}}` would be + * represented by the field path `foo.x&y`. + * + * Within a field path, a quoted field name starts and ends with `` ` `` and + * may contain any character. Some characters, including `` ` ``, must be + * escaped using a `\\`. For example, `` `x&y` `` represents `x&y` and + * `` `bak\\`tik` `` represents `` bak`tik ``. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary *fields; +/** The number of items in @c fields without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger fields_Count; + +/** + * Output only. The time at which the document was created. + * + * This value increases monotonically when a document is deleted then + * recreated. It can also be compared to values from other documents and + * the `read_time` of a query. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *createTime; +/** Test to see if @c createTime has been set. */ +@property(nonatomic, readwrite) BOOL hasCreateTime; + +/** + * Output only. The time at which the document was last changed. + * + * This value is initally set to the `create_time` then increases + * monotonically with each change to the document. It can also be + * compared to values from other documents and the `read_time` of a query. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *updateTime; +/** Test to see if @c updateTime has been set. */ +@property(nonatomic, readwrite) BOOL hasUpdateTime; + +@end + +#pragma mark - GCFSValue + +typedef GPB_ENUM(GCFSValue_FieldNumber) { + GCFSValue_FieldNumber_BooleanValue = 1, + GCFSValue_FieldNumber_IntegerValue = 2, + GCFSValue_FieldNumber_DoubleValue = 3, + GCFSValue_FieldNumber_ReferenceValue = 5, + GCFSValue_FieldNumber_MapValue = 6, + GCFSValue_FieldNumber_GeoPointValue = 8, + GCFSValue_FieldNumber_ArrayValue = 9, + GCFSValue_FieldNumber_TimestampValue = 10, + GCFSValue_FieldNumber_NullValue = 11, + GCFSValue_FieldNumber_StringValue = 17, + GCFSValue_FieldNumber_BytesValue = 18, +}; + +typedef GPB_ENUM(GCFSValue_ValueType_OneOfCase) { + GCFSValue_ValueType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSValue_ValueType_OneOfCase_NullValue = 11, + GCFSValue_ValueType_OneOfCase_BooleanValue = 1, + GCFSValue_ValueType_OneOfCase_IntegerValue = 2, + GCFSValue_ValueType_OneOfCase_DoubleValue = 3, + GCFSValue_ValueType_OneOfCase_TimestampValue = 10, + GCFSValue_ValueType_OneOfCase_StringValue = 17, + GCFSValue_ValueType_OneOfCase_BytesValue = 18, + GCFSValue_ValueType_OneOfCase_ReferenceValue = 5, + GCFSValue_ValueType_OneOfCase_GeoPointValue = 8, + GCFSValue_ValueType_OneOfCase_ArrayValue = 9, + GCFSValue_ValueType_OneOfCase_MapValue = 6, +}; + +/** + * A message that can hold any of the supported value types. + **/ +@interface GCFSValue : GPBMessage + +/** Must have a value set. */ +@property(nonatomic, readonly) GCFSValue_ValueType_OneOfCase valueTypeOneOfCase; + +/** A null value. */ +@property(nonatomic, readwrite) enum GPBNullValue nullValue; + +/** A boolean value. */ +@property(nonatomic, readwrite) BOOL booleanValue; + +/** An integer value. */ +@property(nonatomic, readwrite) int64_t integerValue; + +/** A double value. */ +@property(nonatomic, readwrite) double doubleValue; + +/** + * A timestamp value. + * + * Precise only to microseconds. When stored, any additional precision is + * rounded down. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *timestampValue; + +/** + * A string value. + * + * The string, represented as UTF-8, must not exceed 1 MiB - 89 bytes. + * Only the first 1,500 bytes of the UTF-8 representation are considered by + * queries. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *stringValue; + +/** + * A bytes value. + * + * Must not exceed 1 MiB - 89 bytes. + * Only the first 1,500 bytes are considered by queries. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *bytesValue; + +/** + * A reference to a document. For example: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *referenceValue; + +/** A geo point value representing a point on the surface of Earth. */ +@property(nonatomic, readwrite, strong, null_resettable) GTPLatLng *geoPointValue; + +/** + * An array value. + * + * Cannot contain another array value. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSArrayValue *arrayValue; + +/** A map value. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSMapValue *mapValue; + +@end + +/** + * Fetches the raw value of a @c GCFSValue's @c nullValue property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSValue_NullValue_RawValue(GCFSValue *message); +/** + * Sets the raw value of an @c GCFSValue's @c nullValue property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSValue_NullValue_RawValue(GCFSValue *message, int32_t value); + +/** + * Clears whatever value was set for the oneof 'valueType'. + **/ +void GCFSValue_ClearValueTypeOneOfCase(GCFSValue *message); + +#pragma mark - GCFSArrayValue + +typedef GPB_ENUM(GCFSArrayValue_FieldNumber) { + GCFSArrayValue_FieldNumber_ValuesArray = 1, +}; + +/** + * An array value. + **/ +@interface GCFSArrayValue : GPBMessage + +/** Values in the array. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *valuesArray; +/** The number of items in @c valuesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger valuesArray_Count; + +@end + +#pragma mark - GCFSMapValue + +typedef GPB_ENUM(GCFSMapValue_FieldNumber) { + GCFSMapValue_FieldNumber_Fields = 1, +}; + +/** + * A map value. + **/ +@interface GCFSMapValue : GPBMessage + +/** + * The map's fields. + * + * The map keys represent field names. Field names matching the regular + * expression `__.*__` are reserved. Reserved field names are forbidden except + * in certain documented contexts. The map keys, represented as UTF-8, must + * not exceed 1,500 bytes and cannot be empty. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary *fields; +/** The number of items in @c fields without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger fields_Count; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m new file mode 100644 index 0000000..2c805c3 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.m @@ -0,0 +1,412 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/document.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import + #import +#else + #import "Struct.pbobjc.h" + #import "Timestamp.pbobjc.h" +#endif + + #import "Document.pbobjc.h" + #import "Annotations.pbobjc.h" + #import "Latlng.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - GCFSDocumentRoot + +@implementation GCFSDocumentRoot + + +@end + +#pragma mark - GCFSDocumentRoot_FileDescriptor + +static GPBFileDescriptor *GCFSDocumentRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1" + objcPrefix:@"GCFS" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GCFSDocument + +@implementation GCFSDocument + +@dynamic name; +@dynamic fields, fields_Count; +@dynamic hasCreateTime, createTime; +@dynamic hasUpdateTime, updateTime; + +typedef struct GCFSDocument__storage_ { + uint32_t _has_storage_[1]; + NSString *name; + NSMutableDictionary *fields; + GPBTimestamp *createTime; + GPBTimestamp *updateTime; +} GCFSDocument__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "name", + .dataTypeSpecific.className = NULL, + .number = GCFSDocument_FieldNumber_Name, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDocument__storage_, name), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "fields", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue), + .number = GCFSDocument_FieldNumber_Fields, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocument__storage_, fields), + .flags = GPBFieldMapKeyString, + .dataType = GPBDataTypeMessage, + }, + { + .name = "createTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSDocument_FieldNumber_CreateTime, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSDocument__storage_, createTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "updateTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSDocument_FieldNumber_UpdateTime, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSDocument__storage_, updateTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocument class] + rootClass:[GCFSDocumentRoot class] + file:GCFSDocumentRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocument__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSValue + +@implementation GCFSValue + +@dynamic valueTypeOneOfCase; +@dynamic nullValue; +@dynamic booleanValue; +@dynamic integerValue; +@dynamic doubleValue; +@dynamic timestampValue; +@dynamic stringValue; +@dynamic bytesValue; +@dynamic referenceValue; +@dynamic geoPointValue; +@dynamic arrayValue; +@dynamic mapValue; + +typedef struct GCFSValue__storage_ { + uint32_t _has_storage_[2]; + GPBNullValue nullValue; + NSString *referenceValue; + GCFSMapValue *mapValue; + GTPLatLng *geoPointValue; + GCFSArrayValue *arrayValue; + GPBTimestamp *timestampValue; + NSString *stringValue; + NSData *bytesValue; + int64_t integerValue; + double doubleValue; +} GCFSValue__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "booleanValue", + .dataTypeSpecific.className = NULL, + .number = GCFSValue_FieldNumber_BooleanValue, + .hasIndex = -1, + .offset = 0, // Stored in _has_storage_ to save space. + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBool, + }, + { + .name = "integerValue", + .dataTypeSpecific.className = NULL, + .number = GCFSValue_FieldNumber_IntegerValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, integerValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt64, + }, + { + .name = "doubleValue", + .dataTypeSpecific.className = NULL, + .number = GCFSValue_FieldNumber_DoubleValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, doubleValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeDouble, + }, + { + .name = "referenceValue", + .dataTypeSpecific.className = NULL, + .number = GCFSValue_FieldNumber_ReferenceValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, referenceValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "mapValue", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSMapValue), + .number = GCFSValue_FieldNumber_MapValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, mapValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "geoPointValue", + .dataTypeSpecific.className = GPBStringifySymbol(GTPLatLng), + .number = GCFSValue_FieldNumber_GeoPointValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, geoPointValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "arrayValue", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSArrayValue), + .number = GCFSValue_FieldNumber_ArrayValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, arrayValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "timestampValue", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSValue_FieldNumber_TimestampValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, timestampValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "nullValue", + .dataTypeSpecific.enumDescFunc = GPBNullValue_EnumDescriptor, + .number = GCFSValue_FieldNumber_NullValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, nullValue), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + { + .name = "stringValue", + .dataTypeSpecific.className = NULL, + .number = GCFSValue_FieldNumber_StringValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, stringValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "bytesValue", + .dataTypeSpecific.className = NULL, + .number = GCFSValue_FieldNumber_BytesValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSValue__storage_, bytesValue), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSValue class] + rootClass:[GCFSDocumentRoot class] + file:GCFSDocumentRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSValue__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "valueType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSValue_NullValue_RawValue(GCFSValue *message) { + GPBDescriptor *descriptor = [GCFSValue descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSValue_FieldNumber_NullValue]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSValue_NullValue_RawValue(GCFSValue *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSValue descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSValue_FieldNumber_NullValue]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +void GCFSValue_ClearValueTypeOneOfCase(GCFSValue *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSArrayValue + +@implementation GCFSArrayValue + +@dynamic valuesArray, valuesArray_Count; + +typedef struct GCFSArrayValue__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *valuesArray; +} GCFSArrayValue__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "valuesArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue), + .number = GCFSArrayValue_FieldNumber_ValuesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSArrayValue__storage_, valuesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSArrayValue class] + rootClass:[GCFSDocumentRoot class] + file:GCFSDocumentRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSArrayValue__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSMapValue + +@implementation GCFSMapValue + +@dynamic fields, fields_Count; + +typedef struct GCFSMapValue__storage_ { + uint32_t _has_storage_[1]; + NSMutableDictionary *fields; +} GCFSMapValue__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "fields", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue), + .number = GCFSMapValue_FieldNumber_Fields, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSMapValue__storage_, fields), + .flags = GPBFieldMapKeyString, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSMapValue class] + rootClass:[GCFSDocumentRoot class] + file:GCFSDocumentRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSMapValue__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h new file mode 100644 index 0000000..0acd8c0 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h @@ -0,0 +1,1342 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/firestore.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSDocument; +@class GCFSDocumentChange; +@class GCFSDocumentDelete; +@class GCFSDocumentMask; +@class GCFSDocumentRemove; +@class GCFSExistenceFilter; +@class GCFSPrecondition; +@class GCFSStructuredQuery; +@class GCFSTarget; +@class GCFSTargetChange; +@class GCFSTarget_DocumentsTarget; +@class GCFSTarget_QueryTarget; +@class GCFSTransactionOptions; +@class GCFSWrite; +@class GCFSWriteResult; +@class GPBTimestamp; +@class RPCStatus; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Enum GCFSTargetChange_TargetChangeType + +/** The type of change. */ +typedef GPB_ENUM(GCFSTargetChange_TargetChangeType) { + /** + * Value used if any message's field encounters a value that is not defined + * by this enum. The message will also have C functions to get/set the rawValue + * of the field. + **/ + GCFSTargetChange_TargetChangeType_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, + /** No change has occurred. Used only to send an updated `resume_token`. */ + GCFSTargetChange_TargetChangeType_NoChange = 0, + + /** The targets have been added. */ + GCFSTargetChange_TargetChangeType_Add = 1, + + /** The targets have been removed. */ + GCFSTargetChange_TargetChangeType_Remove = 2, + + /** + * The targets reflect all changes committed before the targets were added + * to the stream. + * + * This will be sent after or with a `read_time` that is greater than or + * equal to the time at which the targets were added. + * + * Listeners can wait for this change if read-after-write semantics + * are desired. + **/ + GCFSTargetChange_TargetChangeType_Current = 3, + + /** + * The targets have been reset, and a new initial state for the targets + * will be returned in subsequent changes. + * + * After the initial state is complete, `CURRENT` will be returned even + * if the target was previously indicated to be `CURRENT`. + **/ + GCFSTargetChange_TargetChangeType_Reset = 4, +}; + +GPBEnumDescriptor *GCFSTargetChange_TargetChangeType_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL GCFSTargetChange_TargetChangeType_IsValidValue(int32_t value); + +#pragma mark - GCFSFirestoreRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GCFSFirestoreRoot : GPBRootObject +@end + +#pragma mark - GCFSGetDocumentRequest + +typedef GPB_ENUM(GCFSGetDocumentRequest_FieldNumber) { + GCFSGetDocumentRequest_FieldNumber_Name = 1, + GCFSGetDocumentRequest_FieldNumber_Mask = 2, + GCFSGetDocumentRequest_FieldNumber_Transaction = 3, + GCFSGetDocumentRequest_FieldNumber_ReadTime = 5, +}; + +typedef GPB_ENUM(GCFSGetDocumentRequest_ConsistencySelector_OneOfCase) { + GCFSGetDocumentRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSGetDocumentRequest_ConsistencySelector_OneOfCase_Transaction = 3, + GCFSGetDocumentRequest_ConsistencySelector_OneOfCase_ReadTime = 5, +}; + +/** + * The request for [Firestore.GetDocument][google.firestore.v1beta1.Firestore.GetDocument]. + **/ +@interface GCFSGetDocumentRequest : GPBMessage + +/** + * The resource name of the Document to get. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *name; + +/** + * The fields to return. If not set, returns all fields. + * + * If the document has a field that is not present in this mask, that field + * will not be returned in the response. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask; +/** Test to see if @c mask has been set. */ +@property(nonatomic, readwrite) BOOL hasMask; + +/** + * The consistency mode for this transaction. + * If not set, defaults to strong consistency. + **/ +@property(nonatomic, readonly) GCFSGetDocumentRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase; + +/** Reads the document in a transaction. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +/** + * Reads the version of the document at the given time. + * This may not be older than 60 seconds. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; + +@end + +/** + * Clears whatever value was set for the oneof 'consistencySelector'. + **/ +void GCFSGetDocumentRequest_ClearConsistencySelectorOneOfCase(GCFSGetDocumentRequest *message); + +#pragma mark - GCFSListDocumentsRequest + +typedef GPB_ENUM(GCFSListDocumentsRequest_FieldNumber) { + GCFSListDocumentsRequest_FieldNumber_Parent = 1, + GCFSListDocumentsRequest_FieldNumber_CollectionId = 2, + GCFSListDocumentsRequest_FieldNumber_PageSize = 3, + GCFSListDocumentsRequest_FieldNumber_PageToken = 4, + GCFSListDocumentsRequest_FieldNumber_OrderBy = 6, + GCFSListDocumentsRequest_FieldNumber_Mask = 7, + GCFSListDocumentsRequest_FieldNumber_Transaction = 8, + GCFSListDocumentsRequest_FieldNumber_ReadTime = 10, + GCFSListDocumentsRequest_FieldNumber_ShowMissing = 12, +}; + +typedef GPB_ENUM(GCFSListDocumentsRequest_ConsistencySelector_OneOfCase) { + GCFSListDocumentsRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSListDocumentsRequest_ConsistencySelector_OneOfCase_Transaction = 8, + GCFSListDocumentsRequest_ConsistencySelector_OneOfCase_ReadTime = 10, +}; + +/** + * The request for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments]. + **/ +@interface GCFSListDocumentsRequest : GPBMessage + +/** + * The parent resource name. In the format: + * `projects/{project_id}/databases/{database_id}/documents` or + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + * For example: + * `projects/my-project/databases/my-database/documents` or + * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *parent; + +/** + * The collection ID, relative to `parent`, to list. For example: `chatrooms` + * or `messages`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *collectionId; + +/** The maximum number of documents to return. */ +@property(nonatomic, readwrite) int32_t pageSize; + +/** The `next_page_token` value returned from a previous List request, if any. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *pageToken; + +/** The order to sort results by. For example: `priority desc, name`. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *orderBy; + +/** + * The fields to return. If not set, returns all fields. + * + * If a document has a field that is not present in this mask, that field + * will not be returned in the response. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask; +/** Test to see if @c mask has been set. */ +@property(nonatomic, readwrite) BOOL hasMask; + +/** + * The consistency mode for this transaction. + * If not set, defaults to strong consistency. + **/ +@property(nonatomic, readonly) GCFSListDocumentsRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase; + +/** Reads documents in a transaction. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +/** + * Reads documents as they were at the given time. + * This may not be older than 60 seconds. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; + +/** + * If the list should show missing documents. A missing document is a + * document that does not exist but has sub-documents. These documents will + * be returned with a key but will not have fields, [Document.create_time][google.firestore.v1beta1.Document.create_time], + * or [Document.update_time][google.firestore.v1beta1.Document.update_time] set. + * + * Requests with `show_missing` may not specify `where` or + * `order_by`. + **/ +@property(nonatomic, readwrite) BOOL showMissing; + +@end + +/** + * Clears whatever value was set for the oneof 'consistencySelector'. + **/ +void GCFSListDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSListDocumentsRequest *message); + +#pragma mark - GCFSListDocumentsResponse + +typedef GPB_ENUM(GCFSListDocumentsResponse_FieldNumber) { + GCFSListDocumentsResponse_FieldNumber_DocumentsArray = 1, + GCFSListDocumentsResponse_FieldNumber_NextPageToken = 2, +}; + +/** + * The response for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments]. + **/ +@interface GCFSListDocumentsResponse : GPBMessage + +/** The Documents found. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *documentsArray; +/** The number of items in @c documentsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger documentsArray_Count; + +/** The next page token. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *nextPageToken; + +@end + +#pragma mark - GCFSCreateDocumentRequest + +typedef GPB_ENUM(GCFSCreateDocumentRequest_FieldNumber) { + GCFSCreateDocumentRequest_FieldNumber_Parent = 1, + GCFSCreateDocumentRequest_FieldNumber_CollectionId = 2, + GCFSCreateDocumentRequest_FieldNumber_DocumentId = 3, + GCFSCreateDocumentRequest_FieldNumber_Document = 4, + GCFSCreateDocumentRequest_FieldNumber_Mask = 5, +}; + +/** + * The request for [Firestore.CreateDocument][google.firestore.v1beta1.Firestore.CreateDocument]. + **/ +@interface GCFSCreateDocumentRequest : GPBMessage + +/** + * The parent resource. For example: + * `projects/{project_id}/databases/{database_id}/documents` or + * `projects/{project_id}/databases/{database_id}/documents/chatrooms/{chatroom_id}` + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *parent; + +/** The collection ID, relative to `parent`, to list. For example: `chatrooms`. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *collectionId; + +/** + * The client-assigned document ID to use for this document. + * + * Optional. If not specified, an ID will be assigned by the service. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *documentId; + +/** The document to create. `name` must not be set. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document; +/** Test to see if @c document has been set. */ +@property(nonatomic, readwrite) BOOL hasDocument; + +/** + * The fields to return. If not set, returns all fields. + * + * If the document has a field that is not present in this mask, that field + * will not be returned in the response. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask; +/** Test to see if @c mask has been set. */ +@property(nonatomic, readwrite) BOOL hasMask; + +@end + +#pragma mark - GCFSUpdateDocumentRequest + +typedef GPB_ENUM(GCFSUpdateDocumentRequest_FieldNumber) { + GCFSUpdateDocumentRequest_FieldNumber_Document = 1, + GCFSUpdateDocumentRequest_FieldNumber_UpdateMask = 2, + GCFSUpdateDocumentRequest_FieldNumber_Mask = 3, + GCFSUpdateDocumentRequest_FieldNumber_CurrentDocument = 4, +}; + +/** + * The request for [Firestore.UpdateDocument][google.firestore.v1beta1.Firestore.UpdateDocument]. + **/ +@interface GCFSUpdateDocumentRequest : GPBMessage + +/** + * The updated document. + * Creates the document if it does not already exist. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document; +/** Test to see if @c document has been set. */ +@property(nonatomic, readwrite) BOOL hasDocument; + +/** + * The fields to update. + * None of the field paths in the mask may contain a reserved name. + * + * If the document exists on the server and has fields not referenced in the + * mask, they are left unchanged. + * Fields referenced in the mask, but not present in the input document, are + * deleted from the document on the server. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *updateMask; +/** Test to see if @c updateMask has been set. */ +@property(nonatomic, readwrite) BOOL hasUpdateMask; + +/** + * The fields to return. If not set, returns all fields. + * + * If the document has a field that is not present in this mask, that field + * will not be returned in the response. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask; +/** Test to see if @c mask has been set. */ +@property(nonatomic, readwrite) BOOL hasMask; + +/** + * An optional precondition on the document. + * The request will fail if this is set and not met by the target document. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSPrecondition *currentDocument; +/** Test to see if @c currentDocument has been set. */ +@property(nonatomic, readwrite) BOOL hasCurrentDocument; + +@end + +#pragma mark - GCFSDeleteDocumentRequest + +typedef GPB_ENUM(GCFSDeleteDocumentRequest_FieldNumber) { + GCFSDeleteDocumentRequest_FieldNumber_Name = 1, + GCFSDeleteDocumentRequest_FieldNumber_CurrentDocument = 2, +}; + +/** + * The request for [Firestore.DeleteDocument][google.firestore.v1beta1.Firestore.DeleteDocument]. + **/ +@interface GCFSDeleteDocumentRequest : GPBMessage + +/** + * The resource name of the Document to delete. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *name; + +/** + * An optional precondition on the document. + * The request will fail if this is set and not met by the target document. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSPrecondition *currentDocument; +/** Test to see if @c currentDocument has been set. */ +@property(nonatomic, readwrite) BOOL hasCurrentDocument; + +@end + +#pragma mark - GCFSBatchGetDocumentsRequest + +typedef GPB_ENUM(GCFSBatchGetDocumentsRequest_FieldNumber) { + GCFSBatchGetDocumentsRequest_FieldNumber_Database = 1, + GCFSBatchGetDocumentsRequest_FieldNumber_DocumentsArray = 2, + GCFSBatchGetDocumentsRequest_FieldNumber_Mask = 3, + GCFSBatchGetDocumentsRequest_FieldNumber_Transaction = 4, + GCFSBatchGetDocumentsRequest_FieldNumber_NewTransaction = 5, + GCFSBatchGetDocumentsRequest_FieldNumber_ReadTime = 7, +}; + +typedef GPB_ENUM(GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase) { + GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_Transaction = 4, + GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_NewTransaction = 5, + GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase_ReadTime = 7, +}; + +/** + * The request for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments]. + **/ +@interface GCFSBatchGetDocumentsRequest : GPBMessage + +/** + * The database name. In the format: + * `projects/{project_id}/databases/{database_id}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *database; + +/** + * The names of the documents to retrieve. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + * The request will fail if any of the document is not a child resource of the + * given `database`. Duplicate names will be elided. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *documentsArray; +/** The number of items in @c documentsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger documentsArray_Count; + +/** + * The fields to return. If not set, returns all fields. + * + * If a document has a field that is not present in this mask, that field will + * not be returned in the response. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *mask; +/** Test to see if @c mask has been set. */ +@property(nonatomic, readwrite) BOOL hasMask; + +/** + * The consistency mode for this transaction. + * If not set, defaults to strong consistency. + **/ +@property(nonatomic, readonly) GCFSBatchGetDocumentsRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase; + +/** Reads documents in a transaction. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +/** + * Starts a new transaction and reads the documents. + * Defaults to a read-only transaction. + * The new transaction ID will be returned as the first response in the + * stream. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions *newTransaction NS_RETURNS_NOT_RETAINED; + +/** + * Reads documents as they were at the given time. + * This may not be older than 60 seconds. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; + +@end + +/** + * Clears whatever value was set for the oneof 'consistencySelector'. + **/ +void GCFSBatchGetDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSBatchGetDocumentsRequest *message); + +#pragma mark - GCFSBatchGetDocumentsResponse + +typedef GPB_ENUM(GCFSBatchGetDocumentsResponse_FieldNumber) { + GCFSBatchGetDocumentsResponse_FieldNumber_Found = 1, + GCFSBatchGetDocumentsResponse_FieldNumber_Missing = 2, + GCFSBatchGetDocumentsResponse_FieldNumber_Transaction = 3, + GCFSBatchGetDocumentsResponse_FieldNumber_ReadTime = 4, +}; + +typedef GPB_ENUM(GCFSBatchGetDocumentsResponse_Result_OneOfCase) { + GCFSBatchGetDocumentsResponse_Result_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found = 1, + GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing = 2, +}; + +/** + * The streamed response for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments]. + **/ +@interface GCFSBatchGetDocumentsResponse : GPBMessage + +/** + * A single result. + * This can be empty if the server is just returning a transaction. + **/ +@property(nonatomic, readonly) GCFSBatchGetDocumentsResponse_Result_OneOfCase resultOneOfCase; + +/** A document that was requested. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *found; + +/** + * A document name that was requested but does not exist. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *missing; + +/** + * The transaction that was started as part of this request. + * Will only be set in the first response, and only if + * [BatchGetDocumentsRequest.new_transaction][google.firestore.v1beta1.BatchGetDocumentsRequest.new_transaction] was set in the request. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +/** + * The time at which the document was read. + * This may be monotically increasing, in this case the previous documents in + * the result stream are guaranteed not to have changed between their + * read_time and this one. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; +/** Test to see if @c readTime has been set. */ +@property(nonatomic, readwrite) BOOL hasReadTime; + +@end + +/** + * Clears whatever value was set for the oneof 'result'. + **/ +void GCFSBatchGetDocumentsResponse_ClearResultOneOfCase(GCFSBatchGetDocumentsResponse *message); + +#pragma mark - GCFSBeginTransactionRequest + +typedef GPB_ENUM(GCFSBeginTransactionRequest_FieldNumber) { + GCFSBeginTransactionRequest_FieldNumber_Database = 1, + GCFSBeginTransactionRequest_FieldNumber_Options = 2, +}; + +/** + * The request for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction]. + **/ +@interface GCFSBeginTransactionRequest : GPBMessage + +/** + * The database name. In the format: + * `projects/{project_id}/databases/{database_id}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *database; + +/** + * The options for the transaction. + * Defaults to a read-write transaction. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions *options; +/** Test to see if @c options has been set. */ +@property(nonatomic, readwrite) BOOL hasOptions; + +@end + +#pragma mark - GCFSBeginTransactionResponse + +typedef GPB_ENUM(GCFSBeginTransactionResponse_FieldNumber) { + GCFSBeginTransactionResponse_FieldNumber_Transaction = 1, +}; + +/** + * The response for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction]. + **/ +@interface GCFSBeginTransactionResponse : GPBMessage + +/** The transaction that was started. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +@end + +#pragma mark - GCFSCommitRequest + +typedef GPB_ENUM(GCFSCommitRequest_FieldNumber) { + GCFSCommitRequest_FieldNumber_Database = 1, + GCFSCommitRequest_FieldNumber_WritesArray = 2, + GCFSCommitRequest_FieldNumber_Transaction = 3, +}; + +/** + * The request for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit]. + **/ +@interface GCFSCommitRequest : GPBMessage + +/** + * The database name. In the format: + * `projects/{project_id}/databases/{database_id}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *database; + +/** + * The writes to apply. + * + * Always executed atomically and in order. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *writesArray; +/** The number of items in @c writesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger writesArray_Count; + +/** + * If non-empty, applies all writes in this transaction, and commits it. + * Otherwise, applies the writes as if they were in their own transaction. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +@end + +#pragma mark - GCFSCommitResponse + +typedef GPB_ENUM(GCFSCommitResponse_FieldNumber) { + GCFSCommitResponse_FieldNumber_WriteResultsArray = 1, + GCFSCommitResponse_FieldNumber_CommitTime = 2, +}; + +/** + * The response for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit]. + **/ +@interface GCFSCommitResponse : GPBMessage + +/** + * The result of applying the writes. + * + * This i-th write result corresponds to the i-th write in the + * request. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *writeResultsArray; +/** The number of items in @c writeResultsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger writeResultsArray_Count; + +/** The time at which the commit occurred. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *commitTime; +/** Test to see if @c commitTime has been set. */ +@property(nonatomic, readwrite) BOOL hasCommitTime; + +@end + +#pragma mark - GCFSRollbackRequest + +typedef GPB_ENUM(GCFSRollbackRequest_FieldNumber) { + GCFSRollbackRequest_FieldNumber_Database = 1, + GCFSRollbackRequest_FieldNumber_Transaction = 2, +}; + +/** + * The request for [Firestore.Rollback][google.firestore.v1beta1.Firestore.Rollback]. + **/ +@interface GCFSRollbackRequest : GPBMessage + +/** + * The database name. In the format: + * `projects/{project_id}/databases/{database_id}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *database; + +/** The transaction to roll back. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +@end + +#pragma mark - GCFSRunQueryRequest + +typedef GPB_ENUM(GCFSRunQueryRequest_FieldNumber) { + GCFSRunQueryRequest_FieldNumber_Parent = 1, + GCFSRunQueryRequest_FieldNumber_StructuredQuery = 2, + GCFSRunQueryRequest_FieldNumber_Transaction = 5, + GCFSRunQueryRequest_FieldNumber_NewTransaction = 6, + GCFSRunQueryRequest_FieldNumber_ReadTime = 7, +}; + +typedef GPB_ENUM(GCFSRunQueryRequest_QueryType_OneOfCase) { + GCFSRunQueryRequest_QueryType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSRunQueryRequest_QueryType_OneOfCase_StructuredQuery = 2, +}; + +typedef GPB_ENUM(GCFSRunQueryRequest_ConsistencySelector_OneOfCase) { + GCFSRunQueryRequest_ConsistencySelector_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSRunQueryRequest_ConsistencySelector_OneOfCase_Transaction = 5, + GCFSRunQueryRequest_ConsistencySelector_OneOfCase_NewTransaction = 6, + GCFSRunQueryRequest_ConsistencySelector_OneOfCase_ReadTime = 7, +}; + +/** + * The request for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery]. + **/ +@interface GCFSRunQueryRequest : GPBMessage + +/** + * The parent resource name. In the format: + * `projects/{project_id}/databases/{database_id}/documents` or + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + * For example: + * `projects/my-project/databases/my-database/documents` or + * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *parent; + +/** The query to run. */ +@property(nonatomic, readonly) GCFSRunQueryRequest_QueryType_OneOfCase queryTypeOneOfCase; + +/** A structured query. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery *structuredQuery; + +/** + * The consistency mode for this transaction. + * If not set, defaults to strong consistency. + **/ +@property(nonatomic, readonly) GCFSRunQueryRequest_ConsistencySelector_OneOfCase consistencySelectorOneOfCase; + +/** Reads documents in a transaction. */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +/** + * Starts a new transaction and reads the documents. + * Defaults to a read-only transaction. + * The new transaction ID will be returned as the first response in the + * stream. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTransactionOptions *newTransaction NS_RETURNS_NOT_RETAINED; + +/** + * Reads documents as they were at the given time. + * This may not be older than 60 seconds. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; + +@end + +/** + * Clears whatever value was set for the oneof 'queryType'. + **/ +void GCFSRunQueryRequest_ClearQueryTypeOneOfCase(GCFSRunQueryRequest *message); +/** + * Clears whatever value was set for the oneof 'consistencySelector'. + **/ +void GCFSRunQueryRequest_ClearConsistencySelectorOneOfCase(GCFSRunQueryRequest *message); + +#pragma mark - GCFSRunQueryResponse + +typedef GPB_ENUM(GCFSRunQueryResponse_FieldNumber) { + GCFSRunQueryResponse_FieldNumber_Document = 1, + GCFSRunQueryResponse_FieldNumber_Transaction = 2, + GCFSRunQueryResponse_FieldNumber_ReadTime = 3, + GCFSRunQueryResponse_FieldNumber_SkippedResults = 4, +}; + +/** + * The response for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery]. + **/ +@interface GCFSRunQueryResponse : GPBMessage + +/** + * The transaction that was started as part of this request. + * Can only be set in the first response, and only if + * [RunQueryRequest.new_transaction][google.firestore.v1beta1.RunQueryRequest.new_transaction] was set in the request. + * If set, no other fields will be set in this response. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *transaction; + +/** + * A query result. + * Not set when reporting partial progress. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document; +/** Test to see if @c document has been set. */ +@property(nonatomic, readwrite) BOOL hasDocument; + +/** + * The time at which the document was read. This may be monotonically + * increasing; in this case, the previous documents in the result stream are + * guaranteed not to have changed between their `read_time` and this one. + * + * If the query returns no results, a response with `read_time` and no + * `document` will be sent, and this represents the time at which the query + * was run. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; +/** Test to see if @c readTime has been set. */ +@property(nonatomic, readwrite) BOOL hasReadTime; + +/** + * The number of results that have been skipped due to an offset between + * the last response and the current response. + **/ +@property(nonatomic, readwrite) int32_t skippedResults; + +@end + +#pragma mark - GCFSWriteRequest + +typedef GPB_ENUM(GCFSWriteRequest_FieldNumber) { + GCFSWriteRequest_FieldNumber_Database = 1, + GCFSWriteRequest_FieldNumber_StreamId = 2, + GCFSWriteRequest_FieldNumber_WritesArray = 3, + GCFSWriteRequest_FieldNumber_StreamToken = 4, + GCFSWriteRequest_FieldNumber_Labels = 5, +}; + +/** + * The request for [Firestore.Write][google.firestore.v1beta1.Firestore.Write]. + * + * The first request creates a stream, or resumes an existing one from a token. + * + * When creating a new stream, the server replies with a response containing + * only an ID and a token, to use in the next request. + * + * When resuming a stream, the server first streams any responses later than the + * given token, then a response containing only an up-to-date token, to use in + * the next request. + **/ +@interface GCFSWriteRequest : GPBMessage + +/** + * The database name. In the format: + * `projects/{project_id}/databases/{database_id}`. + * This is only required in the first message. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *database; + +/** + * The ID of the write stream to resume. + * This may only be set in the first message. When left empty, a new write + * stream will be created. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *streamId; + +/** + * The writes to apply. + * + * Always executed atomically and in order. + * This must be empty on the first request. + * This may be empty on the last request. + * This must not be empty on all other requests. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *writesArray; +/** The number of items in @c writesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger writesArray_Count; + +/** + * A stream token that was previously sent by the server. + * + * The client should set this field to the token from the most recent + * [WriteResponse][google.firestore.v1beta1.WriteResponse] it has received. This acknowledges that the client has + * received responses up to this token. After sending this token, earlier + * tokens may not be used anymore. + * + * The server may close the stream if there are too many unacknowledged + * responses. + * + * Leave this field unset when creating a new stream. To resume a stream at + * a specific point, set this field and the `stream_id` field. + * + * Leave this field unset when creating a new stream. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *streamToken; + +/** Labels associated with this write request. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary *labels; +/** The number of items in @c labels without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger labels_Count; + +@end + +#pragma mark - GCFSWriteResponse + +typedef GPB_ENUM(GCFSWriteResponse_FieldNumber) { + GCFSWriteResponse_FieldNumber_StreamId = 1, + GCFSWriteResponse_FieldNumber_StreamToken = 2, + GCFSWriteResponse_FieldNumber_WriteResultsArray = 3, + GCFSWriteResponse_FieldNumber_CommitTime = 4, +}; + +/** + * The response for [Firestore.Write][google.firestore.v1beta1.Firestore.Write]. + **/ +@interface GCFSWriteResponse : GPBMessage + +/** + * The ID of the stream. + * Only set on the first message, when a new stream was created. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *streamId; + +/** + * A token that represents the position of this response in the stream. + * This can be used by a client to resume the stream at this point. + * + * This field is always set. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *streamToken; + +/** + * The result of applying the writes. + * + * This i-th write result corresponds to the i-th write in the + * request. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *writeResultsArray; +/** The number of items in @c writeResultsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger writeResultsArray_Count; + +/** The time at which the commit occurred. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *commitTime; +/** Test to see if @c commitTime has been set. */ +@property(nonatomic, readwrite) BOOL hasCommitTime; + +@end + +#pragma mark - GCFSListenRequest + +typedef GPB_ENUM(GCFSListenRequest_FieldNumber) { + GCFSListenRequest_FieldNumber_Database = 1, + GCFSListenRequest_FieldNumber_AddTarget = 2, + GCFSListenRequest_FieldNumber_RemoveTarget = 3, + GCFSListenRequest_FieldNumber_Labels = 4, +}; + +typedef GPB_ENUM(GCFSListenRequest_TargetChange_OneOfCase) { + GCFSListenRequest_TargetChange_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSListenRequest_TargetChange_OneOfCase_AddTarget = 2, + GCFSListenRequest_TargetChange_OneOfCase_RemoveTarget = 3, +}; + +/** + * A request for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen] + **/ +@interface GCFSListenRequest : GPBMessage + +/** + * The database name. In the format: + * `projects/{project_id}/databases/{database_id}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *database; + +/** The supported target changes. */ +@property(nonatomic, readonly) GCFSListenRequest_TargetChange_OneOfCase targetChangeOneOfCase; + +/** A target to add to this stream. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget *addTarget; + +/** The ID of a target to remove from this stream. */ +@property(nonatomic, readwrite) int32_t removeTarget; + +/** Labels associated with this target change. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableDictionary *labels; +/** The number of items in @c labels without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger labels_Count; + +@end + +/** + * Clears whatever value was set for the oneof 'targetChange'. + **/ +void GCFSListenRequest_ClearTargetChangeOneOfCase(GCFSListenRequest *message); + +#pragma mark - GCFSListenResponse + +typedef GPB_ENUM(GCFSListenResponse_FieldNumber) { + GCFSListenResponse_FieldNumber_TargetChange = 2, + GCFSListenResponse_FieldNumber_DocumentChange = 3, + GCFSListenResponse_FieldNumber_DocumentDelete = 4, + GCFSListenResponse_FieldNumber_Filter = 5, + GCFSListenResponse_FieldNumber_DocumentRemove = 6, +}; + +typedef GPB_ENUM(GCFSListenResponse_ResponseType_OneOfCase) { + GCFSListenResponse_ResponseType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSListenResponse_ResponseType_OneOfCase_TargetChange = 2, + GCFSListenResponse_ResponseType_OneOfCase_DocumentChange = 3, + GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete = 4, + GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove = 6, + GCFSListenResponse_ResponseType_OneOfCase_Filter = 5, +}; + +/** + * The response for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen]. + **/ +@interface GCFSListenResponse : GPBMessage + +/** The supported responses. */ +@property(nonatomic, readonly) GCFSListenResponse_ResponseType_OneOfCase responseTypeOneOfCase; + +/** Targets have changed. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTargetChange *targetChange; + +/** A [Document][google.firestore.v1beta1.Document] has changed. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentChange *documentChange; + +/** A [Document][google.firestore.v1beta1.Document] has been deleted. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentDelete *documentDelete; + +/** + * A [Document][google.firestore.v1beta1.Document] has been removed from a target (because it is no longer + * relevant to that target). + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentRemove *documentRemove; + +/** + * A filter to apply to the set of documents previously returned for the + * given target. + * + * Returned when documents may have been removed from the given target, but + * the exact documents are unknown. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSExistenceFilter *filter; + +@end + +/** + * Clears whatever value was set for the oneof 'responseType'. + **/ +void GCFSListenResponse_ClearResponseTypeOneOfCase(GCFSListenResponse *message); + +#pragma mark - GCFSTarget + +typedef GPB_ENUM(GCFSTarget_FieldNumber) { + GCFSTarget_FieldNumber_Query = 2, + GCFSTarget_FieldNumber_Documents = 3, + GCFSTarget_FieldNumber_ResumeToken = 4, + GCFSTarget_FieldNumber_TargetId = 5, + GCFSTarget_FieldNumber_Once = 6, + GCFSTarget_FieldNumber_ReadTime = 11, +}; + +typedef GPB_ENUM(GCFSTarget_TargetType_OneOfCase) { + GCFSTarget_TargetType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSTarget_TargetType_OneOfCase_Query = 2, + GCFSTarget_TargetType_OneOfCase_Documents = 3, +}; + +typedef GPB_ENUM(GCFSTarget_ResumeType_OneOfCase) { + GCFSTarget_ResumeType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSTarget_ResumeType_OneOfCase_ResumeToken = 4, + GCFSTarget_ResumeType_OneOfCase_ReadTime = 11, +}; + +/** + * A specification of a set of documents to listen to. + **/ +@interface GCFSTarget : GPBMessage + +/** The type of target to listen to. */ +@property(nonatomic, readonly) GCFSTarget_TargetType_OneOfCase targetTypeOneOfCase; + +/** A target specified by a query. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_QueryTarget *query; + +/** A target specified by a set of document names. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSTarget_DocumentsTarget *documents; + +/** + * When to start listening. + * + * If not specified, all matching Documents are returned before any + * subsequent changes. + **/ +@property(nonatomic, readonly) GCFSTarget_ResumeType_OneOfCase resumeTypeOneOfCase; + +/** + * A resume token from a prior [TargetChange][google.firestore.v1beta1.TargetChange] for an identical target. + * + * Using a resume token with a different target is unsupported and may fail. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *resumeToken; + +/** + * Start listening after a specific `read_time`. + * + * The client must know the state of matching documents at this time. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; + +/** + * A client provided target ID. + * + * If not set, the server will assign an ID for the target. + * + * Used for resuming a target without changing IDs. The IDs can either be + * client-assigned or be server-assigned in a previous stream. All targets + * with client provided IDs must be added before adding a target that needs + * a server-assigned id. + **/ +@property(nonatomic, readwrite) int32_t targetId; + +/** If the target should be removed once it is current and consistent. */ +@property(nonatomic, readwrite) BOOL once; + +@end + +/** + * Clears whatever value was set for the oneof 'targetType'. + **/ +void GCFSTarget_ClearTargetTypeOneOfCase(GCFSTarget *message); +/** + * Clears whatever value was set for the oneof 'resumeType'. + **/ +void GCFSTarget_ClearResumeTypeOneOfCase(GCFSTarget *message); + +#pragma mark - GCFSTarget_DocumentsTarget + +typedef GPB_ENUM(GCFSTarget_DocumentsTarget_FieldNumber) { + GCFSTarget_DocumentsTarget_FieldNumber_DocumentsArray = 2, +}; + +/** + * A target specified by a set of documents names. + **/ +@interface GCFSTarget_DocumentsTarget : GPBMessage + +/** + * The names of the documents to retrieve. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + * The request will fail if any of the document is not a child resource of + * the given `database`. Duplicate names will be elided. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *documentsArray; +/** The number of items in @c documentsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger documentsArray_Count; + +@end + +#pragma mark - GCFSTarget_QueryTarget + +typedef GPB_ENUM(GCFSTarget_QueryTarget_FieldNumber) { + GCFSTarget_QueryTarget_FieldNumber_Parent = 1, + GCFSTarget_QueryTarget_FieldNumber_StructuredQuery = 2, +}; + +typedef GPB_ENUM(GCFSTarget_QueryTarget_QueryType_OneOfCase) { + GCFSTarget_QueryTarget_QueryType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSTarget_QueryTarget_QueryType_OneOfCase_StructuredQuery = 2, +}; + +/** + * A target specified by a query. + **/ +@interface GCFSTarget_QueryTarget : GPBMessage + +/** + * The parent resource name. In the format: + * `projects/{project_id}/databases/{database_id}/documents` or + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + * For example: + * `projects/my-project/databases/my-database/documents` or + * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *parent; + +/** The query to run. */ +@property(nonatomic, readonly) GCFSTarget_QueryTarget_QueryType_OneOfCase queryTypeOneOfCase; + +/** A structured query. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery *structuredQuery; + +@end + +/** + * Clears whatever value was set for the oneof 'queryType'. + **/ +void GCFSTarget_QueryTarget_ClearQueryTypeOneOfCase(GCFSTarget_QueryTarget *message); + +#pragma mark - GCFSTargetChange + +typedef GPB_ENUM(GCFSTargetChange_FieldNumber) { + GCFSTargetChange_FieldNumber_TargetChangeType = 1, + GCFSTargetChange_FieldNumber_TargetIdsArray = 2, + GCFSTargetChange_FieldNumber_Cause = 3, + GCFSTargetChange_FieldNumber_ResumeToken = 4, + GCFSTargetChange_FieldNumber_ReadTime = 6, +}; + +/** + * Targets being watched have changed. + **/ +@interface GCFSTargetChange : GPBMessage + +/** The type of change that occurred. */ +@property(nonatomic, readwrite) GCFSTargetChange_TargetChangeType targetChangeType; + +/** + * The target IDs of targets that have changed. + * + * If empty, the change applies to all targets. + * + * For `target_change_type=ADD`, the order of the target IDs matches the order + * of the requests to add the targets. This allows clients to unambiguously + * associate server-assigned target IDs with added targets. + * + * For other states, the order of the target IDs is not defined. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *targetIdsArray; +/** The number of items in @c targetIdsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger targetIdsArray_Count; + +/** The error that resulted in this change, if applicable. */ +@property(nonatomic, readwrite, strong, null_resettable) RPCStatus *cause; +/** Test to see if @c cause has been set. */ +@property(nonatomic, readwrite) BOOL hasCause; + +/** + * A token that can be used to resume the stream for the given `target_ids`, + * or all targets if `target_ids` is empty. + * + * Not set on every target change. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSData *resumeToken; + +/** + * The consistent `read_time` for the given `target_ids` (omitted when the + * target_ids are not at a consistent snapshot). + * + * The stream is guaranteed to send a `read_time` with `target_ids` empty + * whenever the entire stream reaches a new consistent snapshot. ADD, + * CURRENT, and RESET messages are guaranteed to (eventually) result in a + * new consistent snapshot (while NO_CHANGE and REMOVE messages are not). + * + * For a given stream, `read_time` is guaranteed to be monotonically + * increasing. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; +/** Test to see if @c readTime has been set. */ +@property(nonatomic, readwrite) BOOL hasReadTime; + +@end + +/** + * Fetches the raw value of a @c GCFSTargetChange's @c targetChangeType property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message); +/** + * Sets the raw value of an @c GCFSTargetChange's @c targetChangeType property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message, int32_t value); + +#pragma mark - GCFSListCollectionIdsRequest + +typedef GPB_ENUM(GCFSListCollectionIdsRequest_FieldNumber) { + GCFSListCollectionIdsRequest_FieldNumber_Parent = 1, + GCFSListCollectionIdsRequest_FieldNumber_PageSize = 2, + GCFSListCollectionIdsRequest_FieldNumber_PageToken = 3, +}; + +/** + * The request for [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds]. + **/ +@interface GCFSListCollectionIdsRequest : GPBMessage + +/** + * The parent document. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + * For example: + * `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *parent; + +/** The maximum number of results to return. */ +@property(nonatomic, readwrite) int32_t pageSize; + +/** + * A page token. Must be a value from + * [ListCollectionIdsResponse][google.firestore.v1beta1.ListCollectionIdsResponse]. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *pageToken; + +@end + +#pragma mark - GCFSListCollectionIdsResponse + +typedef GPB_ENUM(GCFSListCollectionIdsResponse_FieldNumber) { + GCFSListCollectionIdsResponse_FieldNumber_CollectionIdsArray = 1, + GCFSListCollectionIdsResponse_FieldNumber_NextPageToken = 2, +}; + +/** + * The response from [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds]. + **/ +@interface GCFSListCollectionIdsResponse : GPBMessage + +/** The collection ids. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *collectionIdsArray; +/** The number of items in @c collectionIdsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger collectionIdsArray_Count; + +/** A page token that may be used to continue the list. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *nextPageToken; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m new file mode 100644 index 0000000..4bdee01 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.m @@ -0,0 +1,2064 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/firestore.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import + #import +#else + #import "Empty.pbobjc.h" + #import "Timestamp.pbobjc.h" +#endif + + #import "Firestore.pbobjc.h" + #import "Annotations.pbobjc.h" + #import "Common.pbobjc.h" + #import "Document.pbobjc.h" + #import "Query.pbobjc.h" + #import "Write.pbobjc.h" + #import "Status.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - GCFSFirestoreRoot + +@implementation GCFSFirestoreRoot + + +@end + +#pragma mark - GCFSFirestoreRoot_FileDescriptor + +static GPBFileDescriptor *GCFSFirestoreRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1" + objcPrefix:@"GCFS" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GCFSGetDocumentRequest + +@implementation GCFSGetDocumentRequest + +@dynamic consistencySelectorOneOfCase; +@dynamic name; +@dynamic hasMask, mask; +@dynamic transaction; +@dynamic readTime; + +typedef struct GCFSGetDocumentRequest__storage_ { + uint32_t _has_storage_[2]; + NSString *name; + GCFSDocumentMask *mask; + NSData *transaction; + GPBTimestamp *readTime; +} GCFSGetDocumentRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "name", + .dataTypeSpecific.className = NULL, + .number = GCFSGetDocumentRequest_FieldNumber_Name, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, name), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "mask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSGetDocumentRequest_FieldNumber_Mask, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, mask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSGetDocumentRequest_FieldNumber_Transaction, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSGetDocumentRequest_FieldNumber_ReadTime, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSGetDocumentRequest__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSGetDocumentRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSGetDocumentRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "consistencySelector", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSGetDocumentRequest_ClearConsistencySelectorOneOfCase(GCFSGetDocumentRequest *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSListDocumentsRequest + +@implementation GCFSListDocumentsRequest + +@dynamic consistencySelectorOneOfCase; +@dynamic parent; +@dynamic collectionId; +@dynamic pageSize; +@dynamic pageToken; +@dynamic orderBy; +@dynamic hasMask, mask; +@dynamic transaction; +@dynamic readTime; +@dynamic showMissing; + +typedef struct GCFSListDocumentsRequest__storage_ { + uint32_t _has_storage_[2]; + int32_t pageSize; + NSString *parent; + NSString *collectionId; + NSString *pageToken; + NSString *orderBy; + GCFSDocumentMask *mask; + NSData *transaction; + GPBTimestamp *readTime; +} GCFSListDocumentsRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "parent", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_Parent, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, parent), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "collectionId", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_CollectionId, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, collectionId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "pageSize", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_PageSize, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, pageSize), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "pageToken", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_PageToken, + .hasIndex = 3, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, pageToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "orderBy", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_OrderBy, + .hasIndex = 4, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, orderBy), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "mask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSListDocumentsRequest_FieldNumber_Mask, + .hasIndex = 5, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, mask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_Transaction, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSListDocumentsRequest_FieldNumber_ReadTime, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListDocumentsRequest__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "showMissing", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsRequest_FieldNumber_ShowMissing, + .hasIndex = 6, + .offset = 7, // Stored in _has_storage_ to save space. + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBool, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSListDocumentsRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSListDocumentsRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "consistencySelector", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSListDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSListDocumentsRequest *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSListDocumentsResponse + +@implementation GCFSListDocumentsResponse + +@dynamic documentsArray, documentsArray_Count; +@dynamic nextPageToken; + +typedef struct GCFSListDocumentsResponse__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *documentsArray; + NSString *nextPageToken; +} GCFSListDocumentsResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "documentsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSListDocumentsResponse_FieldNumber_DocumentsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSListDocumentsResponse__storage_, documentsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "nextPageToken", + .dataTypeSpecific.className = NULL, + .number = GCFSListDocumentsResponse_FieldNumber_NextPageToken, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSListDocumentsResponse__storage_, nextPageToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSListDocumentsResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSListDocumentsResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSCreateDocumentRequest + +@implementation GCFSCreateDocumentRequest + +@dynamic parent; +@dynamic collectionId; +@dynamic documentId; +@dynamic hasDocument, document; +@dynamic hasMask, mask; + +typedef struct GCFSCreateDocumentRequest__storage_ { + uint32_t _has_storage_[1]; + NSString *parent; + NSString *collectionId; + NSString *documentId; + GCFSDocument *document; + GCFSDocumentMask *mask; +} GCFSCreateDocumentRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "parent", + .dataTypeSpecific.className = NULL, + .number = GCFSCreateDocumentRequest_FieldNumber_Parent, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, parent), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "collectionId", + .dataTypeSpecific.className = NULL, + .number = GCFSCreateDocumentRequest_FieldNumber_CollectionId, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, collectionId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "documentId", + .dataTypeSpecific.className = NULL, + .number = GCFSCreateDocumentRequest_FieldNumber_DocumentId, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, documentId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "document", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSCreateDocumentRequest_FieldNumber_Document, + .hasIndex = 3, + .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "mask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSCreateDocumentRequest_FieldNumber_Mask, + .hasIndex = 4, + .offset = (uint32_t)offsetof(GCFSCreateDocumentRequest__storage_, mask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSCreateDocumentRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSCreateDocumentRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSUpdateDocumentRequest + +@implementation GCFSUpdateDocumentRequest + +@dynamic hasDocument, document; +@dynamic hasUpdateMask, updateMask; +@dynamic hasMask, mask; +@dynamic hasCurrentDocument, currentDocument; + +typedef struct GCFSUpdateDocumentRequest__storage_ { + uint32_t _has_storage_[1]; + GCFSDocument *document; + GCFSDocumentMask *updateMask; + GCFSDocumentMask *mask; + GCFSPrecondition *currentDocument; +} GCFSUpdateDocumentRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "document", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSUpdateDocumentRequest_FieldNumber_Document, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "updateMask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSUpdateDocumentRequest_FieldNumber_UpdateMask, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, updateMask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "mask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSUpdateDocumentRequest_FieldNumber_Mask, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, mask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "currentDocument", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSPrecondition), + .number = GCFSUpdateDocumentRequest_FieldNumber_CurrentDocument, + .hasIndex = 3, + .offset = (uint32_t)offsetof(GCFSUpdateDocumentRequest__storage_, currentDocument), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSUpdateDocumentRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSUpdateDocumentRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSDeleteDocumentRequest + +@implementation GCFSDeleteDocumentRequest + +@dynamic name; +@dynamic hasCurrentDocument, currentDocument; + +typedef struct GCFSDeleteDocumentRequest__storage_ { + uint32_t _has_storage_[1]; + NSString *name; + GCFSPrecondition *currentDocument; +} GCFSDeleteDocumentRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "name", + .dataTypeSpecific.className = NULL, + .number = GCFSDeleteDocumentRequest_FieldNumber_Name, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDeleteDocumentRequest__storage_, name), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "currentDocument", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSPrecondition), + .number = GCFSDeleteDocumentRequest_FieldNumber_CurrentDocument, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSDeleteDocumentRequest__storage_, currentDocument), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDeleteDocumentRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDeleteDocumentRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSBatchGetDocumentsRequest + +@implementation GCFSBatchGetDocumentsRequest + +@dynamic consistencySelectorOneOfCase; +@dynamic database; +@dynamic documentsArray, documentsArray_Count; +@dynamic hasMask, mask; +@dynamic transaction; +@dynamic newTransaction; +@dynamic readTime; + +typedef struct GCFSBatchGetDocumentsRequest__storage_ { + uint32_t _has_storage_[2]; + NSString *database; + NSMutableArray *documentsArray; + GCFSDocumentMask *mask; + NSData *transaction; + GCFSTransactionOptions *newTransaction; + GPBTimestamp *readTime; +} GCFSBatchGetDocumentsRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "database", + .dataTypeSpecific.className = NULL, + .number = GCFSBatchGetDocumentsRequest_FieldNumber_Database, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, database), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "documentsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSBatchGetDocumentsRequest_FieldNumber_DocumentsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, documentsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeString, + }, + { + .name = "mask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSBatchGetDocumentsRequest_FieldNumber_Mask, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, mask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSBatchGetDocumentsRequest_FieldNumber_Transaction, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "newTransaction", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions), + .number = GCFSBatchGetDocumentsRequest_FieldNumber_NewTransaction, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, newTransaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSBatchGetDocumentsRequest_FieldNumber_ReadTime, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsRequest__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSBatchGetDocumentsRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSBatchGetDocumentsRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "consistencySelector", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSBatchGetDocumentsRequest_ClearConsistencySelectorOneOfCase(GCFSBatchGetDocumentsRequest *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSBatchGetDocumentsResponse + +@implementation GCFSBatchGetDocumentsResponse + +@dynamic resultOneOfCase; +@dynamic found; +@dynamic missing; +@dynamic transaction; +@dynamic hasReadTime, readTime; + +typedef struct GCFSBatchGetDocumentsResponse__storage_ { + uint32_t _has_storage_[2]; + GCFSDocument *found; + NSString *missing; + NSData *transaction; + GPBTimestamp *readTime; +} GCFSBatchGetDocumentsResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "found", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSBatchGetDocumentsResponse_FieldNumber_Found, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, found), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "missing", + .dataTypeSpecific.className = NULL, + .number = GCFSBatchGetDocumentsResponse_FieldNumber_Missing, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, missing), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSBatchGetDocumentsResponse_FieldNumber_Transaction, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSBatchGetDocumentsResponse_FieldNumber_ReadTime, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSBatchGetDocumentsResponse__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSBatchGetDocumentsResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSBatchGetDocumentsResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "result", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSBatchGetDocumentsResponse_ClearResultOneOfCase(GCFSBatchGetDocumentsResponse *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSBeginTransactionRequest + +@implementation GCFSBeginTransactionRequest + +@dynamic database; +@dynamic hasOptions, options; + +typedef struct GCFSBeginTransactionRequest__storage_ { + uint32_t _has_storage_[1]; + NSString *database; + GCFSTransactionOptions *options; +} GCFSBeginTransactionRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "database", + .dataTypeSpecific.className = NULL, + .number = GCFSBeginTransactionRequest_FieldNumber_Database, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSBeginTransactionRequest__storage_, database), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "options", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions), + .number = GCFSBeginTransactionRequest_FieldNumber_Options, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSBeginTransactionRequest__storage_, options), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSBeginTransactionRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSBeginTransactionRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSBeginTransactionResponse + +@implementation GCFSBeginTransactionResponse + +@dynamic transaction; + +typedef struct GCFSBeginTransactionResponse__storage_ { + uint32_t _has_storage_[1]; + NSData *transaction; +} GCFSBeginTransactionResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSBeginTransactionResponse_FieldNumber_Transaction, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSBeginTransactionResponse__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSBeginTransactionResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSBeginTransactionResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSCommitRequest + +@implementation GCFSCommitRequest + +@dynamic database; +@dynamic writesArray, writesArray_Count; +@dynamic transaction; + +typedef struct GCFSCommitRequest__storage_ { + uint32_t _has_storage_[1]; + NSString *database; + NSMutableArray *writesArray; + NSData *transaction; +} GCFSCommitRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "database", + .dataTypeSpecific.className = NULL, + .number = GCFSCommitRequest_FieldNumber_Database, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSCommitRequest__storage_, database), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "writesArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSWrite), + .number = GCFSCommitRequest_FieldNumber_WritesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSCommitRequest__storage_, writesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSCommitRequest_FieldNumber_Transaction, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSCommitRequest__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSCommitRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSCommitRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSCommitResponse + +@implementation GCFSCommitResponse + +@dynamic writeResultsArray, writeResultsArray_Count; +@dynamic hasCommitTime, commitTime; + +typedef struct GCFSCommitResponse__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *writeResultsArray; + GPBTimestamp *commitTime; +} GCFSCommitResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "writeResultsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSWriteResult), + .number = GCFSCommitResponse_FieldNumber_WriteResultsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSCommitResponse__storage_, writeResultsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "commitTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSCommitResponse_FieldNumber_CommitTime, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSCommitResponse__storage_, commitTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSCommitResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSCommitResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSRollbackRequest + +@implementation GCFSRollbackRequest + +@dynamic database; +@dynamic transaction; + +typedef struct GCFSRollbackRequest__storage_ { + uint32_t _has_storage_[1]; + NSString *database; + NSData *transaction; +} GCFSRollbackRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "database", + .dataTypeSpecific.className = NULL, + .number = GCFSRollbackRequest_FieldNumber_Database, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSRollbackRequest__storage_, database), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSRollbackRequest_FieldNumber_Transaction, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSRollbackRequest__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSRollbackRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSRollbackRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSRunQueryRequest + +@implementation GCFSRunQueryRequest + +@dynamic queryTypeOneOfCase; +@dynamic consistencySelectorOneOfCase; +@dynamic parent; +@dynamic structuredQuery; +@dynamic transaction; +@dynamic newTransaction; +@dynamic readTime; + +typedef struct GCFSRunQueryRequest__storage_ { + uint32_t _has_storage_[3]; + NSString *parent; + GCFSStructuredQuery *structuredQuery; + NSData *transaction; + GCFSTransactionOptions *newTransaction; + GPBTimestamp *readTime; +} GCFSRunQueryRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "parent", + .dataTypeSpecific.className = NULL, + .number = GCFSRunQueryRequest_FieldNumber_Parent, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, parent), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "structuredQuery", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery), + .number = GCFSRunQueryRequest_FieldNumber_StructuredQuery, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, structuredQuery), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSRunQueryRequest_FieldNumber_Transaction, + .hasIndex = -2, + .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "newTransaction", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTransactionOptions), + .number = GCFSRunQueryRequest_FieldNumber_NewTransaction, + .hasIndex = -2, + .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, newTransaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSRunQueryRequest_FieldNumber_ReadTime, + .hasIndex = -2, + .offset = (uint32_t)offsetof(GCFSRunQueryRequest__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSRunQueryRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSRunQueryRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "queryType", + "consistencySelector", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSRunQueryRequest_ClearQueryTypeOneOfCase(GCFSRunQueryRequest *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +void GCFSRunQueryRequest_ClearConsistencySelectorOneOfCase(GCFSRunQueryRequest *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:1]; + GPBMaybeClearOneof(message, oneof, -2, 0); +} +#pragma mark - GCFSRunQueryResponse + +@implementation GCFSRunQueryResponse + +@dynamic transaction; +@dynamic hasDocument, document; +@dynamic hasReadTime, readTime; +@dynamic skippedResults; + +typedef struct GCFSRunQueryResponse__storage_ { + uint32_t _has_storage_[1]; + int32_t skippedResults; + GCFSDocument *document; + NSData *transaction; + GPBTimestamp *readTime; +} GCFSRunQueryResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "document", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSRunQueryResponse_FieldNumber_Document, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transaction", + .dataTypeSpecific.className = NULL, + .number = GCFSRunQueryResponse_FieldNumber_Transaction, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, transaction), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSRunQueryResponse_FieldNumber_ReadTime, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "skippedResults", + .dataTypeSpecific.className = NULL, + .number = GCFSRunQueryResponse_FieldNumber_SkippedResults, + .hasIndex = 3, + .offset = (uint32_t)offsetof(GCFSRunQueryResponse__storage_, skippedResults), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSRunQueryResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSRunQueryResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSWriteRequest + +@implementation GCFSWriteRequest + +@dynamic database; +@dynamic streamId; +@dynamic writesArray, writesArray_Count; +@dynamic streamToken; +@dynamic labels, labels_Count; + +typedef struct GCFSWriteRequest__storage_ { + uint32_t _has_storage_[1]; + NSString *database; + NSString *streamId; + NSMutableArray *writesArray; + NSData *streamToken; + NSMutableDictionary *labels; +} GCFSWriteRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "database", + .dataTypeSpecific.className = NULL, + .number = GCFSWriteRequest_FieldNumber_Database, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, database), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "streamId", + .dataTypeSpecific.className = NULL, + .number = GCFSWriteRequest_FieldNumber_StreamId, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, streamId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "writesArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSWrite), + .number = GCFSWriteRequest_FieldNumber_WritesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, writesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "streamToken", + .dataTypeSpecific.className = NULL, + .number = GCFSWriteRequest_FieldNumber_StreamToken, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, streamToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "labels", + .dataTypeSpecific.className = NULL, + .number = GCFSWriteRequest_FieldNumber_Labels, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSWriteRequest__storage_, labels), + .flags = GPBFieldMapKeyString, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSWriteRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSWriteRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSWriteResponse + +@implementation GCFSWriteResponse + +@dynamic streamId; +@dynamic streamToken; +@dynamic writeResultsArray, writeResultsArray_Count; +@dynamic hasCommitTime, commitTime; + +typedef struct GCFSWriteResponse__storage_ { + uint32_t _has_storage_[1]; + NSString *streamId; + NSData *streamToken; + NSMutableArray *writeResultsArray; + GPBTimestamp *commitTime; +} GCFSWriteResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "streamId", + .dataTypeSpecific.className = NULL, + .number = GCFSWriteResponse_FieldNumber_StreamId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, streamId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "streamToken", + .dataTypeSpecific.className = NULL, + .number = GCFSWriteResponse_FieldNumber_StreamToken, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, streamToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "writeResultsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSWriteResult), + .number = GCFSWriteResponse_FieldNumber_WriteResultsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, writeResultsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "commitTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSWriteResponse_FieldNumber_CommitTime, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSWriteResponse__storage_, commitTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSWriteResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSWriteResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSListenRequest + +@implementation GCFSListenRequest + +@dynamic targetChangeOneOfCase; +@dynamic database; +@dynamic addTarget; +@dynamic removeTarget; +@dynamic labels, labels_Count; + +typedef struct GCFSListenRequest__storage_ { + uint32_t _has_storage_[2]; + int32_t removeTarget; + NSString *database; + GCFSTarget *addTarget; + NSMutableDictionary *labels; +} GCFSListenRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "database", + .dataTypeSpecific.className = NULL, + .number = GCFSListenRequest_FieldNumber_Database, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, database), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "addTarget", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget), + .number = GCFSListenRequest_FieldNumber_AddTarget, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, addTarget), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "removeTarget", + .dataTypeSpecific.className = NULL, + .number = GCFSListenRequest_FieldNumber_RemoveTarget, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, removeTarget), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "labels", + .dataTypeSpecific.className = NULL, + .number = GCFSListenRequest_FieldNumber_Labels, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSListenRequest__storage_, labels), + .flags = GPBFieldMapKeyString, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSListenRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSListenRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "targetChange", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSListenRequest_ClearTargetChangeOneOfCase(GCFSListenRequest *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSListenResponse + +@implementation GCFSListenResponse + +@dynamic responseTypeOneOfCase; +@dynamic targetChange; +@dynamic documentChange; +@dynamic documentDelete; +@dynamic documentRemove; +@dynamic filter; + +typedef struct GCFSListenResponse__storage_ { + uint32_t _has_storage_[2]; + GCFSTargetChange *targetChange; + GCFSDocumentChange *documentChange; + GCFSDocumentDelete *documentDelete; + GCFSExistenceFilter *filter; + GCFSDocumentRemove *documentRemove; +} GCFSListenResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "targetChange", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTargetChange), + .number = GCFSListenResponse_FieldNumber_TargetChange, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, targetChange), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "documentChange", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentChange), + .number = GCFSListenResponse_FieldNumber_DocumentChange, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, documentChange), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "documentDelete", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentDelete), + .number = GCFSListenResponse_FieldNumber_DocumentDelete, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, documentDelete), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "filter", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSExistenceFilter), + .number = GCFSListenResponse_FieldNumber_Filter, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, filter), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "documentRemove", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentRemove), + .number = GCFSListenResponse_FieldNumber_DocumentRemove, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSListenResponse__storage_, documentRemove), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSListenResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSListenResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "responseType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSListenResponse_ClearResponseTypeOneOfCase(GCFSListenResponse *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSTarget + +@implementation GCFSTarget + +@dynamic targetTypeOneOfCase; +@dynamic resumeTypeOneOfCase; +@dynamic query; +@dynamic documents; +@dynamic resumeToken; +@dynamic readTime; +@dynamic targetId; +@dynamic once; + +typedef struct GCFSTarget__storage_ { + uint32_t _has_storage_[3]; + int32_t targetId; + GCFSTarget_QueryTarget *query; + GCFSTarget_DocumentsTarget *documents; + NSData *resumeToken; + GPBTimestamp *readTime; +} GCFSTarget__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "query", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_QueryTarget), + .number = GCFSTarget_FieldNumber_Query, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSTarget__storage_, query), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "documents", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSTarget_DocumentsTarget), + .number = GCFSTarget_FieldNumber_Documents, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSTarget__storage_, documents), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "resumeToken", + .dataTypeSpecific.className = NULL, + .number = GCFSTarget_FieldNumber_ResumeToken, + .hasIndex = -2, + .offset = (uint32_t)offsetof(GCFSTarget__storage_, resumeToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "targetId", + .dataTypeSpecific.className = NULL, + .number = GCFSTarget_FieldNumber_TargetId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSTarget__storage_, targetId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "once", + .dataTypeSpecific.className = NULL, + .number = GCFSTarget_FieldNumber_Once, + .hasIndex = 1, + .offset = 2, // Stored in _has_storage_ to save space. + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBool, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSTarget_FieldNumber_ReadTime, + .hasIndex = -2, + .offset = (uint32_t)offsetof(GCFSTarget__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTarget class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTarget__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "targetType", + "resumeType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSTarget_ClearTargetTypeOneOfCase(GCFSTarget *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +void GCFSTarget_ClearResumeTypeOneOfCase(GCFSTarget *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:1]; + GPBMaybeClearOneof(message, oneof, -2, 0); +} +#pragma mark - GCFSTarget_DocumentsTarget + +@implementation GCFSTarget_DocumentsTarget + +@dynamic documentsArray, documentsArray_Count; + +typedef struct GCFSTarget_DocumentsTarget__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *documentsArray; +} GCFSTarget_DocumentsTarget__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "documentsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSTarget_DocumentsTarget_FieldNumber_DocumentsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSTarget_DocumentsTarget__storage_, documentsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTarget_DocumentsTarget class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTarget_DocumentsTarget__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTarget)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSTarget_QueryTarget + +@implementation GCFSTarget_QueryTarget + +@dynamic queryTypeOneOfCase; +@dynamic parent; +@dynamic structuredQuery; + +typedef struct GCFSTarget_QueryTarget__storage_ { + uint32_t _has_storage_[2]; + NSString *parent; + GCFSStructuredQuery *structuredQuery; +} GCFSTarget_QueryTarget__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "parent", + .dataTypeSpecific.className = NULL, + .number = GCFSTarget_QueryTarget_FieldNumber_Parent, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSTarget_QueryTarget__storage_, parent), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "structuredQuery", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery), + .number = GCFSTarget_QueryTarget_FieldNumber_StructuredQuery, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSTarget_QueryTarget__storage_, structuredQuery), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTarget_QueryTarget class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTarget_QueryTarget__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "queryType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSTarget)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSTarget_QueryTarget_ClearQueryTypeOneOfCase(GCFSTarget_QueryTarget *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSTargetChange + +@implementation GCFSTargetChange + +@dynamic targetChangeType; +@dynamic targetIdsArray, targetIdsArray_Count; +@dynamic hasCause, cause; +@dynamic resumeToken; +@dynamic hasReadTime, readTime; + +typedef struct GCFSTargetChange__storage_ { + uint32_t _has_storage_[1]; + GCFSTargetChange_TargetChangeType targetChangeType; + GPBInt32Array *targetIdsArray; + RPCStatus *cause; + NSData *resumeToken; + GPBTimestamp *readTime; +} GCFSTargetChange__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "targetChangeType", + .dataTypeSpecific.enumDescFunc = GCFSTargetChange_TargetChangeType_EnumDescriptor, + .number = GCFSTargetChange_FieldNumber_TargetChangeType, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, targetChangeType), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + { + .name = "targetIdsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSTargetChange_FieldNumber_TargetIdsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, targetIdsArray), + .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked), + .dataType = GPBDataTypeInt32, + }, + { + .name = "cause", + .dataTypeSpecific.className = GPBStringifySymbol(RPCStatus), + .number = GCFSTargetChange_FieldNumber_Cause, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, cause), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "resumeToken", + .dataTypeSpecific.className = NULL, + .number = GCFSTargetChange_FieldNumber_ResumeToken, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, resumeToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSTargetChange_FieldNumber_ReadTime, + .hasIndex = 3, + .offset = (uint32_t)offsetof(GCFSTargetChange__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSTargetChange class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSTargetChange__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message) { + GPBDescriptor *descriptor = [GCFSTargetChange descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSTargetChange_FieldNumber_TargetChangeType]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSTargetChange_TargetChangeType_RawValue(GCFSTargetChange *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSTargetChange descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSTargetChange_FieldNumber_TargetChangeType]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +#pragma mark - Enum GCFSTargetChange_TargetChangeType + +GPBEnumDescriptor *GCFSTargetChange_TargetChangeType_EnumDescriptor(void) { + static GPBEnumDescriptor *descriptor = NULL; + if (!descriptor) { + static const char *valueNames = + "NoChange\000Add\000Remove\000Current\000Reset\000"; + static const int32_t values[] = { + GCFSTargetChange_TargetChangeType_NoChange, + GCFSTargetChange_TargetChangeType_Add, + GCFSTargetChange_TargetChangeType_Remove, + GCFSTargetChange_TargetChangeType_Current, + GCFSTargetChange_TargetChangeType_Reset, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSTargetChange_TargetChangeType) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:GCFSTargetChange_TargetChangeType_IsValidValue]; + if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) { + [worker release]; + } + } + return descriptor; +} + +BOOL GCFSTargetChange_TargetChangeType_IsValidValue(int32_t value__) { + switch (value__) { + case GCFSTargetChange_TargetChangeType_NoChange: + case GCFSTargetChange_TargetChangeType_Add: + case GCFSTargetChange_TargetChangeType_Remove: + case GCFSTargetChange_TargetChangeType_Current: + case GCFSTargetChange_TargetChangeType_Reset: + return YES; + default: + return NO; + } +} + +#pragma mark - GCFSListCollectionIdsRequest + +@implementation GCFSListCollectionIdsRequest + +@dynamic parent; +@dynamic pageSize; +@dynamic pageToken; + +typedef struct GCFSListCollectionIdsRequest__storage_ { + uint32_t _has_storage_[1]; + int32_t pageSize; + NSString *parent; + NSString *pageToken; +} GCFSListCollectionIdsRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "parent", + .dataTypeSpecific.className = NULL, + .number = GCFSListCollectionIdsRequest_FieldNumber_Parent, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSListCollectionIdsRequest__storage_, parent), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "pageSize", + .dataTypeSpecific.className = NULL, + .number = GCFSListCollectionIdsRequest_FieldNumber_PageSize, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSListCollectionIdsRequest__storage_, pageSize), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "pageToken", + .dataTypeSpecific.className = NULL, + .number = GCFSListCollectionIdsRequest_FieldNumber_PageToken, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSListCollectionIdsRequest__storage_, pageToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSListCollectionIdsRequest class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSListCollectionIdsRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSListCollectionIdsResponse + +@implementation GCFSListCollectionIdsResponse + +@dynamic collectionIdsArray, collectionIdsArray_Count; +@dynamic nextPageToken; + +typedef struct GCFSListCollectionIdsResponse__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *collectionIdsArray; + NSString *nextPageToken; +} GCFSListCollectionIdsResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "collectionIdsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSListCollectionIdsResponse_FieldNumber_CollectionIdsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSListCollectionIdsResponse__storage_, collectionIdsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeString, + }, + { + .name = "nextPageToken", + .dataTypeSpecific.className = NULL, + .number = GCFSListCollectionIdsResponse_FieldNumber_NextPageToken, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSListCollectionIdsResponse__storage_, nextPageToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSListCollectionIdsResponse class] + rootClass:[GCFSFirestoreRoot class] + file:GCFSFirestoreRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSListCollectionIdsResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h new file mode 100644 index 0000000..5704c2b --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h @@ -0,0 +1,232 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore.pbobjc.h" + +#import +#import +#import +#import + +#import "Annotations.pbobjc.h" +#import "Common.pbobjc.h" +#import "Document.pbobjc.h" +#import "Query.pbobjc.h" +#import "Write.pbobjc.h" +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Empty.pbobjc.h" +#endif +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Timestamp.pbobjc.h" +#endif +#import "Status.pbobjc.h" + + +NS_ASSUME_NONNULL_BEGIN + +@protocol GCFSFirestore + +#pragma mark GetDocument(GetDocumentRequest) returns (Document) + +/** + * Gets a single document. + */ +- (void)getDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler; + +/** + * Gets a single document. + */ +- (GRPCProtoCall *)RPCToGetDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark ListDocuments(ListDocumentsRequest) returns (ListDocumentsResponse) + +/** + * Lists documents. + */ +- (void)listDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler; + +/** + * Lists documents. + */ +- (GRPCProtoCall *)RPCToListDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark CreateDocument(CreateDocumentRequest) returns (Document) + +/** + * Creates a new document. + */ +- (void)createDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler; + +/** + * Creates a new document. + */ +- (GRPCProtoCall *)RPCToCreateDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark UpdateDocument(UpdateDocumentRequest) returns (Document) + +/** + * Updates or inserts a document. + */ +- (void)updateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler; + +/** + * Updates or inserts a document. + */ +- (GRPCProtoCall *)RPCToUpdateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark DeleteDocument(DeleteDocumentRequest) returns (Empty) + +/** + * Deletes a document. + */ +- (void)deleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler; + +/** + * Deletes a document. + */ +- (GRPCProtoCall *)RPCToDeleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark BatchGetDocuments(BatchGetDocumentsRequest) returns (stream BatchGetDocumentsResponse) + +/** + * Gets multiple documents. + * + * Documents returned by this method are not guaranteed to be returned in the + * same order that they were requested. + */ +- (void)batchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler; + +/** + * Gets multiple documents. + * + * Documents returned by this method are not guaranteed to be returned in the + * same order that they were requested. + */ +- (GRPCProtoCall *)RPCToBatchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler; + + +#pragma mark BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse) + +/** + * Starts a new transaction. + */ +- (void)beginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler; + +/** + * Starts a new transaction. + */ +- (GRPCProtoCall *)RPCToBeginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark Commit(CommitRequest) returns (CommitResponse) + +/** + * Commits a transaction, while optionally updating documents. + */ +- (void)commitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler; + +/** + * Commits a transaction, while optionally updating documents. + */ +- (GRPCProtoCall *)RPCToCommitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark Rollback(RollbackRequest) returns (Empty) + +/** + * Rolls back a transaction. + */ +- (void)rollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler; + +/** + * Rolls back a transaction. + */ +- (GRPCProtoCall *)RPCToRollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler; + + +#pragma mark RunQuery(RunQueryRequest) returns (stream RunQueryResponse) + +/** + * Runs a query. + */ +- (void)runQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler; + +/** + * Runs a query. + */ +- (GRPCProtoCall *)RPCToRunQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler; + + +#pragma mark Write(stream WriteRequest) returns (stream WriteResponse) + +/** + * Streams batches of document updates and deletes, in order. + */ +- (void)writeWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler; + +/** + * Streams batches of document updates and deletes, in order. + */ +- (GRPCProtoCall *)RPCToWriteWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler; + + +#pragma mark Listen(stream ListenRequest) returns (stream ListenResponse) + +/** + * Listens to changes. + */ +- (void)listenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler; + +/** + * Listens to changes. + */ +- (GRPCProtoCall *)RPCToListenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler; + + +#pragma mark ListCollectionIds(ListCollectionIdsRequest) returns (ListCollectionIdsResponse) + +/** + * Lists all the collection IDs underneath a document. + */ +- (void)listCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler; + +/** + * Lists all the collection IDs underneath a document. + */ +- (GRPCProtoCall *)RPCToListCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler; + + +@end + +/** + * Basic service implementation, over gRPC, that only does + * marshalling and parsing. + */ +@interface GCFSFirestore : GRPCProtoService +- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; ++ (instancetype)serviceWithHost:(NSString *)host; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m new file mode 100644 index 0000000..a3e338d --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.m @@ -0,0 +1,281 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Firestore.pbrpc.h" + +#import +#import + +@implementation GCFSFirestore + +// Designated initializer +- (instancetype)initWithHost:(NSString *)host { + return (self = [super initWithHost:host packageName:@"google.firestore.v1beta1" serviceName:@"Firestore"]); +} + +// Override superclass initializer to disallow different package and service names. +- (instancetype)initWithHost:(NSString *)host + packageName:(NSString *)packageName + serviceName:(NSString *)serviceName { + return [self initWithHost:host]; +} + ++ (instancetype)serviceWithHost:(NSString *)host { + return [[self alloc] initWithHost:host]; +} + + +#pragma mark GetDocument(GetDocumentRequest) returns (Document) + +/** + * Gets a single document. + */ +- (void)getDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToGetDocumentWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Gets a single document. + */ +- (GRPCProtoCall *)RPCToGetDocumentWithRequest:(GCFSGetDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"GetDocument" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSDocument class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark ListDocuments(ListDocumentsRequest) returns (ListDocumentsResponse) + +/** + * Lists documents. + */ +- (void)listDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToListDocumentsWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Lists documents. + */ +- (GRPCProtoCall *)RPCToListDocumentsWithRequest:(GCFSListDocumentsRequest *)request handler:(void(^)(GCFSListDocumentsResponse *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"ListDocuments" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSListDocumentsResponse class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark CreateDocument(CreateDocumentRequest) returns (Document) + +/** + * Creates a new document. + */ +- (void)createDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToCreateDocumentWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Creates a new document. + */ +- (GRPCProtoCall *)RPCToCreateDocumentWithRequest:(GCFSCreateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"CreateDocument" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSDocument class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark UpdateDocument(UpdateDocumentRequest) returns (Document) + +/** + * Updates or inserts a document. + */ +- (void)updateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToUpdateDocumentWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Updates or inserts a document. + */ +- (GRPCProtoCall *)RPCToUpdateDocumentWithRequest:(GCFSUpdateDocumentRequest *)request handler:(void(^)(GCFSDocument *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"UpdateDocument" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSDocument class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark DeleteDocument(DeleteDocumentRequest) returns (Empty) + +/** + * Deletes a document. + */ +- (void)deleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToDeleteDocumentWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Deletes a document. + */ +- (GRPCProtoCall *)RPCToDeleteDocumentWithRequest:(GCFSDeleteDocumentRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"DeleteDocument" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GPBEmpty class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark BatchGetDocuments(BatchGetDocumentsRequest) returns (stream BatchGetDocumentsResponse) + +/** + * Gets multiple documents. + * + * Documents returned by this method are not guaranteed to be returned in the + * same order that they were requested. + */ +- (void)batchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + [[self RPCToBatchGetDocumentsWithRequest:request eventHandler:eventHandler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Gets multiple documents. + * + * Documents returned by this method are not guaranteed to be returned in the + * same order that they were requested. + */ +- (GRPCProtoCall *)RPCToBatchGetDocumentsWithRequest:(GCFSBatchGetDocumentsRequest *)request eventHandler:(void(^)(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + return [self RPCToMethod:@"BatchGetDocuments" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSBatchGetDocumentsResponse class] + responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]]; +} +#pragma mark BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse) + +/** + * Starts a new transaction. + */ +- (void)beginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToBeginTransactionWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Starts a new transaction. + */ +- (GRPCProtoCall *)RPCToBeginTransactionWithRequest:(GCFSBeginTransactionRequest *)request handler:(void(^)(GCFSBeginTransactionResponse *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"BeginTransaction" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSBeginTransactionResponse class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark Commit(CommitRequest) returns (CommitResponse) + +/** + * Commits a transaction, while optionally updating documents. + */ +- (void)commitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToCommitWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Commits a transaction, while optionally updating documents. + */ +- (GRPCProtoCall *)RPCToCommitWithRequest:(GCFSCommitRequest *)request handler:(void(^)(GCFSCommitResponse *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"Commit" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSCommitResponse class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark Rollback(RollbackRequest) returns (Empty) + +/** + * Rolls back a transaction. + */ +- (void)rollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToRollbackWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Rolls back a transaction. + */ +- (GRPCProtoCall *)RPCToRollbackWithRequest:(GCFSRollbackRequest *)request handler:(void(^)(GPBEmpty *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"Rollback" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GPBEmpty class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +#pragma mark RunQuery(RunQueryRequest) returns (stream RunQueryResponse) + +/** + * Runs a query. + */ +- (void)runQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + [[self RPCToRunQueryWithRequest:request eventHandler:eventHandler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Runs a query. + */ +- (GRPCProtoCall *)RPCToRunQueryWithRequest:(GCFSRunQueryRequest *)request eventHandler:(void(^)(BOOL done, GCFSRunQueryResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + return [self RPCToMethod:@"RunQuery" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSRunQueryResponse class] + responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]]; +} +#pragma mark Write(stream WriteRequest) returns (stream WriteResponse) + +/** + * Streams batches of document updates and deletes, in order. + */ +- (void)writeWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + [[self RPCToWriteWithRequestsWriter:requestWriter eventHandler:eventHandler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Streams batches of document updates and deletes, in order. + */ +- (GRPCProtoCall *)RPCToWriteWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSWriteResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + return [self RPCToMethod:@"Write" + requestsWriter:requestWriter + responseClass:[GCFSWriteResponse class] + responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]]; +} +#pragma mark Listen(stream ListenRequest) returns (stream ListenResponse) + +/** + * Listens to changes. + */ +- (void)listenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + [[self RPCToListenWithRequestsWriter:requestWriter eventHandler:eventHandler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Listens to changes. + */ +- (GRPCProtoCall *)RPCToListenWithRequestsWriter:(GRXWriter *)requestWriter eventHandler:(void(^)(BOOL done, GCFSListenResponse *_Nullable response, NSError *_Nullable error))eventHandler{ + return [self RPCToMethod:@"Listen" + requestsWriter:requestWriter + responseClass:[GCFSListenResponse class] + responsesWriteable:[GRXWriteable writeableWithEventHandler:eventHandler]]; +} +#pragma mark ListCollectionIds(ListCollectionIdsRequest) returns (ListCollectionIdsResponse) + +/** + * Lists all the collection IDs underneath a document. + */ +- (void)listCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler{ + [[self RPCToListCollectionIdsWithRequest:request handler:handler] start]; +} +// Returns a not-yet-started RPC object. +/** + * Lists all the collection IDs underneath a document. + */ +- (GRPCProtoCall *)RPCToListCollectionIdsWithRequest:(GCFSListCollectionIdsRequest *)request handler:(void(^)(GCFSListCollectionIdsResponse *_Nullable response, NSError *_Nullable error))handler{ + return [self RPCToMethod:@"ListCollectionIds" + requestsWriter:[GRXWriter writerWithValue:request] + responseClass:[GCFSListCollectionIdsResponse class] + responsesWriteable:[GRXWriteable writeableWithSingleHandler:handler]]; +} +@end diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h new file mode 100644 index 0000000..c2d80e7 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h @@ -0,0 +1,579 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/query.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSCursor; +@class GCFSStructuredQuery_CollectionSelector; +@class GCFSStructuredQuery_CompositeFilter; +@class GCFSStructuredQuery_FieldFilter; +@class GCFSStructuredQuery_FieldReference; +@class GCFSStructuredQuery_Filter; +@class GCFSStructuredQuery_Order; +@class GCFSStructuredQuery_Projection; +@class GCFSStructuredQuery_UnaryFilter; +@class GCFSValue; +@class GPBInt32Value; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Enum GCFSStructuredQuery_Direction + +/** A sort direction. */ +typedef GPB_ENUM(GCFSStructuredQuery_Direction) { + /** + * Value used if any message's field encounters a value that is not defined + * by this enum. The message will also have C functions to get/set the rawValue + * of the field. + **/ + GCFSStructuredQuery_Direction_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, + /** Unspecified. */ + GCFSStructuredQuery_Direction_DirectionUnspecified = 0, + + /** Ascending. */ + GCFSStructuredQuery_Direction_Ascending = 1, + + /** Descending. */ + GCFSStructuredQuery_Direction_Descending = 2, +}; + +GPBEnumDescriptor *GCFSStructuredQuery_Direction_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL GCFSStructuredQuery_Direction_IsValidValue(int32_t value); + +#pragma mark - Enum GCFSStructuredQuery_CompositeFilter_Operator + +/** A composite filter operator. */ +typedef GPB_ENUM(GCFSStructuredQuery_CompositeFilter_Operator) { + /** + * Value used if any message's field encounters a value that is not defined + * by this enum. The message will also have C functions to get/set the rawValue + * of the field. + **/ + GCFSStructuredQuery_CompositeFilter_Operator_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, + /** Unspecified. This value must not be used. */ + GCFSStructuredQuery_CompositeFilter_Operator_OperatorUnspecified = 0, + + /** The results are required to satisfy each of the combined filters. */ + GCFSStructuredQuery_CompositeFilter_Operator_And = 1, +}; + +GPBEnumDescriptor *GCFSStructuredQuery_CompositeFilter_Operator_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL GCFSStructuredQuery_CompositeFilter_Operator_IsValidValue(int32_t value); + +#pragma mark - Enum GCFSStructuredQuery_FieldFilter_Operator + +/** A field filter operator. */ +typedef GPB_ENUM(GCFSStructuredQuery_FieldFilter_Operator) { + /** + * Value used if any message's field encounters a value that is not defined + * by this enum. The message will also have C functions to get/set the rawValue + * of the field. + **/ + GCFSStructuredQuery_FieldFilter_Operator_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, + /** Unspecified. This value must not be used. */ + GCFSStructuredQuery_FieldFilter_Operator_OperatorUnspecified = 0, + + /** Less than. Requires that the field come first in `order_by`. */ + GCFSStructuredQuery_FieldFilter_Operator_LessThan = 1, + + /** Less than or equal. Requires that the field come first in `order_by`. */ + GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual = 2, + + /** Greater than. Requires that the field come first in `order_by`. */ + GCFSStructuredQuery_FieldFilter_Operator_GreaterThan = 3, + + /** + * Greater than or equal. Requires that the field come first in + * `order_by`. + **/ + GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual = 4, + + /** Equal. */ + GCFSStructuredQuery_FieldFilter_Operator_Equal = 5, +}; + +GPBEnumDescriptor *GCFSStructuredQuery_FieldFilter_Operator_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL GCFSStructuredQuery_FieldFilter_Operator_IsValidValue(int32_t value); + +#pragma mark - Enum GCFSStructuredQuery_UnaryFilter_Operator + +/** A unary operator. */ +typedef GPB_ENUM(GCFSStructuredQuery_UnaryFilter_Operator) { + /** + * Value used if any message's field encounters a value that is not defined + * by this enum. The message will also have C functions to get/set the rawValue + * of the field. + **/ + GCFSStructuredQuery_UnaryFilter_Operator_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, + /** Unspecified. This value must not be used. */ + GCFSStructuredQuery_UnaryFilter_Operator_OperatorUnspecified = 0, + + /** Test if a field is equal to NaN. */ + GCFSStructuredQuery_UnaryFilter_Operator_IsNan = 2, + + /** Test if an exprestion evaluates to Null. */ + GCFSStructuredQuery_UnaryFilter_Operator_IsNull = 3, +}; + +GPBEnumDescriptor *GCFSStructuredQuery_UnaryFilter_Operator_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL GCFSStructuredQuery_UnaryFilter_Operator_IsValidValue(int32_t value); + +#pragma mark - GCFSQueryRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GCFSQueryRoot : GPBRootObject +@end + +#pragma mark - GCFSStructuredQuery + +typedef GPB_ENUM(GCFSStructuredQuery_FieldNumber) { + GCFSStructuredQuery_FieldNumber_Select = 1, + GCFSStructuredQuery_FieldNumber_FromArray = 2, + GCFSStructuredQuery_FieldNumber_Where = 3, + GCFSStructuredQuery_FieldNumber_OrderByArray = 4, + GCFSStructuredQuery_FieldNumber_Limit = 5, + GCFSStructuredQuery_FieldNumber_Offset = 6, + GCFSStructuredQuery_FieldNumber_StartAt = 7, + GCFSStructuredQuery_FieldNumber_EndAt = 8, +}; + +/** + * A Firestore query. + **/ +@interface GCFSStructuredQuery : GPBMessage + +/** The projection to return. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_Projection *select; +/** Test to see if @c select has been set. */ +@property(nonatomic, readwrite) BOOL hasSelect; + +/** The collections to query. */ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *fromArray; +/** The number of items in @c fromArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger fromArray_Count; + +/** The filter to apply. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_Filter *where; +/** Test to see if @c where has been set. */ +@property(nonatomic, readwrite) BOOL hasWhere; + +/** + * The order to apply to the query results. + * + * Firestore guarantees a stable ordering through the following rules: + * + * * Any field required to appear in `order_by`, that is not already + * specified in `order_by`, is appended to the order in field name order + * by default. + * * If an order on `__name__` is not specified, it is appended by default. + * + * Fields are appended with the same sort direction as the last order + * specified, or 'ASCENDING' if no order was specified. For example: + * + * * `SELECT * FROM Foo ORDER BY A` becomes + * `SELECT * FROM Foo ORDER BY A, __name__` + * * `SELECT * FROM Foo ORDER BY A DESC` becomes + * `SELECT * FROM Foo ORDER BY A DESC, __name__ DESC` + * * `SELECT * FROM Foo WHERE A > 1` becomes + * `SELECT * FROM Foo WHERE A > 1 ORDER BY A, __name__` + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *orderByArray; +/** The number of items in @c orderByArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger orderByArray_Count; + +/** A starting point for the query results. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSCursor *startAt; +/** Test to see if @c startAt has been set. */ +@property(nonatomic, readwrite) BOOL hasStartAt; + +/** A end point for the query results. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSCursor *endAt; +/** Test to see if @c endAt has been set. */ +@property(nonatomic, readwrite) BOOL hasEndAt; + +/** + * The number of results to skip. + * + * Applies before limit, but after all other constraints. Must be >= 0 if + * specified. + **/ +@property(nonatomic, readwrite) int32_t offset; + +/** + * The maximum number of results to return. + * + * Applies after all other constraints. + * Must be >= 0 if specified. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Value *limit; +/** Test to see if @c limit has been set. */ +@property(nonatomic, readwrite) BOOL hasLimit; + +@end + +#pragma mark - GCFSStructuredQuery_CollectionSelector + +typedef GPB_ENUM(GCFSStructuredQuery_CollectionSelector_FieldNumber) { + GCFSStructuredQuery_CollectionSelector_FieldNumber_CollectionId = 2, + GCFSStructuredQuery_CollectionSelector_FieldNumber_AllDescendants = 3, +}; + +/** + * A selection of a collection, such as `messages as m1`. + **/ +@interface GCFSStructuredQuery_CollectionSelector : GPBMessage + +/** + * The collection ID. + * When set, selects only collections with this ID. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *collectionId; + +/** + * When false, selects only collections that are immediate children of + * the `parent` specified in the containing `RunQueryRequest`. + * When true, selects all descendant collections. + **/ +@property(nonatomic, readwrite) BOOL allDescendants; + +@end + +#pragma mark - GCFSStructuredQuery_Filter + +typedef GPB_ENUM(GCFSStructuredQuery_Filter_FieldNumber) { + GCFSStructuredQuery_Filter_FieldNumber_CompositeFilter = 1, + GCFSStructuredQuery_Filter_FieldNumber_FieldFilter = 2, + GCFSStructuredQuery_Filter_FieldNumber_UnaryFilter = 3, +}; + +typedef GPB_ENUM(GCFSStructuredQuery_Filter_FilterType_OneOfCase) { + GCFSStructuredQuery_Filter_FilterType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter = 1, + GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter = 2, + GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter = 3, +}; + +/** + * A filter. + **/ +@interface GCFSStructuredQuery_Filter : GPBMessage + +/** The type of filter. */ +@property(nonatomic, readonly) GCFSStructuredQuery_Filter_FilterType_OneOfCase filterTypeOneOfCase; + +/** A composite filter. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_CompositeFilter *compositeFilter; + +/** A filter on a document field. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldFilter *fieldFilter; + +/** A filter that takes exactly one argument. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_UnaryFilter *unaryFilter; + +@end + +/** + * Clears whatever value was set for the oneof 'filterType'. + **/ +void GCFSStructuredQuery_Filter_ClearFilterTypeOneOfCase(GCFSStructuredQuery_Filter *message); + +#pragma mark - GCFSStructuredQuery_CompositeFilter + +typedef GPB_ENUM(GCFSStructuredQuery_CompositeFilter_FieldNumber) { + GCFSStructuredQuery_CompositeFilter_FieldNumber_Op = 1, + GCFSStructuredQuery_CompositeFilter_FieldNumber_FiltersArray = 2, +}; + +/** + * A filter that merges multiple other filters using the given operator. + **/ +@interface GCFSStructuredQuery_CompositeFilter : GPBMessage + +/** The operator for combining multiple filters. */ +@property(nonatomic, readwrite) GCFSStructuredQuery_CompositeFilter_Operator op; + +/** + * The list of filters to combine. + * Must contain at least one filter. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *filtersArray; +/** The number of items in @c filtersArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger filtersArray_Count; + +@end + +/** + * Fetches the raw value of a @c GCFSStructuredQuery_CompositeFilter's @c op property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message); +/** + * Sets the raw value of an @c GCFSStructuredQuery_CompositeFilter's @c op property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message, int32_t value); + +#pragma mark - GCFSStructuredQuery_FieldFilter + +typedef GPB_ENUM(GCFSStructuredQuery_FieldFilter_FieldNumber) { + GCFSStructuredQuery_FieldFilter_FieldNumber_Field = 1, + GCFSStructuredQuery_FieldFilter_FieldNumber_Op = 2, + GCFSStructuredQuery_FieldFilter_FieldNumber_Value = 3, +}; + +/** + * A filter on a specific field. + **/ +@interface GCFSStructuredQuery_FieldFilter : GPBMessage + +/** The field to filter by. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldReference *field; +/** Test to see if @c field has been set. */ +@property(nonatomic, readwrite) BOOL hasField; + +/** The operator to filter by. */ +@property(nonatomic, readwrite) GCFSStructuredQuery_FieldFilter_Operator op; + +/** The value to compare to. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSValue *value; +/** Test to see if @c value has been set. */ +@property(nonatomic, readwrite) BOOL hasValue; + +@end + +/** + * Fetches the raw value of a @c GCFSStructuredQuery_FieldFilter's @c op property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message); +/** + * Sets the raw value of an @c GCFSStructuredQuery_FieldFilter's @c op property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message, int32_t value); + +#pragma mark - GCFSStructuredQuery_UnaryFilter + +typedef GPB_ENUM(GCFSStructuredQuery_UnaryFilter_FieldNumber) { + GCFSStructuredQuery_UnaryFilter_FieldNumber_Op = 1, + GCFSStructuredQuery_UnaryFilter_FieldNumber_Field = 2, +}; + +typedef GPB_ENUM(GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase) { + GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase_Field = 2, +}; + +/** + * A filter with a single operand. + **/ +@interface GCFSStructuredQuery_UnaryFilter : GPBMessage + +/** The unary operator to apply. */ +@property(nonatomic, readwrite) GCFSStructuredQuery_UnaryFilter_Operator op; + +/** The argument to the filter. */ +@property(nonatomic, readonly) GCFSStructuredQuery_UnaryFilter_OperandType_OneOfCase operandTypeOneOfCase; + +/** The field to which to apply the operator. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldReference *field; + +@end + +/** + * Fetches the raw value of a @c GCFSStructuredQuery_UnaryFilter's @c op property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message); +/** + * Sets the raw value of an @c GCFSStructuredQuery_UnaryFilter's @c op property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message, int32_t value); + +/** + * Clears whatever value was set for the oneof 'operandType'. + **/ +void GCFSStructuredQuery_UnaryFilter_ClearOperandTypeOneOfCase(GCFSStructuredQuery_UnaryFilter *message); + +#pragma mark - GCFSStructuredQuery_Order + +typedef GPB_ENUM(GCFSStructuredQuery_Order_FieldNumber) { + GCFSStructuredQuery_Order_FieldNumber_Field = 1, + GCFSStructuredQuery_Order_FieldNumber_Direction = 2, +}; + +/** + * An order on a field. + **/ +@interface GCFSStructuredQuery_Order : GPBMessage + +/** The field to order by. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSStructuredQuery_FieldReference *field; +/** Test to see if @c field has been set. */ +@property(nonatomic, readwrite) BOOL hasField; + +/** The direction to order by. Defaults to `ASCENDING`. */ +@property(nonatomic, readwrite) GCFSStructuredQuery_Direction direction; + +@end + +/** + * Fetches the raw value of a @c GCFSStructuredQuery_Order's @c direction property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message); +/** + * Sets the raw value of an @c GCFSStructuredQuery_Order's @c direction property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message, int32_t value); + +#pragma mark - GCFSStructuredQuery_FieldReference + +typedef GPB_ENUM(GCFSStructuredQuery_FieldReference_FieldNumber) { + GCFSStructuredQuery_FieldReference_FieldNumber_FieldPath = 2, +}; + +/** + * A reference to a field, such as `max(messages.time) as max_time`. + **/ +@interface GCFSStructuredQuery_FieldReference : GPBMessage + +@property(nonatomic, readwrite, copy, null_resettable) NSString *fieldPath; + +@end + +#pragma mark - GCFSStructuredQuery_Projection + +typedef GPB_ENUM(GCFSStructuredQuery_Projection_FieldNumber) { + GCFSStructuredQuery_Projection_FieldNumber_FieldsArray = 2, +}; + +/** + * The projection of document's fields to return. + **/ +@interface GCFSStructuredQuery_Projection : GPBMessage + +/** + * The fields to return. + * + * If empty, all fields are returned. To only return the name + * of the document, use `['__name__']`. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *fieldsArray; +/** The number of items in @c fieldsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger fieldsArray_Count; + +@end + +#pragma mark - GCFSCursor + +typedef GPB_ENUM(GCFSCursor_FieldNumber) { + GCFSCursor_FieldNumber_ValuesArray = 1, + GCFSCursor_FieldNumber_Before = 2, +}; + +/** + * A position in a query result set. + **/ +@interface GCFSCursor : GPBMessage + +/** + * The values that represent a position, in the order they appear in + * the order by clause of a query. + * + * Can contain fewer values than specified in the order by clause. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *valuesArray; +/** The number of items in @c valuesArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger valuesArray_Count; + +/** + * If the position is just before or just after the given values, relative + * to the sort order defined by the query. + **/ +@property(nonatomic, readwrite) BOOL before; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m new file mode 100644 index 0000000..804a5d0 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.m @@ -0,0 +1,907 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/query.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Wrappers.pbobjc.h" +#endif + + #import "Query.pbobjc.h" + #import "Annotations.pbobjc.h" + #import "Document.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - GCFSQueryRoot + +@implementation GCFSQueryRoot + + +@end + +#pragma mark - GCFSQueryRoot_FileDescriptor + +static GPBFileDescriptor *GCFSQueryRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1" + objcPrefix:@"GCFS" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GCFSStructuredQuery + +@implementation GCFSStructuredQuery + +@dynamic hasSelect, select; +@dynamic fromArray, fromArray_Count; +@dynamic hasWhere, where; +@dynamic orderByArray, orderByArray_Count; +@dynamic hasStartAt, startAt; +@dynamic hasEndAt, endAt; +@dynamic offset; +@dynamic hasLimit, limit; + +typedef struct GCFSStructuredQuery__storage_ { + uint32_t _has_storage_[1]; + int32_t offset; + GCFSStructuredQuery_Projection *select; + NSMutableArray *fromArray; + GCFSStructuredQuery_Filter *where; + NSMutableArray *orderByArray; + GPBInt32Value *limit; + GCFSCursor *startAt; + GCFSCursor *endAt; +} GCFSStructuredQuery__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "select", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Projection), + .number = GCFSStructuredQuery_FieldNumber_Select, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, select), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "fromArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_CollectionSelector), + .number = GCFSStructuredQuery_FieldNumber_FromArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, fromArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "where", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Filter), + .number = GCFSStructuredQuery_FieldNumber_Where, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, where), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "orderByArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Order), + .number = GCFSStructuredQuery_FieldNumber_OrderByArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, orderByArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "limit", + .dataTypeSpecific.className = GPBStringifySymbol(GPBInt32Value), + .number = GCFSStructuredQuery_FieldNumber_Limit, + .hasIndex = 5, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, limit), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "offset", + .dataTypeSpecific.className = NULL, + .number = GCFSStructuredQuery_FieldNumber_Offset, + .hasIndex = 4, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, offset), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "startAt", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSCursor), + .number = GCFSStructuredQuery_FieldNumber_StartAt, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, startAt), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "endAt", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSCursor), + .number = GCFSStructuredQuery_FieldNumber_EndAt, + .hasIndex = 3, + .offset = (uint32_t)offsetof(GCFSStructuredQuery__storage_, endAt), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - Enum GCFSStructuredQuery_Direction + +GPBEnumDescriptor *GCFSStructuredQuery_Direction_EnumDescriptor(void) { + static GPBEnumDescriptor *descriptor = NULL; + if (!descriptor) { + static const char *valueNames = + "DirectionUnspecified\000Ascending\000Descendin" + "g\000"; + static const int32_t values[] = { + GCFSStructuredQuery_Direction_DirectionUnspecified, + GCFSStructuredQuery_Direction_Ascending, + GCFSStructuredQuery_Direction_Descending, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_Direction) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:GCFSStructuredQuery_Direction_IsValidValue]; + if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) { + [worker release]; + } + } + return descriptor; +} + +BOOL GCFSStructuredQuery_Direction_IsValidValue(int32_t value__) { + switch (value__) { + case GCFSStructuredQuery_Direction_DirectionUnspecified: + case GCFSStructuredQuery_Direction_Ascending: + case GCFSStructuredQuery_Direction_Descending: + return YES; + default: + return NO; + } +} + +#pragma mark - GCFSStructuredQuery_CollectionSelector + +@implementation GCFSStructuredQuery_CollectionSelector + +@dynamic collectionId; +@dynamic allDescendants; + +typedef struct GCFSStructuredQuery_CollectionSelector__storage_ { + uint32_t _has_storage_[1]; + NSString *collectionId; +} GCFSStructuredQuery_CollectionSelector__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "collectionId", + .dataTypeSpecific.className = NULL, + .number = GCFSStructuredQuery_CollectionSelector_FieldNumber_CollectionId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_CollectionSelector__storage_, collectionId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "allDescendants", + .dataTypeSpecific.className = NULL, + .number = GCFSStructuredQuery_CollectionSelector_FieldNumber_AllDescendants, + .hasIndex = 1, + .offset = 2, // Stored in _has_storage_ to save space. + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBool, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_CollectionSelector class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_CollectionSelector__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSStructuredQuery_Filter + +@implementation GCFSStructuredQuery_Filter + +@dynamic filterTypeOneOfCase; +@dynamic compositeFilter; +@dynamic fieldFilter; +@dynamic unaryFilter; + +typedef struct GCFSStructuredQuery_Filter__storage_ { + uint32_t _has_storage_[2]; + GCFSStructuredQuery_CompositeFilter *compositeFilter; + GCFSStructuredQuery_FieldFilter *fieldFilter; + GCFSStructuredQuery_UnaryFilter *unaryFilter; +} GCFSStructuredQuery_Filter__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "compositeFilter", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_CompositeFilter), + .number = GCFSStructuredQuery_Filter_FieldNumber_CompositeFilter, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_Filter__storage_, compositeFilter), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "fieldFilter", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldFilter), + .number = GCFSStructuredQuery_Filter_FieldNumber_FieldFilter, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_Filter__storage_, fieldFilter), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "unaryFilter", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_UnaryFilter), + .number = GCFSStructuredQuery_Filter_FieldNumber_UnaryFilter, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_Filter__storage_, unaryFilter), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_Filter class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_Filter__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "filterType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSStructuredQuery_Filter_ClearFilterTypeOneOfCase(GCFSStructuredQuery_Filter *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSStructuredQuery_CompositeFilter + +@implementation GCFSStructuredQuery_CompositeFilter + +@dynamic op; +@dynamic filtersArray, filtersArray_Count; + +typedef struct GCFSStructuredQuery_CompositeFilter__storage_ { + uint32_t _has_storage_[1]; + GCFSStructuredQuery_CompositeFilter_Operator op; + NSMutableArray *filtersArray; +} GCFSStructuredQuery_CompositeFilter__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "op", + .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_CompositeFilter_Operator_EnumDescriptor, + .number = GCFSStructuredQuery_CompositeFilter_FieldNumber_Op, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_CompositeFilter__storage_, op), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + { + .name = "filtersArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_Filter), + .number = GCFSStructuredQuery_CompositeFilter_FieldNumber_FiltersArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_CompositeFilter__storage_, filtersArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_CompositeFilter class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_CompositeFilter__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_CompositeFilter descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_CompositeFilter_FieldNumber_Op]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSStructuredQuery_CompositeFilter_Op_RawValue(GCFSStructuredQuery_CompositeFilter *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_CompositeFilter descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_CompositeFilter_FieldNumber_Op]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +#pragma mark - Enum GCFSStructuredQuery_CompositeFilter_Operator + +GPBEnumDescriptor *GCFSStructuredQuery_CompositeFilter_Operator_EnumDescriptor(void) { + static GPBEnumDescriptor *descriptor = NULL; + if (!descriptor) { + static const char *valueNames = + "OperatorUnspecified\000And\000"; + static const int32_t values[] = { + GCFSStructuredQuery_CompositeFilter_Operator_OperatorUnspecified, + GCFSStructuredQuery_CompositeFilter_Operator_And, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_CompositeFilter_Operator) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:GCFSStructuredQuery_CompositeFilter_Operator_IsValidValue]; + if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) { + [worker release]; + } + } + return descriptor; +} + +BOOL GCFSStructuredQuery_CompositeFilter_Operator_IsValidValue(int32_t value__) { + switch (value__) { + case GCFSStructuredQuery_CompositeFilter_Operator_OperatorUnspecified: + case GCFSStructuredQuery_CompositeFilter_Operator_And: + return YES; + default: + return NO; + } +} + +#pragma mark - GCFSStructuredQuery_FieldFilter + +@implementation GCFSStructuredQuery_FieldFilter + +@dynamic hasField, field; +@dynamic op; +@dynamic hasValue, value; + +typedef struct GCFSStructuredQuery_FieldFilter__storage_ { + uint32_t _has_storage_[1]; + GCFSStructuredQuery_FieldFilter_Operator op; + GCFSStructuredQuery_FieldReference *field; + GCFSValue *value; +} GCFSStructuredQuery_FieldFilter__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "field", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference), + .number = GCFSStructuredQuery_FieldFilter_FieldNumber_Field, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldFilter__storage_, field), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "op", + .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_FieldFilter_Operator_EnumDescriptor, + .number = GCFSStructuredQuery_FieldFilter_FieldNumber_Op, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldFilter__storage_, op), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + { + .name = "value", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue), + .number = GCFSStructuredQuery_FieldFilter_FieldNumber_Value, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldFilter__storage_, value), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_FieldFilter class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_FieldFilter__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_FieldFilter descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_FieldFilter_FieldNumber_Op]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSStructuredQuery_FieldFilter_Op_RawValue(GCFSStructuredQuery_FieldFilter *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_FieldFilter descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_FieldFilter_FieldNumber_Op]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +#pragma mark - Enum GCFSStructuredQuery_FieldFilter_Operator + +GPBEnumDescriptor *GCFSStructuredQuery_FieldFilter_Operator_EnumDescriptor(void) { + static GPBEnumDescriptor *descriptor = NULL; + if (!descriptor) { + static const char *valueNames = + "OperatorUnspecified\000LessThan\000LessThanOrE" + "qual\000GreaterThan\000GreaterThanOrEqual\000Equa" + "l\000"; + static const int32_t values[] = { + GCFSStructuredQuery_FieldFilter_Operator_OperatorUnspecified, + GCFSStructuredQuery_FieldFilter_Operator_LessThan, + GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual, + GCFSStructuredQuery_FieldFilter_Operator_GreaterThan, + GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual, + GCFSStructuredQuery_FieldFilter_Operator_Equal, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_FieldFilter_Operator) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:GCFSStructuredQuery_FieldFilter_Operator_IsValidValue]; + if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) { + [worker release]; + } + } + return descriptor; +} + +BOOL GCFSStructuredQuery_FieldFilter_Operator_IsValidValue(int32_t value__) { + switch (value__) { + case GCFSStructuredQuery_FieldFilter_Operator_OperatorUnspecified: + case GCFSStructuredQuery_FieldFilter_Operator_LessThan: + case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual: + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan: + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual: + case GCFSStructuredQuery_FieldFilter_Operator_Equal: + return YES; + default: + return NO; + } +} + +#pragma mark - GCFSStructuredQuery_UnaryFilter + +@implementation GCFSStructuredQuery_UnaryFilter + +@dynamic operandTypeOneOfCase; +@dynamic op; +@dynamic field; + +typedef struct GCFSStructuredQuery_UnaryFilter__storage_ { + uint32_t _has_storage_[2]; + GCFSStructuredQuery_UnaryFilter_Operator op; + GCFSStructuredQuery_FieldReference *field; +} GCFSStructuredQuery_UnaryFilter__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "op", + .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_UnaryFilter_Operator_EnumDescriptor, + .number = GCFSStructuredQuery_UnaryFilter_FieldNumber_Op, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_UnaryFilter__storage_, op), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + { + .name = "field", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference), + .number = GCFSStructuredQuery_UnaryFilter_FieldNumber_Field, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_UnaryFilter__storage_, field), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_UnaryFilter class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_UnaryFilter__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "operandType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_UnaryFilter descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_UnaryFilter_FieldNumber_Op]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSStructuredQuery_UnaryFilter_Op_RawValue(GCFSStructuredQuery_UnaryFilter *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_UnaryFilter descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_UnaryFilter_FieldNumber_Op]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +void GCFSStructuredQuery_UnaryFilter_ClearOperandTypeOneOfCase(GCFSStructuredQuery_UnaryFilter *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - Enum GCFSStructuredQuery_UnaryFilter_Operator + +GPBEnumDescriptor *GCFSStructuredQuery_UnaryFilter_Operator_EnumDescriptor(void) { + static GPBEnumDescriptor *descriptor = NULL; + if (!descriptor) { + static const char *valueNames = + "OperatorUnspecified\000IsNan\000IsNull\000"; + static const int32_t values[] = { + GCFSStructuredQuery_UnaryFilter_Operator_OperatorUnspecified, + GCFSStructuredQuery_UnaryFilter_Operator_IsNan, + GCFSStructuredQuery_UnaryFilter_Operator_IsNull, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSStructuredQuery_UnaryFilter_Operator) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:GCFSStructuredQuery_UnaryFilter_Operator_IsValidValue]; + if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) { + [worker release]; + } + } + return descriptor; +} + +BOOL GCFSStructuredQuery_UnaryFilter_Operator_IsValidValue(int32_t value__) { + switch (value__) { + case GCFSStructuredQuery_UnaryFilter_Operator_OperatorUnspecified: + case GCFSStructuredQuery_UnaryFilter_Operator_IsNan: + case GCFSStructuredQuery_UnaryFilter_Operator_IsNull: + return YES; + default: + return NO; + } +} + +#pragma mark - GCFSStructuredQuery_Order + +@implementation GCFSStructuredQuery_Order + +@dynamic hasField, field; +@dynamic direction; + +typedef struct GCFSStructuredQuery_Order__storage_ { + uint32_t _has_storage_[1]; + GCFSStructuredQuery_Direction direction; + GCFSStructuredQuery_FieldReference *field; +} GCFSStructuredQuery_Order__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "field", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference), + .number = GCFSStructuredQuery_Order_FieldNumber_Field, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_Order__storage_, field), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "direction", + .dataTypeSpecific.enumDescFunc = GCFSStructuredQuery_Direction_EnumDescriptor, + .number = GCFSStructuredQuery_Order_FieldNumber_Direction, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_Order__storage_, direction), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_Order class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_Order__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_Order descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_Order_FieldNumber_Direction]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSStructuredQuery_Order_Direction_RawValue(GCFSStructuredQuery_Order *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSStructuredQuery_Order descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSStructuredQuery_Order_FieldNumber_Direction]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +#pragma mark - GCFSStructuredQuery_FieldReference + +@implementation GCFSStructuredQuery_FieldReference + +@dynamic fieldPath; + +typedef struct GCFSStructuredQuery_FieldReference__storage_ { + uint32_t _has_storage_[1]; + NSString *fieldPath; +} GCFSStructuredQuery_FieldReference__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "fieldPath", + .dataTypeSpecific.className = NULL, + .number = GCFSStructuredQuery_FieldReference_FieldNumber_FieldPath, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_FieldReference__storage_, fieldPath), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_FieldReference class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_FieldReference__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSStructuredQuery_Projection + +@implementation GCFSStructuredQuery_Projection + +@dynamic fieldsArray, fieldsArray_Count; + +typedef struct GCFSStructuredQuery_Projection__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *fieldsArray; +} GCFSStructuredQuery_Projection__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "fieldsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSStructuredQuery_FieldReference), + .number = GCFSStructuredQuery_Projection_FieldNumber_FieldsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSStructuredQuery_Projection__storage_, fieldsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSStructuredQuery_Projection class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSStructuredQuery_Projection__storage_) + flags:GPBDescriptorInitializationFlag_None]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSStructuredQuery)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSCursor + +@implementation GCFSCursor + +@dynamic valuesArray, valuesArray_Count; +@dynamic before; + +typedef struct GCFSCursor__storage_ { + uint32_t _has_storage_[1]; + NSMutableArray *valuesArray; +} GCFSCursor__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "valuesArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue), + .number = GCFSCursor_FieldNumber_ValuesArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSCursor__storage_, valuesArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "before", + .dataTypeSpecific.className = NULL, + .number = GCFSCursor_FieldNumber_Before, + .hasIndex = 0, + .offset = 1, // Stored in _has_storage_ to save space. + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBool, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSCursor class] + rootClass:[GCFSQueryRoot class] + file:GCFSQueryRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSCursor__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h new file mode 100644 index 0000000..c3c4498 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h @@ -0,0 +1,432 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/write.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GCFSDocument; +@class GCFSDocumentMask; +@class GCFSDocumentTransform; +@class GCFSDocumentTransform_FieldTransform; +@class GCFSPrecondition; +@class GCFSValue; +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Enum GCFSDocumentTransform_FieldTransform_ServerValue + +/** A value that is calculated by the server. */ +typedef GPB_ENUM(GCFSDocumentTransform_FieldTransform_ServerValue) { + /** + * Value used if any message's field encounters a value that is not defined + * by this enum. The message will also have C functions to get/set the rawValue + * of the field. + **/ + GCFSDocumentTransform_FieldTransform_ServerValue_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue, + /** Unspecified. This value must not be used. */ + GCFSDocumentTransform_FieldTransform_ServerValue_ServerValueUnspecified = 0, + + /** The time at which the server processed the request. */ + GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime = 1, +}; + +GPBEnumDescriptor *GCFSDocumentTransform_FieldTransform_ServerValue_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL GCFSDocumentTransform_FieldTransform_ServerValue_IsValidValue(int32_t value); + +#pragma mark - GCFSWriteRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GCFSWriteRoot : GPBRootObject +@end + +#pragma mark - GCFSWrite + +typedef GPB_ENUM(GCFSWrite_FieldNumber) { + GCFSWrite_FieldNumber_Update = 1, + GCFSWrite_FieldNumber_Delete_p = 2, + GCFSWrite_FieldNumber_UpdateMask = 3, + GCFSWrite_FieldNumber_CurrentDocument = 4, + GCFSWrite_FieldNumber_Verify = 5, + GCFSWrite_FieldNumber_Transform = 6, +}; + +typedef GPB_ENUM(GCFSWrite_Operation_OneOfCase) { + GCFSWrite_Operation_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSWrite_Operation_OneOfCase_Update = 1, + GCFSWrite_Operation_OneOfCase_Delete_p = 2, + GCFSWrite_Operation_OneOfCase_Verify = 5, + GCFSWrite_Operation_OneOfCase_Transform = 6, +}; + +/** + * A write on a document. + **/ +@interface GCFSWrite : GPBMessage + +/** The operation to execute. */ +@property(nonatomic, readonly) GCFSWrite_Operation_OneOfCase operationOneOfCase; + +/** A document to write. */ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *update; + +/** + * A document name to delete. In the format: + * `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *delete_p; + +/** + * The name of a document on which to verify the `current_document` + * precondition. + * This only requires read access to the document. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *verify; + +/** + * Applies a tranformation to a document. + * At most one `transform` per document is allowed in a given request. + * An `update` cannot follow a `transform` on the same document in a given + * request. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentTransform *transform; + +/** + * The fields to update in this write. + * + * This field can be set only when the operation is `update`. + * None of the field paths in the mask may contain a reserved name. + * If the document exists on the server and has fields not referenced in the + * mask, they are left unchanged. + * Fields referenced in the mask, but not present in the input document, are + * deleted from the document on the server. + * The field paths in this mask must not contain a reserved field name. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocumentMask *updateMask; +/** Test to see if @c updateMask has been set. */ +@property(nonatomic, readwrite) BOOL hasUpdateMask; + +/** + * An optional precondition on the document. + * + * The write will fail if this is set and not met by the target document. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSPrecondition *currentDocument; +/** Test to see if @c currentDocument has been set. */ +@property(nonatomic, readwrite) BOOL hasCurrentDocument; + +@end + +/** + * Clears whatever value was set for the oneof 'operation'. + **/ +void GCFSWrite_ClearOperationOneOfCase(GCFSWrite *message); + +#pragma mark - GCFSDocumentTransform + +typedef GPB_ENUM(GCFSDocumentTransform_FieldNumber) { + GCFSDocumentTransform_FieldNumber_Document = 1, + GCFSDocumentTransform_FieldNumber_FieldTransformsArray = 2, +}; + +/** + * A transformation of a document. + **/ +@interface GCFSDocumentTransform : GPBMessage + +/** The name of the document to transform. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *document; + +/** + * The list of transformations to apply to the fields of the document, in + * order. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *fieldTransformsArray; +/** The number of items in @c fieldTransformsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger fieldTransformsArray_Count; + +@end + +#pragma mark - GCFSDocumentTransform_FieldTransform + +typedef GPB_ENUM(GCFSDocumentTransform_FieldTransform_FieldNumber) { + GCFSDocumentTransform_FieldTransform_FieldNumber_FieldPath = 1, + GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue = 2, +}; + +typedef GPB_ENUM(GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase) { + GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_GPBUnsetOneOfCase = 0, + GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase_SetToServerValue = 2, +}; + +/** + * A transformation of a field of the document. + **/ +@interface GCFSDocumentTransform_FieldTransform : GPBMessage + +/** + * The path of the field. See [Document.fields][google.firestore.v1beta1.Document.fields] for the field path syntax + * reference. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *fieldPath; + +/** The transformation to apply on the field. */ +@property(nonatomic, readonly) GCFSDocumentTransform_FieldTransform_TransformType_OneOfCase transformTypeOneOfCase; + +/** Sets the field to the given server value. */ +@property(nonatomic, readwrite) GCFSDocumentTransform_FieldTransform_ServerValue setToServerValue; + +@end + +/** + * Fetches the raw value of a @c GCFSDocumentTransform_FieldTransform's @c setToServerValue property, even + * if the value was not defined by the enum at the time the code was generated. + **/ +int32_t GCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message); +/** + * Sets the raw value of an @c GCFSDocumentTransform_FieldTransform's @c setToServerValue property, allowing + * it to be set to a value that was not defined by the enum at the time the code + * was generated. + **/ +void SetGCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message, int32_t value); + +/** + * Clears whatever value was set for the oneof 'transformType'. + **/ +void GCFSDocumentTransform_FieldTransform_ClearTransformTypeOneOfCase(GCFSDocumentTransform_FieldTransform *message); + +#pragma mark - GCFSWriteResult + +typedef GPB_ENUM(GCFSWriteResult_FieldNumber) { + GCFSWriteResult_FieldNumber_UpdateTime = 1, + GCFSWriteResult_FieldNumber_TransformResultsArray = 2, +}; + +/** + * The result of applying a write. + **/ +@interface GCFSWriteResult : GPBMessage + +/** + * The last update time of the document after applying the write. Not set + * after a `delete`. + * + * If the write did not actually change the document, this will be the + * previous update_time. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *updateTime; +/** Test to see if @c updateTime has been set. */ +@property(nonatomic, readwrite) BOOL hasUpdateTime; + +/** + * The results of applying each [DocumentTransform.FieldTransform][google.firestore.v1beta1.DocumentTransform.FieldTransform], in the + * same order. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *transformResultsArray; +/** The number of items in @c transformResultsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger transformResultsArray_Count; + +@end + +#pragma mark - GCFSDocumentChange + +typedef GPB_ENUM(GCFSDocumentChange_FieldNumber) { + GCFSDocumentChange_FieldNumber_Document = 1, + GCFSDocumentChange_FieldNumber_TargetIdsArray = 5, + GCFSDocumentChange_FieldNumber_RemovedTargetIdsArray = 6, +}; + +/** + * A [Document][google.firestore.v1beta1.Document] has changed. + * + * May be the result of multiple [writes][google.firestore.v1beta1.Write], including deletes, that + * ultimately resulted in a new value for the [Document][google.firestore.v1beta1.Document]. + * + * Multiple [DocumentChange][google.firestore.v1beta1.DocumentChange] messages may be returned for the same logical + * change, if multiple targets are affected. + **/ +@interface GCFSDocumentChange : GPBMessage + +/** + * The new state of the [Document][google.firestore.v1beta1.Document]. + * + * If `mask` is set, contains only fields that were updated or added. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GCFSDocument *document; +/** Test to see if @c document has been set. */ +@property(nonatomic, readwrite) BOOL hasDocument; + +/** A set of target IDs of targets that match this document. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *targetIdsArray; +/** The number of items in @c targetIdsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger targetIdsArray_Count; + +/** A set of target IDs for targets that no longer match this document. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *removedTargetIdsArray; +/** The number of items in @c removedTargetIdsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger removedTargetIdsArray_Count; + +@end + +#pragma mark - GCFSDocumentDelete + +typedef GPB_ENUM(GCFSDocumentDelete_FieldNumber) { + GCFSDocumentDelete_FieldNumber_Document = 1, + GCFSDocumentDelete_FieldNumber_ReadTime = 4, + GCFSDocumentDelete_FieldNumber_RemovedTargetIdsArray = 6, +}; + +/** + * A [Document][google.firestore.v1beta1.Document] has been deleted. + * + * May be the result of multiple [writes][google.firestore.v1beta1.Write], including updates, the + * last of which deleted the [Document][google.firestore.v1beta1.Document]. + * + * Multiple [DocumentDelete][google.firestore.v1beta1.DocumentDelete] messages may be returned for the same logical + * delete, if multiple targets are affected. + **/ +@interface GCFSDocumentDelete : GPBMessage + +/** The resource name of the [Document][google.firestore.v1beta1.Document] that was deleted. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *document; + +/** A set of target IDs for targets that previously matched this entity. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *removedTargetIdsArray; +/** The number of items in @c removedTargetIdsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger removedTargetIdsArray_Count; + +/** + * The read timestamp at which the delete was observed. + * + * Greater or equal to the `commit_time` of the delete. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; +/** Test to see if @c readTime has been set. */ +@property(nonatomic, readwrite) BOOL hasReadTime; + +@end + +#pragma mark - GCFSDocumentRemove + +typedef GPB_ENUM(GCFSDocumentRemove_FieldNumber) { + GCFSDocumentRemove_FieldNumber_Document = 1, + GCFSDocumentRemove_FieldNumber_RemovedTargetIdsArray = 2, + GCFSDocumentRemove_FieldNumber_ReadTime = 4, +}; + +/** + * A [Document][google.firestore.v1beta1.Document] has been removed from the view of the targets. + * + * Sent if the document is no longer relevant to a target and is out of view. + * Can be sent instead of a DocumentDelete or a DocumentChange if the server + * can not send the new value of the document. + * + * Multiple [DocumentRemove][google.firestore.v1beta1.DocumentRemove] messages may be returned for the same logical + * write or delete, if multiple targets are affected. + **/ +@interface GCFSDocumentRemove : GPBMessage + +/** The resource name of the [Document][google.firestore.v1beta1.Document] that has gone out of view. */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *document; + +/** A set of target IDs for targets that previously matched this document. */ +@property(nonatomic, readwrite, strong, null_resettable) GPBInt32Array *removedTargetIdsArray; +/** The number of items in @c removedTargetIdsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger removedTargetIdsArray_Count; + +/** + * The read timestamp at which the remove was observed. + * + * Greater or equal to the `commit_time` of the change/delete/remove. + **/ +@property(nonatomic, readwrite, strong, null_resettable) GPBTimestamp *readTime; +/** Test to see if @c readTime has been set. */ +@property(nonatomic, readwrite) BOOL hasReadTime; + +@end + +#pragma mark - GCFSExistenceFilter + +typedef GPB_ENUM(GCFSExistenceFilter_FieldNumber) { + GCFSExistenceFilter_FieldNumber_TargetId = 1, + GCFSExistenceFilter_FieldNumber_Count = 2, +}; + +/** + * A digest of all the documents that match a given target. + **/ +@interface GCFSExistenceFilter : GPBMessage + +/** The target ID to which this filter applies. */ +@property(nonatomic, readwrite) int32_t targetId; + +/** + * The total count of documents that match [target_id][google.firestore.v1beta1.ExistenceFilter.target_id]. + * + * If different from the count of documents in the client that match, the + * client must manually determine which documents no longer match the target. + **/ +@property(nonatomic, readwrite) int32_t count; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m new file mode 100644 index 0000000..e6fd0f4 --- /dev/null +++ b/Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.m @@ -0,0 +1,653 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/firestore/v1beta1/write.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Timestamp.pbobjc.h" +#endif + + #import "Write.pbobjc.h" + #import "Annotations.pbobjc.h" + #import "Common.pbobjc.h" + #import "Document.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +#pragma mark - GCFSWriteRoot + +@implementation GCFSWriteRoot + + +@end + +#pragma mark - GCFSWriteRoot_FileDescriptor + +static GPBFileDescriptor *GCFSWriteRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.firestore.v1beta1" + objcPrefix:@"GCFS" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GCFSWrite + +@implementation GCFSWrite + +@dynamic operationOneOfCase; +@dynamic update; +@dynamic delete_p; +@dynamic verify; +@dynamic transform; +@dynamic hasUpdateMask, updateMask; +@dynamic hasCurrentDocument, currentDocument; + +typedef struct GCFSWrite__storage_ { + uint32_t _has_storage_[2]; + GCFSDocument *update; + NSString *delete_p; + GCFSDocumentMask *updateMask; + GCFSPrecondition *currentDocument; + NSString *verify; + GCFSDocumentTransform *transform; +} GCFSWrite__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "update", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSWrite_FieldNumber_Update, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSWrite__storage_, update), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "delete_p", + .dataTypeSpecific.className = NULL, + .number = GCFSWrite_FieldNumber_Delete_p, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSWrite__storage_, delete_p), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "updateMask", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentMask), + .number = GCFSWrite_FieldNumber_UpdateMask, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSWrite__storage_, updateMask), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "currentDocument", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSPrecondition), + .number = GCFSWrite_FieldNumber_CurrentDocument, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSWrite__storage_, currentDocument), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "verify", + .dataTypeSpecific.className = NULL, + .number = GCFSWrite_FieldNumber_Verify, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSWrite__storage_, verify), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "transform", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentTransform), + .number = GCFSWrite_FieldNumber_Transform, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSWrite__storage_, transform), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSWrite class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSWrite__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "operation", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +void GCFSWrite_ClearOperationOneOfCase(GCFSWrite *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - GCFSDocumentTransform + +@implementation GCFSDocumentTransform + +@dynamic document; +@dynamic fieldTransformsArray, fieldTransformsArray_Count; + +typedef struct GCFSDocumentTransform__storage_ { + uint32_t _has_storage_[1]; + NSString *document; + NSMutableArray *fieldTransformsArray; +} GCFSDocumentTransform__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "document", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentTransform_FieldNumber_Document, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDocumentTransform__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "fieldTransformsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocumentTransform_FieldTransform), + .number = GCFSDocumentTransform_FieldNumber_FieldTransformsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocumentTransform__storage_, fieldTransformsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocumentTransform class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocumentTransform__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSDocumentTransform_FieldTransform + +@implementation GCFSDocumentTransform_FieldTransform + +@dynamic transformTypeOneOfCase; +@dynamic fieldPath; +@dynamic setToServerValue; + +typedef struct GCFSDocumentTransform_FieldTransform__storage_ { + uint32_t _has_storage_[2]; + GCFSDocumentTransform_FieldTransform_ServerValue setToServerValue; + NSString *fieldPath; +} GCFSDocumentTransform_FieldTransform__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "fieldPath", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentTransform_FieldTransform_FieldNumber_FieldPath, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDocumentTransform_FieldTransform__storage_, fieldPath), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "setToServerValue", + .dataTypeSpecific.enumDescFunc = GCFSDocumentTransform_FieldTransform_ServerValue_EnumDescriptor, + .number = GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GCFSDocumentTransform_FieldTransform__storage_, setToServerValue), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocumentTransform_FieldTransform class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocumentTransform_FieldTransform__storage_) + flags:GPBDescriptorInitializationFlag_None]; + static const char *oneofs[] = { + "transformType", + }; + [localDescriptor setupOneofs:oneofs + count:(uint32_t)(sizeof(oneofs) / sizeof(char*)) + firstHasIndex:-1]; + [localDescriptor setupContainingMessageClassName:GPBStringifySymbol(GCFSDocumentTransform)]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +int32_t GCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message) { + GPBDescriptor *descriptor = [GCFSDocumentTransform_FieldTransform descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue]; + return GPBGetMessageInt32Field(message, field); +} + +void SetGCFSDocumentTransform_FieldTransform_SetToServerValue_RawValue(GCFSDocumentTransform_FieldTransform *message, int32_t value) { + GPBDescriptor *descriptor = [GCFSDocumentTransform_FieldTransform descriptor]; + GPBFieldDescriptor *field = [descriptor fieldWithNumber:GCFSDocumentTransform_FieldTransform_FieldNumber_SetToServerValue]; + GPBSetInt32IvarWithFieldInternal(message, field, value, descriptor.file.syntax); +} + +void GCFSDocumentTransform_FieldTransform_ClearTransformTypeOneOfCase(GCFSDocumentTransform_FieldTransform *message) { + GPBDescriptor *descriptor = [message descriptor]; + GPBOneofDescriptor *oneof = [descriptor.oneofs objectAtIndex:0]; + GPBMaybeClearOneof(message, oneof, -1, 0); +} +#pragma mark - Enum GCFSDocumentTransform_FieldTransform_ServerValue + +GPBEnumDescriptor *GCFSDocumentTransform_FieldTransform_ServerValue_EnumDescriptor(void) { + static GPBEnumDescriptor *descriptor = NULL; + if (!descriptor) { + static const char *valueNames = + "ServerValueUnspecified\000RequestTime\000"; + static const int32_t values[] = { + GCFSDocumentTransform_FieldTransform_ServerValue_ServerValueUnspecified, + GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(GCFSDocumentTransform_FieldTransform_ServerValue) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:GCFSDocumentTransform_FieldTransform_ServerValue_IsValidValue]; + if (!OSAtomicCompareAndSwapPtrBarrier(nil, worker, (void * volatile *)&descriptor)) { + [worker release]; + } + } + return descriptor; +} + +BOOL GCFSDocumentTransform_FieldTransform_ServerValue_IsValidValue(int32_t value__) { + switch (value__) { + case GCFSDocumentTransform_FieldTransform_ServerValue_ServerValueUnspecified: + case GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime: + return YES; + default: + return NO; + } +} + +#pragma mark - GCFSWriteResult + +@implementation GCFSWriteResult + +@dynamic hasUpdateTime, updateTime; +@dynamic transformResultsArray, transformResultsArray_Count; + +typedef struct GCFSWriteResult__storage_ { + uint32_t _has_storage_[1]; + GPBTimestamp *updateTime; + NSMutableArray *transformResultsArray; +} GCFSWriteResult__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "updateTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSWriteResult_FieldNumber_UpdateTime, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSWriteResult__storage_, updateTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "transformResultsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSValue), + .number = GCFSWriteResult_FieldNumber_TransformResultsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSWriteResult__storage_, transformResultsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSWriteResult class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSWriteResult__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSDocumentChange + +@implementation GCFSDocumentChange + +@dynamic hasDocument, document; +@dynamic targetIdsArray, targetIdsArray_Count; +@dynamic removedTargetIdsArray, removedTargetIdsArray_Count; + +typedef struct GCFSDocumentChange__storage_ { + uint32_t _has_storage_[1]; + GCFSDocument *document; + GPBInt32Array *targetIdsArray; + GPBInt32Array *removedTargetIdsArray; +} GCFSDocumentChange__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "document", + .dataTypeSpecific.className = GPBStringifySymbol(GCFSDocument), + .number = GCFSDocumentChange_FieldNumber_Document, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDocumentChange__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "targetIdsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentChange_FieldNumber_TargetIdsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocumentChange__storage_, targetIdsArray), + .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked), + .dataType = GPBDataTypeInt32, + }, + { + .name = "removedTargetIdsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentChange_FieldNumber_RemovedTargetIdsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocumentChange__storage_, removedTargetIdsArray), + .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked), + .dataType = GPBDataTypeInt32, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocumentChange class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocumentChange__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSDocumentDelete + +@implementation GCFSDocumentDelete + +@dynamic document; +@dynamic removedTargetIdsArray, removedTargetIdsArray_Count; +@dynamic hasReadTime, readTime; + +typedef struct GCFSDocumentDelete__storage_ { + uint32_t _has_storage_[1]; + NSString *document; + GPBTimestamp *readTime; + GPBInt32Array *removedTargetIdsArray; +} GCFSDocumentDelete__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "document", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentDelete_FieldNumber_Document, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDocumentDelete__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSDocumentDelete_FieldNumber_ReadTime, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSDocumentDelete__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "removedTargetIdsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentDelete_FieldNumber_RemovedTargetIdsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocumentDelete__storage_, removedTargetIdsArray), + .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked), + .dataType = GPBDataTypeInt32, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocumentDelete class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocumentDelete__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSDocumentRemove + +@implementation GCFSDocumentRemove + +@dynamic document; +@dynamic removedTargetIdsArray, removedTargetIdsArray_Count; +@dynamic hasReadTime, readTime; + +typedef struct GCFSDocumentRemove__storage_ { + uint32_t _has_storage_[1]; + NSString *document; + GPBInt32Array *removedTargetIdsArray; + GPBTimestamp *readTime; +} GCFSDocumentRemove__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "document", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentRemove_FieldNumber_Document, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSDocumentRemove__storage_, document), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "removedTargetIdsArray", + .dataTypeSpecific.className = NULL, + .number = GCFSDocumentRemove_FieldNumber_RemovedTargetIdsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(GCFSDocumentRemove__storage_, removedTargetIdsArray), + .flags = (GPBFieldFlags)(GPBFieldRepeated | GPBFieldPacked), + .dataType = GPBDataTypeInt32, + }, + { + .name = "readTime", + .dataTypeSpecific.className = GPBStringifySymbol(GPBTimestamp), + .number = GCFSDocumentRemove_FieldNumber_ReadTime, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSDocumentRemove__storage_, readTime), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSDocumentRemove class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSDocumentRemove__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GCFSExistenceFilter + +@implementation GCFSExistenceFilter + +@dynamic targetId; +@dynamic count; + +typedef struct GCFSExistenceFilter__storage_ { + uint32_t _has_storage_[1]; + int32_t targetId; + int32_t count; +} GCFSExistenceFilter__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "targetId", + .dataTypeSpecific.className = NULL, + .number = GCFSExistenceFilter_FieldNumber_TargetId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GCFSExistenceFilter__storage_, targetId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "count", + .dataTypeSpecific.className = NULL, + .number = GCFSExistenceFilter_FieldNumber_Count, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GCFSExistenceFilter__storage_, count), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GCFSExistenceFilter class] + rootClass:[GCFSWriteRoot class] + file:GCFSWriteRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GCFSExistenceFilter__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/rpc/Status.pbobjc.h b/Firestore/Protos/objc/google/rpc/Status.pbobjc.h new file mode 100644 index 0000000..86c21c7 --- /dev/null +++ b/Firestore/Protos/objc/google/rpc/Status.pbobjc.h @@ -0,0 +1,155 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/rpc/status.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class GPBAny; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - RPCStatusRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface RPCStatusRoot : GPBRootObject +@end + +#pragma mark - RPCStatus + +typedef GPB_ENUM(RPCStatus_FieldNumber) { + RPCStatus_FieldNumber_Code = 1, + RPCStatus_FieldNumber_Message = 2, + RPCStatus_FieldNumber_DetailsArray = 3, +}; + +/** + * The `Status` type defines a logical error model that is suitable for different + * programming environments, including REST APIs and RPC APIs. It is used by + * [gRPC](https://github.com/grpc). The error model is designed to be: + * + * - Simple to use and understand for most users + * - Flexible enough to meet unexpected needs + * + * # Overview + * + * The `Status` message contains three pieces of data: error code, error message, + * and error details. The error code should be an enum value of + * [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The + * error message should be a developer-facing English message that helps + * developers *understand* and *resolve* the error. If a localized user-facing + * error message is needed, put the localized message in the error details or + * localize it in the client. The optional error details may contain arbitrary + * information about the error. There is a predefined set of error detail types + * in the package `google.rpc` that can be used for common error conditions. + * + * # Language mapping + * + * The `Status` message is the logical representation of the error model, but it + * is not necessarily the actual wire format. When the `Status` message is + * exposed in different client libraries and different wire protocols, it can be + * mapped differently. For example, it will likely be mapped to some exceptions + * in Java, but more likely mapped to some error codes in C. + * + * # Other uses + * + * The error model and the `Status` message can be used in a variety of + * environments, either with or without APIs, to provide a + * consistent developer experience across different environments. + * + * Example uses of this error model include: + * + * - Partial errors. If a service needs to return partial errors to the client, + * it may embed the `Status` in the normal response to indicate the partial + * errors. + * + * - Workflow errors. A typical workflow has multiple steps. Each step may + * have a `Status` message for error reporting. + * + * - Batch operations. If a client uses batch request and batch response, the + * `Status` message should be used directly inside batch response, one for + * each error sub-response. + * + * - Asynchronous operations. If an API call embeds asynchronous operation + * results in its response, the status of those operations should be + * represented directly using the `Status` message. + * + * - Logging. If some API errors are stored in logs, the message `Status` could + * be used directly after any stripping needed for security/privacy reasons. + **/ +@interface RPCStatus : GPBMessage + +/** The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. */ +@property(nonatomic, readwrite) int32_t code; + +/** + * A developer-facing error message, which should be in English. Any + * user-facing error message should be localized and sent in the + * [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + **/ +@property(nonatomic, readwrite, copy, null_resettable) NSString *message; + +/** + * A list of messages that carry the error details. There is a common set of + * message types for APIs to use. + **/ +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *detailsArray; +/** The number of items in @c detailsArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger detailsArray_Count; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/rpc/Status.pbobjc.m b/Firestore/Protos/objc/google/rpc/Status.pbobjc.m new file mode 100644 index 0000000..831073c --- /dev/null +++ b/Firestore/Protos/objc/google/rpc/Status.pbobjc.m @@ -0,0 +1,136 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/rpc/status.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "Any.pbobjc.h" +#endif + + #import "Status.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#pragma mark - RPCStatusRoot + +@implementation RPCStatusRoot + +// No extensions in the file and none of the imports (direct or indirect) +// defined extensions, so no need to generate +extensionRegistry. + +@end + +#pragma mark - RPCStatusRoot_FileDescriptor + +static GPBFileDescriptor *RPCStatusRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.rpc" + objcPrefix:@"RPC" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - RPCStatus + +@implementation RPCStatus + +@dynamic code; +@dynamic message; +@dynamic detailsArray, detailsArray_Count; + +typedef struct RPCStatus__storage_ { + uint32_t _has_storage_[1]; + int32_t code; + NSString *message; + NSMutableArray *detailsArray; +} RPCStatus__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "code", + .dataTypeSpecific.className = NULL, + .number = RPCStatus_FieldNumber_Code, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RPCStatus__storage_, code), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "message", + .dataTypeSpecific.className = NULL, + .number = RPCStatus_FieldNumber_Message, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RPCStatus__storage_, message), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "detailsArray", + .dataTypeSpecific.className = GPBStringifySymbol(GPBAny), + .number = RPCStatus_FieldNumber_DetailsArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RPCStatus__storage_, detailsArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RPCStatus class] + rootClass:[RPCStatusRoot class] + file:RPCStatusRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RPCStatus__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/type/Latlng.pbobjc.h b/Firestore/Protos/objc/google/type/Latlng.pbobjc.h new file mode 100644 index 0000000..0af137f --- /dev/null +++ b/Firestore/Protos/objc/google/type/Latlng.pbobjc.h @@ -0,0 +1,127 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/type/latlng.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - GTPLatlngRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface GTPLatlngRoot : GPBRootObject +@end + +#pragma mark - GTPLatLng + +typedef GPB_ENUM(GTPLatLng_FieldNumber) { + GTPLatLng_FieldNumber_Latitude = 1, + GTPLatLng_FieldNumber_Longitude = 2, +}; + +/** + * An object representing a latitude/longitude pair. This is expressed as a pair + * of doubles representing degrees latitude and degrees longitude. Unless + * specified otherwise, this must conform to the + * WGS84 + * standard. Values must be within normalized ranges. + * + * Example of normalization code in Python: + * + * def NormalizeLongitude(longitude): + * """Wraps decimal degrees longitude to [-180.0, 180.0].""" + * q, r = divmod(longitude, 360.0) + * if r > 180.0 or (r == 180.0 and q <= -1.0): + * return r - 360.0 + * return r + * + * def NormalizeLatLng(latitude, longitude): + * """Wraps decimal degrees latitude and longitude to + * [-90.0, 90.0] and [-180.0, 180.0], respectively.""" + * r = latitude % 360.0 + * if r <= 90.0: + * return r, NormalizeLongitude(longitude) + * elif r >= 270.0: + * return r - 360, NormalizeLongitude(longitude) + * else: + * return 180 - r, NormalizeLongitude(longitude + 180.0) + * + * assert 180.0 == NormalizeLongitude(180.0) + * assert -180.0 == NormalizeLongitude(-180.0) + * assert -179.0 == NormalizeLongitude(181.0) + * assert (0.0, 0.0) == NormalizeLatLng(360.0, 0.0) + * assert (0.0, 0.0) == NormalizeLatLng(-360.0, 0.0) + * assert (85.0, 180.0) == NormalizeLatLng(95.0, 0.0) + * assert (-85.0, -170.0) == NormalizeLatLng(-95.0, 10.0) + * assert (90.0, 10.0) == NormalizeLatLng(90.0, 10.0) + * assert (-90.0, -10.0) == NormalizeLatLng(-90.0, -10.0) + * assert (0.0, -170.0) == NormalizeLatLng(-180.0, 10.0) + * assert (0.0, -170.0) == NormalizeLatLng(180.0, 10.0) + * assert (-90.0, 10.0) == NormalizeLatLng(270.0, 10.0) + * assert (90.0, 10.0) == NormalizeLatLng(-270.0, 10.0) + **/ +@interface GTPLatLng : GPBMessage + +/** The latitude in degrees. It must be in the range [-90.0, +90.0]. */ +@property(nonatomic, readwrite) double latitude; + +/** The longitude in degrees. It must be in the range [-180.0, +180.0]. */ +@property(nonatomic, readwrite) double longitude; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/objc/google/type/Latlng.pbobjc.m b/Firestore/Protos/objc/google/type/Latlng.pbobjc.m new file mode 100644 index 0000000..9bb37ab --- /dev/null +++ b/Firestore/Protos/objc/google/type/Latlng.pbobjc.m @@ -0,0 +1,119 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: google/type/latlng.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + + #import "Latlng.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#pragma mark - GTPLatlngRoot + +@implementation GTPLatlngRoot + +// No extensions in the file and no imports, so no need to generate +// +extensionRegistry. + +@end + +#pragma mark - GTPLatlngRoot_FileDescriptor + +static GPBFileDescriptor *GTPLatlngRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"google.type" + objcPrefix:@"GTP" + syntax:GPBFileSyntaxProto3]; + } + return descriptor; +} + +#pragma mark - GTPLatLng + +@implementation GTPLatLng + +@dynamic latitude; +@dynamic longitude; + +typedef struct GTPLatLng__storage_ { + uint32_t _has_storage_[1]; + double latitude; + double longitude; +} GTPLatLng__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "latitude", + .dataTypeSpecific.className = NULL, + .number = GTPLatLng_FieldNumber_Latitude, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GTPLatLng__storage_, latitude), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeDouble, + }, + { + .name = "longitude", + .dataTypeSpecific.className = NULL, + .number = GTPLatLng_FieldNumber_Longitude, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GTPLatLng__storage_, longitude), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeDouble, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GTPLatLng class] + rootClass:[GTPLatlngRoot class] + file:GTPLatlngRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GTPLatLng__storage_) + flags:GPBDescriptorInitializationFlag_None]; + NSAssert(descriptor == nil, @"Startup recursed!"); + descriptor = localDescriptor; + } + return descriptor; +} + +@end + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/Firestore/Protos/protos/firestore/local/maybe_document.proto b/Firestore/Protos/protos/firestore/local/maybe_document.proto new file mode 100644 index 0000000..67d2f68 --- /dev/null +++ b/Firestore/Protos/protos/firestore/local/maybe_document.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package firestore.client; + +option java_multiple_files = true; +option java_package = "com.google.firebase.firestore.proto"; + +option objc_class_prefix = "FSTPB"; + +import "google/firestore/v1beta1/document.proto"; +import "google/protobuf/timestamp.proto"; + +// A message indicating that the document is known to not exist. +message NoDocument { + // The name of the document that does not exist, in the standard format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}` + string name = 1; + + // The time at which we observed that it does not exist. + google.protobuf.Timestamp read_time = 2; +} + +// Represents either an existing document or the explicitly known absence of a +// document. +message MaybeDocument { + oneof document_type { + // Used if the document is known to not exist. + NoDocument no_document = 1; + + // The document (if it exists). + google.firestore.v1beta1.Document document = 2; + } +} diff --git a/Firestore/Protos/protos/firestore/local/mutation.proto b/Firestore/Protos/protos/firestore/local/mutation.proto new file mode 100644 index 0000000..6087f87 --- /dev/null +++ b/Firestore/Protos/protos/firestore/local/mutation.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +import "google/firestore/v1beta1/write.proto"; +import "google/protobuf/timestamp.proto"; + +package firestore.client; + +option java_multiple_files = true; +option java_package = "com.google.firebase.firestore.proto"; + +option objc_class_prefix = "FSTPB"; + +// Each user gets a single queue of WriteBatches to apply to the server. +// MutationQueue tracks the metadata about the queue. +message MutationQueue { + // An identifier for the highest numbered batch that has been acknowledged by + // the server. All WriteBatches in this queue with batch_ids less than or + // equal to this value are considered to have been acknowledged by the + // server. + int32 last_acknowledged_batch_id = 1; + + // A stream token that was previously sent by the server. + // + // See StreamingWriteRequest in datastore.proto for more details about usage. + // + // After sending this token, earlier tokens may not be used anymore so only a + // single stream token is retained. + bytes last_stream_token = 2; +} + +// Message containing a batch of user-level writes intended to be sent to +// the server in a single call. Each user-level batch gets a separate +// WriteBatch with a new batch_id. +message WriteBatch { + // An identifier for this batch, allocated by the mutation queue in a + // monotonically increasing manner. + int32 batch_id = 1; + + // A list of writes to apply. All writes will be applied atomically. + repeated google.firestore.v1beta1.Write writes = 2; + + // The local time at which the write batch was initiated. + google.protobuf.Timestamp local_write_time = 3; +} diff --git a/Firestore/Protos/protos/firestore/local/target.proto b/Firestore/Protos/protos/firestore/local/target.proto new file mode 100644 index 0000000..7f34515 --- /dev/null +++ b/Firestore/Protos/protos/firestore/local/target.proto @@ -0,0 +1,90 @@ +syntax = "proto3"; + +package firestore.client; + +option java_multiple_files = true; +option java_package = "com.google.firebase.firestore.proto"; + +option objc_class_prefix = "FSTPB"; + +import "google/firestore/v1beta1/firestore.proto"; +import "google/protobuf/timestamp.proto"; + +// A Target is a long-lived data structure representing a resumable listen on a +// particular user query. While the query describes what to listen to, the +// Target records data about when the results were last updated and enough +// information to be able to resume listening later. +message Target { + // An auto-generated sequential numeric identifier for the target. This + // serves as the identity of the target, and once assigned never changes. + int32 target_id = 1; + + // The last snapshot version received from the Watch Service for this target. + // + // This is the same value as TargetChange.read_time + google.protobuf.Timestamp snapshot_version = 2; + + // An opaque, server-assigned token that allows watching a query to be + // resumed after disconnecting without retransmitting all the data that + // matches the query. The resume token essentially identifies a point in + // time from which the server should resume sending results. + // + // This is related to the snapshot_version in that the resume_token + // effectively also encodes that value, but the resume_token is opaque and + // sometimes encodes additional information. + // + // A consequence of this is that the resume_token should be used when asking + // the server to reason about where this client is in the watch stream, but + // the client should use the snapshot_version for its own purposes. + // + // This is the same value as TargetChange.resume_token + bytes resume_token = 3; + + // A sequence number representing the last time this query was listened to, + // used for garbage collection purposes. + // + // Conventionally this would be a timestamp value, but device-local clocks + // are unreliable and they must be able to create new listens even while + // disconnected. Instead this should be a monotonically increasing number + // that's incremented on each listen call. + // + // This is different from the target_id since the target_id is an immutable + // identifier assigned to the Target on first use while + // last_listen_sequence_number is updated every time the query is listened + // to. + int64 last_listen_sequence_number = 4; + + // The server-side type of target to listen to. + oneof target_type { + // A target specified by a query. + google.firestore.v1beta1.Target.QueryTarget query = 5; + + // A target specified by a set of document names. + google.firestore.v1beta1.Target.DocumentsTarget documents = 6; + } +} + +// Global state tracked across all Targets, tracked separately to avoid the +// need for extra indexes. +message TargetGlobal { + // The highest numbered target id across all Targets. + // + // See Target.target_id. + int32 highest_target_id = 1; + + // The highest numbered last_listen_sequence_number across all Targets. + // + // See Target.last_listen_sequence_number. + int64 highest_listen_sequence_number = 2; + + // A global snapshot version representing the last consistent snapshot we + // received from the backend. This is monotonically increasing and any + // snapshots received from the backend prior to this version (e.g. for + // targets resumed with a resume_token) should be suppressed (buffered) until + // the backend has caught up to this snapshot_version again. This prevents + // our cache from ever going backwards in time. + // + // This is updated whenever our we get a TargetChange with a read_time and + // empty target_ids. + google.protobuf.Timestamp last_remote_snapshot_version = 3; +} diff --git a/Firestore/Protos/protos/google/api/annotations.proto b/Firestore/Protos/protos/google/api/annotations.proto new file mode 100644 index 0000000..85c361b --- /dev/null +++ b/Firestore/Protos/protos/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/Firestore/Protos/protos/google/api/http.proto b/Firestore/Protos/protos/google/api/http.proto new file mode 100644 index 0000000..5f8538a --- /dev/null +++ b/Firestore/Protos/protos/google/api/http.proto @@ -0,0 +1,291 @@ +// Copyright 2016 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +// Defines the HTTP configuration for a service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; +} + +// `HttpRule` defines the mapping of an RPC method to one or more HTTP +// REST APIs. The mapping determines what portions of the request +// message are populated from the path, query parameters, or body of +// the HTTP request. The mapping is typically specified as an +// `google.api.http` annotation, see "google/api/annotations.proto" +// for details. +// +// The mapping consists of a field specifying the path template and +// method kind. The path template can refer to fields in the request +// message, as in the example below which describes a REST GET +// operation on a resource collection of messages: +// +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// SubMessage sub = 2; // `sub.subfield` is url-mapped +// } +// message Message { +// string text = 1; // content of the resource +// } +// +// The same http annotation can alternatively be expressed inside the +// `GRPC API Configuration` YAML file. +// +// http: +// rules: +// - selector: .Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// This definition enables an automatic, bidrectional mapping of HTTP +// JSON to RPC. Example: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` +// +// In general, not only fields but also field paths can be referenced +// from a path pattern. Fields mapped to the path pattern cannot be +// repeated and must have a primitive (non-message) type. +// +// Any fields in the request message which are not bound by the path +// pattern automatically become (optional) HTTP query +// parameters. Assume the following definition of the request message: +// +// +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // mapped to the URL +// int64 revision = 2; // becomes a parameter +// SubMessage sub = 3; // `sub.subfield` becomes a parameter +// } +// +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` +// +// Note that fields which are mapped to HTTP parameters must have a +// primitive type or a repeated primitive type. Message types are not +// allowed. In the case of a repeated type, the parameter can be +// repeated in the URL, as in `...?param=A¶m=B`. +// +// For HTTP method kinds which allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// put: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | RPC +// -----|----- +// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice of +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// +// This enables the following two alternative HTTP JSON to RPC +// mappings: +// +// HTTP | RPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` +// +// # Rules for HTTP mapping +// +// The rules for mapping HTTP path, query parameters, and body fields +// to the request message are as follows: +// +// 1. The `body` field specifies either `*` or a field path, or is +// omitted. If omitted, it assumes there is no HTTP body. +// 2. Leaf fields (recursive expansion of nested messages in the +// request) can be classified into three types: +// (a) Matched in the URL template. +// (b) Covered by body (if body is `*`, everything except (a) fields; +// else everything under the body field) +// (c) All other fields. +// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +// 4. Any body sent with an HTTP request can contain only (b) fields. +// +// The syntax of the path template is as follows: +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single path segment. It follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion. +// +// The syntax `**` matches zero or more path segments. It follows the semantics +// of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved +// Expansion. NOTE: it must be the last segment in the path except the Verb. +// +// The syntax `LITERAL` matches literal text in the URL path. +// +// The syntax `Variable` matches the entire path as specified by its template; +// this nested template must not contain further variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// NOTE: the field paths in variables and in the `body` must not refer to +// repeated fields or map fields. +// +// Use CustomHttpPattern to specify any HTTP method that is not included in the +// `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for +// a given URL path rule. The wild-card rule is useful for services that provide +// content to Web (HTML) clients. +message HttpRule { + // Selects methods to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Used for listing and getting information about resources. + string get = 2; + + // Used for updating a resource. + string put = 3; + + // Used for creating a resource. + string post = 4; + + // Used for deleting a resource. + string delete = 5; + + // Used for updating a resource. + string patch = 6; + + // Custom pattern is used for defining custom verbs. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP body, or + // `*` for mapping all fields not captured by the path pattern to the HTTP + // body. NOTE: the referred field must not be a repeated field and must be + // present at the top-level of request message type. + string body = 7; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/common.proto b/Firestore/Protos/protos/google/firestore/v1beta1/common.proto new file mode 100644 index 0000000..e624323 --- /dev/null +++ b/Firestore/Protos/protos/google/firestore/v1beta1/common.proto @@ -0,0 +1,82 @@ +// Copyright 2017 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.firestore.v1beta1; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "Google.Cloud.Firestore.V1Beta1"; +option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore"; +option java_multiple_files = true; +option java_outer_classname = "CommonProto"; +option java_package = "com.google.firestore.v1beta1"; +option objc_class_prefix = "GCFS"; + + +// A set of field paths on a document. +// Used to restrict a get or update operation on a document to a subset of its +// fields. +// This is different from standard field masks, as this is always scoped to a +// [Document][google.firestore.v1beta1.Document], and takes in account the dynamic nature of [Value][google.firestore.v1beta1.Value]. +message DocumentMask { + // The list of field paths in the mask. See [Document.fields][google.firestore.v1beta1.Document.fields] for a field + // path syntax reference. + repeated string field_paths = 1; +} + +// A precondition on a document, used for conditional operations. +message Precondition { + // The type of precondition. + oneof condition_type { + // When set to `true`, the target document must exist. + // When set to `false`, the target document must not exist. + bool exists = 1; + + // When set, the target document must exist and have been last updated at + // that time. + google.protobuf.Timestamp update_time = 2; + } +} + +// Options for creating a new transaction. +message TransactionOptions { + // Options for a transaction that can be used to read and write documents. + message ReadWrite { + // An optional transaction to retry. + bytes retry_transaction = 1; + } + + // Options for a transaction that can only be used to read documents. + message ReadOnly { + // The consistency mode for this transaction. If not set, defaults to strong + // consistency. + oneof consistency_selector { + // Reads documents at the given time. + // This may not be older than 60 seconds. + google.protobuf.Timestamp read_time = 2; + } + } + + // The mode of the transaction. + oneof mode { + // The transaction can only be used for read operations. + ReadOnly read_only = 2; + + // The transaction can be used for both read and write operations. + ReadWrite read_write = 3; + } +} diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/document.proto b/Firestore/Protos/protos/google/firestore/v1beta1/document.proto new file mode 100644 index 0000000..cf6001d --- /dev/null +++ b/Firestore/Protos/protos/google/firestore/v1beta1/document.proto @@ -0,0 +1,148 @@ +// Copyright 2017 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.firestore.v1beta1; + +import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/type/latlng.proto"; + +option csharp_namespace = "Google.Cloud.Firestore.V1Beta1"; +option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore"; +option java_multiple_files = true; +option java_outer_classname = "DocumentProto"; +option java_package = "com.google.firestore.v1beta1"; +option objc_class_prefix = "GCFS"; + + +// A Firestore document. +// +// Must not exceed 1 MiB - 4 bytes. +message Document { + // The resource name of the document, for example + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + string name = 1; + + // The document's fields. + // + // The map keys represent field names. + // + // A simple field name contains only characters `a` to `z`, `A` to `Z`, + // `0` to `9`, or `_`, and must not start with `0` to `9` or `_`. For example, + // `foo_bar_17`. + // + // Field names matching the regular expression `__.*__` are reserved. Reserved + // field names are forbidden except in certain documented contexts. The map + // keys, represented as UTF-8, must not exceed 1,500 bytes and cannot be + // empty. + // + // Field paths may be used in other contexts to refer to structured fields + // defined here. For `map_value`, the field path is represented by the simple + // or quoted field names of the containing fields, delimited by `.`. For + // example, the structured field + // `"foo" : { map_value: { "x&y" : { string_value: "hello" }}}` would be + // represented by the field path `foo.x&y`. + // + // Within a field path, a quoted field name starts and ends with `` ` `` and + // may contain any character. Some characters, including `` ` ``, must be + // escaped using a `\`. For example, `` `x&y` `` represents `x&y` and + // `` `bak\`tik` `` represents `` bak`tik ``. + map fields = 2; + + // Output only. The time at which the document was created. + // + // This value increases monotonically when a document is deleted then + // recreated. It can also be compared to values from other documents and + // the `read_time` of a query. + google.protobuf.Timestamp create_time = 3; + + // Output only. The time at which the document was last changed. + // + // This value is initally set to the `create_time` then increases + // monotonically with each change to the document. It can also be + // compared to values from other documents and the `read_time` of a query. + google.protobuf.Timestamp update_time = 4; +} + +// A message that can hold any of the supported value types. +message Value { + // Must have a value set. + oneof value_type { + // A null value. + google.protobuf.NullValue null_value = 11; + + // A boolean value. + bool boolean_value = 1; + + // An integer value. + int64 integer_value = 2; + + // A double value. + double double_value = 3; + + // A timestamp value. + // + // Precise only to microseconds. When stored, any additional precision is + // rounded down. + google.protobuf.Timestamp timestamp_value = 10; + + // A string value. + // + // The string, represented as UTF-8, must not exceed 1 MiB - 89 bytes. + // Only the first 1,500 bytes of the UTF-8 representation are considered by + // queries. + string string_value = 17; + + // A bytes value. + // + // Must not exceed 1 MiB - 89 bytes. + // Only the first 1,500 bytes are considered by queries. + bytes bytes_value = 18; + + // A reference to a document. For example: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + string reference_value = 5; + + // A geo point value representing a point on the surface of Earth. + google.type.LatLng geo_point_value = 8; + + // An array value. + // + // Cannot contain another array value. + ArrayValue array_value = 9; + + // A map value. + MapValue map_value = 6; + } +} + +// An array value. +message ArrayValue { + // Values in the array. + repeated Value values = 1; +} + +// A map value. +message MapValue { + // The map's fields. + // + // The map keys represent field names. Field names matching the regular + // expression `__.*__` are reserved. Reserved field names are forbidden except + // in certain documented contexts. The map keys, represented as UTF-8, must + // not exceed 1,500 bytes and cannot be empty. + map fields = 1; +} diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto b/Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto new file mode 100644 index 0000000..3939caa --- /dev/null +++ b/Firestore/Protos/protos/google/firestore/v1beta1/firestore.proto @@ -0,0 +1,719 @@ +// Copyright 2017 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.firestore.v1beta1; + +import "google/api/annotations.proto"; +import "google/firestore/v1beta1/common.proto"; +import "google/firestore/v1beta1/document.proto"; +import "google/firestore/v1beta1/query.proto"; +import "google/firestore/v1beta1/write.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "google/rpc/status.proto"; + +option csharp_namespace = "Google.Cloud.Firestore.V1Beta1"; +option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore"; +option java_multiple_files = true; +option java_outer_classname = "FirestoreProto"; +option java_package = "com.google.firestore.v1beta1"; +option objc_class_prefix = "GCFS"; + +// Specification of the Firestore API. + + + +// The Cloud Firestore service. +// +// This service exposes several types of comparable timestamps: +// +// * `create_time` - The time at which a document was created. Changes only +// when a document is deleted, then re-created. Increases in a strict +// monotonic fashion. +// * `update_time` - The time at which a document was last updated. Changes +// every time a document is modified. Does not change when a write results +// in no modifications. Increases in a strict monotonic fashion. +// * `read_time` - The time at which a particular state was observed. Used +// to denote a consistent snapshot of the database or the time at which a +// Document was observed to not exist. +// * `commit_time` - The time at which the writes in a transaction were +// committed. Any read with an equal or greater `read_time` is guaranteed +// to see the effects of the transaction. +service Firestore { + // Gets a single document. + rpc GetDocument(GetDocumentRequest) returns (Document) { + option (google.api.http) = { get: "/v1beta1/{name=projects/*/databases/*/documents/*/**}" }; + } + + // Lists documents. + rpc ListDocuments(ListDocumentsRequest) returns (ListDocumentsResponse) { + option (google.api.http) = { get: "/v1beta1/{parent=projects/*/databases/*/documents/*/**}/{collection_id}" }; + } + + // Creates a new document. + rpc CreateDocument(CreateDocumentRequest) returns (Document) { + option (google.api.http) = { post: "/v1beta1/{parent=projects/*/databases/*/documents/**}/{collection_id}" body: "document" }; + } + + // Updates or inserts a document. + rpc UpdateDocument(UpdateDocumentRequest) returns (Document) { + option (google.api.http) = { patch: "/v1beta1/{document.name=projects/*/databases/*/documents/*/**}" body: "document" }; + } + + // Deletes a document. + rpc DeleteDocument(DeleteDocumentRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { delete: "/v1beta1/{name=projects/*/databases/*/documents/*/**}" }; + } + + // Gets multiple documents. + // + // Documents returned by this method are not guaranteed to be returned in the + // same order that they were requested. + rpc BatchGetDocuments(BatchGetDocumentsRequest) returns (stream BatchGetDocumentsResponse) { + option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:batchGet" body: "*" }; + } + + // Starts a new transaction. + rpc BeginTransaction(BeginTransactionRequest) returns (BeginTransactionResponse) { + option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:beginTransaction" body: "*" }; + } + + // Commits a transaction, while optionally updating documents. + rpc Commit(CommitRequest) returns (CommitResponse) { + option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:commit" body: "*" }; + } + + // Rolls back a transaction. + rpc Rollback(RollbackRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:rollback" body: "*" }; + } + + // Runs a query. + rpc RunQuery(RunQueryRequest) returns (stream RunQueryResponse) { + option (google.api.http) = { post: "/v1beta1/{parent=projects/*/databases/*/documents}:runQuery" body: "*" }; + } + + // Streams batches of document updates and deletes, in order. + rpc Write(stream WriteRequest) returns (stream WriteResponse) { + option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:write" body: "*" }; + } + + // Listens to changes. + rpc Listen(stream ListenRequest) returns (stream ListenResponse) { + option (google.api.http) = { post: "/v1beta1/{database=projects/*/databases/*}/documents:listen" body: "*" }; + } + + // Lists all the collection IDs underneath a document. + rpc ListCollectionIds(ListCollectionIdsRequest) returns (ListCollectionIdsResponse) { + option (google.api.http) = { post: "/v1beta1/{parent=projects/*/databases/*/documents}:listCollectionIds" body: "*" }; + } +} + +// The request for [Firestore.GetDocument][google.firestore.v1beta1.Firestore.GetDocument]. +message GetDocumentRequest { + // The resource name of the Document to get. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + string name = 1; + + // The fields to return. If not set, returns all fields. + // + // If the document has a field that is not present in this mask, that field + // will not be returned in the response. + DocumentMask mask = 2; + + // The consistency mode for this transaction. + // If not set, defaults to strong consistency. + oneof consistency_selector { + // Reads the document in a transaction. + bytes transaction = 3; + + // Reads the version of the document at the given time. + // This may not be older than 60 seconds. + google.protobuf.Timestamp read_time = 5; + } +} + +// The request for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments]. +message ListDocumentsRequest { + // The parent resource name. In the format: + // `projects/{project_id}/databases/{database_id}/documents` or + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + // For example: + // `projects/my-project/databases/my-database/documents` or + // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + string parent = 1; + + // The collection ID, relative to `parent`, to list. For example: `chatrooms` + // or `messages`. + string collection_id = 2; + + // The maximum number of documents to return. + int32 page_size = 3; + + // The `next_page_token` value returned from a previous List request, if any. + string page_token = 4; + + // The order to sort results by. For example: `priority desc, name`. + string order_by = 6; + + // The fields to return. If not set, returns all fields. + // + // If a document has a field that is not present in this mask, that field + // will not be returned in the response. + DocumentMask mask = 7; + + // The consistency mode for this transaction. + // If not set, defaults to strong consistency. + oneof consistency_selector { + // Reads documents in a transaction. + bytes transaction = 8; + + // Reads documents as they were at the given time. + // This may not be older than 60 seconds. + google.protobuf.Timestamp read_time = 10; + } + + // If the list should show missing documents. A missing document is a + // document that does not exist but has sub-documents. These documents will + // be returned with a key but will not have fields, [Document.create_time][google.firestore.v1beta1.Document.create_time], + // or [Document.update_time][google.firestore.v1beta1.Document.update_time] set. + // + // Requests with `show_missing` may not specify `where` or + // `order_by`. + bool show_missing = 12; +} + +// The response for [Firestore.ListDocuments][google.firestore.v1beta1.Firestore.ListDocuments]. +message ListDocumentsResponse { + // The Documents found. + repeated Document documents = 1; + + // The next page token. + string next_page_token = 2; +} + +// The request for [Firestore.CreateDocument][google.firestore.v1beta1.Firestore.CreateDocument]. +message CreateDocumentRequest { + // The parent resource. For example: + // `projects/{project_id}/databases/{database_id}/documents` or + // `projects/{project_id}/databases/{database_id}/documents/chatrooms/{chatroom_id}` + string parent = 1; + + // The collection ID, relative to `parent`, to list. For example: `chatrooms`. + string collection_id = 2; + + // The client-assigned document ID to use for this document. + // + // Optional. If not specified, an ID will be assigned by the service. + string document_id = 3; + + // The document to create. `name` must not be set. + Document document = 4; + + // The fields to return. If not set, returns all fields. + // + // If the document has a field that is not present in this mask, that field + // will not be returned in the response. + DocumentMask mask = 5; +} + +// The request for [Firestore.UpdateDocument][google.firestore.v1beta1.Firestore.UpdateDocument]. +message UpdateDocumentRequest { + // The updated document. + // Creates the document if it does not already exist. + Document document = 1; + + // The fields to update. + // None of the field paths in the mask may contain a reserved name. + // + // If the document exists on the server and has fields not referenced in the + // mask, they are left unchanged. + // Fields referenced in the mask, but not present in the input document, are + // deleted from the document on the server. + DocumentMask update_mask = 2; + + // The fields to return. If not set, returns all fields. + // + // If the document has a field that is not present in this mask, that field + // will not be returned in the response. + DocumentMask mask = 3; + + // An optional precondition on the document. + // The request will fail if this is set and not met by the target document. + Precondition current_document = 4; +} + +// The request for [Firestore.DeleteDocument][google.firestore.v1beta1.Firestore.DeleteDocument]. +message DeleteDocumentRequest { + // The resource name of the Document to delete. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + string name = 1; + + // An optional precondition on the document. + // The request will fail if this is set and not met by the target document. + Precondition current_document = 2; +} + +// The request for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments]. +message BatchGetDocumentsRequest { + // The database name. In the format: + // `projects/{project_id}/databases/{database_id}`. + string database = 1; + + // The names of the documents to retrieve. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + // The request will fail if any of the document is not a child resource of the + // given `database`. Duplicate names will be elided. + repeated string documents = 2; + + // The fields to return. If not set, returns all fields. + // + // If a document has a field that is not present in this mask, that field will + // not be returned in the response. + DocumentMask mask = 3; + + // The consistency mode for this transaction. + // If not set, defaults to strong consistency. + oneof consistency_selector { + // Reads documents in a transaction. + bytes transaction = 4; + + // Starts a new transaction and reads the documents. + // Defaults to a read-only transaction. + // The new transaction ID will be returned as the first response in the + // stream. + TransactionOptions new_transaction = 5; + + // Reads documents as they were at the given time. + // This may not be older than 60 seconds. + google.protobuf.Timestamp read_time = 7; + } +} + +// The streamed response for [Firestore.BatchGetDocuments][google.firestore.v1beta1.Firestore.BatchGetDocuments]. +message BatchGetDocumentsResponse { + // A single result. + // This can be empty if the server is just returning a transaction. + oneof result { + // A document that was requested. + Document found = 1; + + // A document name that was requested but does not exist. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + string missing = 2; + } + + // The transaction that was started as part of this request. + // Will only be set in the first response, and only if + // [BatchGetDocumentsRequest.new_transaction][google.firestore.v1beta1.BatchGetDocumentsRequest.new_transaction] was set in the request. + bytes transaction = 3; + + // The time at which the document was read. + // This may be monotically increasing, in this case the previous documents in + // the result stream are guaranteed not to have changed between their + // read_time and this one. + google.protobuf.Timestamp read_time = 4; +} + +// The request for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction]. +message BeginTransactionRequest { + // The database name. In the format: + // `projects/{project_id}/databases/{database_id}`. + string database = 1; + + // The options for the transaction. + // Defaults to a read-write transaction. + TransactionOptions options = 2; +} + +// The response for [Firestore.BeginTransaction][google.firestore.v1beta1.Firestore.BeginTransaction]. +message BeginTransactionResponse { + // The transaction that was started. + bytes transaction = 1; +} + +// The request for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit]. +message CommitRequest { + // The database name. In the format: + // `projects/{project_id}/databases/{database_id}`. + string database = 1; + + // The writes to apply. + // + // Always executed atomically and in order. + repeated Write writes = 2; + + // If non-empty, applies all writes in this transaction, and commits it. + // Otherwise, applies the writes as if they were in their own transaction. + bytes transaction = 3; +} + +// The response for [Firestore.Commit][google.firestore.v1beta1.Firestore.Commit]. +message CommitResponse { + // The result of applying the writes. + // + // This i-th write result corresponds to the i-th write in the + // request. + repeated WriteResult write_results = 1; + + // The time at which the commit occurred. + google.protobuf.Timestamp commit_time = 2; +} + +// The request for [Firestore.Rollback][google.firestore.v1beta1.Firestore.Rollback]. +message RollbackRequest { + // The database name. In the format: + // `projects/{project_id}/databases/{database_id}`. + string database = 1; + + // The transaction to roll back. + bytes transaction = 2; +} + +// The request for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery]. +message RunQueryRequest { + // The parent resource name. In the format: + // `projects/{project_id}/databases/{database_id}/documents` or + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + // For example: + // `projects/my-project/databases/my-database/documents` or + // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + string parent = 1; + + // The query to run. + oneof query_type { + // A structured query. + StructuredQuery structured_query = 2; + } + + // The consistency mode for this transaction. + // If not set, defaults to strong consistency. + oneof consistency_selector { + // Reads documents in a transaction. + bytes transaction = 5; + + // Starts a new transaction and reads the documents. + // Defaults to a read-only transaction. + // The new transaction ID will be returned as the first response in the + // stream. + TransactionOptions new_transaction = 6; + + // Reads documents as they were at the given time. + // This may not be older than 60 seconds. + google.protobuf.Timestamp read_time = 7; + } +} + +// The response for [Firestore.RunQuery][google.firestore.v1beta1.Firestore.RunQuery]. +message RunQueryResponse { + // The transaction that was started as part of this request. + // Can only be set in the first response, and only if + // [RunQueryRequest.new_transaction][google.firestore.v1beta1.RunQueryRequest.new_transaction] was set in the request. + // If set, no other fields will be set in this response. + bytes transaction = 2; + + // A query result. + // Not set when reporting partial progress. + Document document = 1; + + // The time at which the document was read. This may be monotonically + // increasing; in this case, the previous documents in the result stream are + // guaranteed not to have changed between their `read_time` and this one. + // + // If the query returns no results, a response with `read_time` and no + // `document` will be sent, and this represents the time at which the query + // was run. + google.protobuf.Timestamp read_time = 3; + + // The number of results that have been skipped due to an offset between + // the last response and the current response. + int32 skipped_results = 4; +} + +// The request for [Firestore.Write][google.firestore.v1beta1.Firestore.Write]. +// +// The first request creates a stream, or resumes an existing one from a token. +// +// When creating a new stream, the server replies with a response containing +// only an ID and a token, to use in the next request. +// +// When resuming a stream, the server first streams any responses later than the +// given token, then a response containing only an up-to-date token, to use in +// the next request. +message WriteRequest { + // The database name. In the format: + // `projects/{project_id}/databases/{database_id}`. + // This is only required in the first message. + string database = 1; + + // The ID of the write stream to resume. + // This may only be set in the first message. When left empty, a new write + // stream will be created. + string stream_id = 2; + + // The writes to apply. + // + // Always executed atomically and in order. + // This must be empty on the first request. + // This may be empty on the last request. + // This must not be empty on all other requests. + repeated Write writes = 3; + + // A stream token that was previously sent by the server. + // + // The client should set this field to the token from the most recent + // [WriteResponse][google.firestore.v1beta1.WriteResponse] it has received. This acknowledges that the client has + // received responses up to this token. After sending this token, earlier + // tokens may not be used anymore. + // + // The server may close the stream if there are too many unacknowledged + // responses. + // + // Leave this field unset when creating a new stream. To resume a stream at + // a specific point, set this field and the `stream_id` field. + // + // Leave this field unset when creating a new stream. + bytes stream_token = 4; + + // Labels associated with this write request. + map labels = 5; +} + +// The response for [Firestore.Write][google.firestore.v1beta1.Firestore.Write]. +message WriteResponse { + // The ID of the stream. + // Only set on the first message, when a new stream was created. + string stream_id = 1; + + // A token that represents the position of this response in the stream. + // This can be used by a client to resume the stream at this point. + // + // This field is always set. + bytes stream_token = 2; + + // The result of applying the writes. + // + // This i-th write result corresponds to the i-th write in the + // request. + repeated WriteResult write_results = 3; + + // The time at which the commit occurred. + google.protobuf.Timestamp commit_time = 4; +} + +// A request for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen] +message ListenRequest { + // The database name. In the format: + // `projects/{project_id}/databases/{database_id}`. + string database = 1; + + // The supported target changes. + oneof target_change { + // A target to add to this stream. + Target add_target = 2; + + // The ID of a target to remove from this stream. + int32 remove_target = 3; + } + + // Labels associated with this target change. + map labels = 4; +} + +// The response for [Firestore.Listen][google.firestore.v1beta1.Firestore.Listen]. +message ListenResponse { + // The supported responses. + oneof response_type { + // Targets have changed. + TargetChange target_change = 2; + + // A [Document][google.firestore.v1beta1.Document] has changed. + DocumentChange document_change = 3; + + // A [Document][google.firestore.v1beta1.Document] has been deleted. + DocumentDelete document_delete = 4; + + // A [Document][google.firestore.v1beta1.Document] has been removed from a target (because it is no longer + // relevant to that target). + DocumentRemove document_remove = 6; + + // A filter to apply to the set of documents previously returned for the + // given target. + // + // Returned when documents may have been removed from the given target, but + // the exact documents are unknown. + ExistenceFilter filter = 5; + } +} + +// A specification of a set of documents to listen to. +message Target { + // A target specified by a set of documents names. + message DocumentsTarget { + // The names of the documents to retrieve. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + // The request will fail if any of the document is not a child resource of + // the given `database`. Duplicate names will be elided. + repeated string documents = 2; + } + + // A target specified by a query. + message QueryTarget { + // The parent resource name. In the format: + // `projects/{project_id}/databases/{database_id}/documents` or + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + // For example: + // `projects/my-project/databases/my-database/documents` or + // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + string parent = 1; + + // The query to run. + oneof query_type { + // A structured query. + StructuredQuery structured_query = 2; + } + } + + // The type of target to listen to. + oneof target_type { + // A target specified by a query. + QueryTarget query = 2; + + // A target specified by a set of document names. + DocumentsTarget documents = 3; + } + + // When to start listening. + // + // If not specified, all matching Documents are returned before any + // subsequent changes. + oneof resume_type { + // A resume token from a prior [TargetChange][google.firestore.v1beta1.TargetChange] for an identical target. + // + // Using a resume token with a different target is unsupported and may fail. + bytes resume_token = 4; + + // Start listening after a specific `read_time`. + // + // The client must know the state of matching documents at this time. + google.protobuf.Timestamp read_time = 11; + } + + // A client provided target ID. + // + // If not set, the server will assign an ID for the target. + // + // Used for resuming a target without changing IDs. The IDs can either be + // client-assigned or be server-assigned in a previous stream. All targets + // with client provided IDs must be added before adding a target that needs + // a server-assigned id. + int32 target_id = 5; + + // If the target should be removed once it is current and consistent. + bool once = 6; +} + +// Targets being watched have changed. +message TargetChange { + // The type of change. + enum TargetChangeType { + // No change has occurred. Used only to send an updated `resume_token`. + NO_CHANGE = 0; + + // The targets have been added. + ADD = 1; + + // The targets have been removed. + REMOVE = 2; + + // The targets reflect all changes committed before the targets were added + // to the stream. + // + // This will be sent after or with a `read_time` that is greater than or + // equal to the time at which the targets were added. + // + // Listeners can wait for this change if read-after-write semantics + // are desired. + CURRENT = 3; + + // The targets have been reset, and a new initial state for the targets + // will be returned in subsequent changes. + // + // After the initial state is complete, `CURRENT` will be returned even + // if the target was previously indicated to be `CURRENT`. + RESET = 4; + } + + // The type of change that occurred. + TargetChangeType target_change_type = 1; + + // The target IDs of targets that have changed. + // + // If empty, the change applies to all targets. + // + // For `target_change_type=ADD`, the order of the target IDs matches the order + // of the requests to add the targets. This allows clients to unambiguously + // associate server-assigned target IDs with added targets. + // + // For other states, the order of the target IDs is not defined. + repeated int32 target_ids = 2; + + // The error that resulted in this change, if applicable. + google.rpc.Status cause = 3; + + // A token that can be used to resume the stream for the given `target_ids`, + // or all targets if `target_ids` is empty. + // + // Not set on every target change. + bytes resume_token = 4; + + // The consistent `read_time` for the given `target_ids` (omitted when the + // target_ids are not at a consistent snapshot). + // + // The stream is guaranteed to send a `read_time` with `target_ids` empty + // whenever the entire stream reaches a new consistent snapshot. ADD, + // CURRENT, and RESET messages are guaranteed to (eventually) result in a + // new consistent snapshot (while NO_CHANGE and REMOVE messages are not). + // + // For a given stream, `read_time` is guaranteed to be monotonically + // increasing. + google.protobuf.Timestamp read_time = 6; +} + +// The request for [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds]. +message ListCollectionIdsRequest { + // The parent document. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + // For example: + // `projects/my-project/databases/my-database/documents/chatrooms/my-chatroom` + string parent = 1; + + // The maximum number of results to return. + int32 page_size = 2; + + // A page token. Must be a value from + // [ListCollectionIdsResponse][google.firestore.v1beta1.ListCollectionIdsResponse]. + string page_token = 3; +} + +// The response from [Firestore.ListCollectionIds][google.firestore.v1beta1.Firestore.ListCollectionIds]. +message ListCollectionIdsResponse { + // The collection ids. + repeated string collection_ids = 1; + + // A page token that may be used to continue the list. + string next_page_token = 2; +} diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/query.proto b/Firestore/Protos/protos/google/firestore/v1beta1/query.proto new file mode 100644 index 0000000..d19b022 --- /dev/null +++ b/Firestore/Protos/protos/google/firestore/v1beta1/query.proto @@ -0,0 +1,231 @@ +// Copyright 2017 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.firestore.v1beta1; + +import "google/api/annotations.proto"; +import "google/firestore/v1beta1/document.proto"; +import "google/protobuf/wrappers.proto"; + +option csharp_namespace = "Google.Cloud.Firestore.V1Beta1"; +option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore"; +option java_multiple_files = true; +option java_outer_classname = "QueryProto"; +option java_package = "com.google.firestore.v1beta1"; +option objc_class_prefix = "GCFS"; + + +// A Firestore query. +message StructuredQuery { + // A selection of a collection, such as `messages as m1`. + message CollectionSelector { + // The collection ID. + // When set, selects only collections with this ID. + string collection_id = 2; + + // When false, selects only collections that are immediate children of + // the `parent` specified in the containing `RunQueryRequest`. + // When true, selects all descendant collections. + bool all_descendants = 3; + } + + // A filter. + message Filter { + // The type of filter. + oneof filter_type { + // A composite filter. + CompositeFilter composite_filter = 1; + + // A filter on a document field. + FieldFilter field_filter = 2; + + // A filter that takes exactly one argument. + UnaryFilter unary_filter = 3; + } + } + + // A filter that merges multiple other filters using the given operator. + message CompositeFilter { + // A composite filter operator. + enum Operator { + // Unspecified. This value must not be used. + OPERATOR_UNSPECIFIED = 0; + + // The results are required to satisfy each of the combined filters. + AND = 1; + } + + // The operator for combining multiple filters. + Operator op = 1; + + // The list of filters to combine. + // Must contain at least one filter. + repeated Filter filters = 2; + } + + // A filter on a specific field. + message FieldFilter { + // A field filter operator. + enum Operator { + // Unspecified. This value must not be used. + OPERATOR_UNSPECIFIED = 0; + + // Less than. Requires that the field come first in `order_by`. + LESS_THAN = 1; + + // Less than or equal. Requires that the field come first in `order_by`. + LESS_THAN_OR_EQUAL = 2; + + // Greater than. Requires that the field come first in `order_by`. + GREATER_THAN = 3; + + // Greater than or equal. Requires that the field come first in + // `order_by`. + GREATER_THAN_OR_EQUAL = 4; + + // Equal. + EQUAL = 5; + } + + // The field to filter by. + FieldReference field = 1; + + // The operator to filter by. + Operator op = 2; + + // The value to compare to. + Value value = 3; + } + + // A filter with a single operand. + message UnaryFilter { + // A unary operator. + enum Operator { + // Unspecified. This value must not be used. + OPERATOR_UNSPECIFIED = 0; + + // Test if a field is equal to NaN. + IS_NAN = 2; + + // Test if an exprestion evaluates to Null. + IS_NULL = 3; + } + + // The unary operator to apply. + Operator op = 1; + + // The argument to the filter. + oneof operand_type { + // The field to which to apply the operator. + FieldReference field = 2; + } + } + + // An order on a field. + message Order { + // The field to order by. + FieldReference field = 1; + + // The direction to order by. Defaults to `ASCENDING`. + Direction direction = 2; + } + + // A reference to a field, such as `max(messages.time) as max_time`. + message FieldReference { + string field_path = 2; + } + + // The projection of document's fields to return. + message Projection { + // The fields to return. + // + // If empty, all fields are returned. To only return the name + // of the document, use `['__name__']`. + repeated FieldReference fields = 2; + } + + // A sort direction. + enum Direction { + // Unspecified. + DIRECTION_UNSPECIFIED = 0; + + // Ascending. + ASCENDING = 1; + + // Descending. + DESCENDING = 2; + } + + // The projection to return. + Projection select = 1; + + // The collections to query. + repeated CollectionSelector from = 2; + + // The filter to apply. + Filter where = 3; + + // The order to apply to the query results. + // + // Firestore guarantees a stable ordering through the following rules: + // + // * Any field required to appear in `order_by`, that is not already + // specified in `order_by`, is appended to the order in field name order + // by default. + // * If an order on `__name__` is not specified, it is appended by default. + // + // Fields are appended with the same sort direction as the last order + // specified, or 'ASCENDING' if no order was specified. For example: + // + // * `SELECT * FROM Foo ORDER BY A` becomes + // `SELECT * FROM Foo ORDER BY A, __name__` + // * `SELECT * FROM Foo ORDER BY A DESC` becomes + // `SELECT * FROM Foo ORDER BY A DESC, __name__ DESC` + // * `SELECT * FROM Foo WHERE A > 1` becomes + // `SELECT * FROM Foo WHERE A > 1 ORDER BY A, __name__` + repeated Order order_by = 4; + + // A starting point for the query results. + Cursor start_at = 7; + + // A end point for the query results. + Cursor end_at = 8; + + // The number of results to skip. + // + // Applies before limit, but after all other constraints. Must be >= 0 if + // specified. + int32 offset = 6; + + // The maximum number of results to return. + // + // Applies after all other constraints. + // Must be >= 0 if specified. + google.protobuf.Int32Value limit = 5; +} + +// A position in a query result set. +message Cursor { + // The values that represent a position, in the order they appear in + // the order by clause of a query. + // + // Can contain fewer values than specified in the order by clause. + repeated Value values = 1; + + // If the position is just before or just after the given values, relative + // to the sort order defined by the query. + bool before = 2; +} diff --git a/Firestore/Protos/protos/google/firestore/v1beta1/write.proto b/Firestore/Protos/protos/google/firestore/v1beta1/write.proto new file mode 100644 index 0000000..b6e9d5f --- /dev/null +++ b/Firestore/Protos/protos/google/firestore/v1beta1/write.proto @@ -0,0 +1,189 @@ +// Copyright 2017 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.firestore.v1beta1; + +import "google/api/annotations.proto"; +import "google/firestore/v1beta1/common.proto"; +import "google/firestore/v1beta1/document.proto"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "Google.Cloud.Firestore.V1Beta1"; +option go_package = "google.golang.org/genproto/googleapis/firestore/v1beta1;firestore"; +option java_multiple_files = true; +option java_outer_classname = "WriteProto"; +option java_package = "com.google.firestore.v1beta1"; +option objc_class_prefix = "GCFS"; + + +// A write on a document. +message Write { + // The operation to execute. + oneof operation { + // A document to write. + Document update = 1; + + // A document name to delete. In the format: + // `projects/{project_id}/databases/{database_id}/documents/{document_path}`. + string delete = 2; + + // The name of a document on which to verify the `current_document` + // precondition. + // This only requires read access to the document. + string verify = 5; + + // Applies a tranformation to a document. + // At most one `transform` per document is allowed in a given request. + // An `update` cannot follow a `transform` on the same document in a given + // request. + DocumentTransform transform = 6; + } + + // The fields to update in this write. + // + // This field can be set only when the operation is `update`. + // None of the field paths in the mask may contain a reserved name. + // If the document exists on the server and has fields not referenced in the + // mask, they are left unchanged. + // Fields referenced in the mask, but not present in the input document, are + // deleted from the document on the server. + // The field paths in this mask must not contain a reserved field name. + DocumentMask update_mask = 3; + + // An optional precondition on the document. + // + // The write will fail if this is set and not met by the target document. + Precondition current_document = 4; +} + +// A transformation of a document. +message DocumentTransform { + // A transformation of a field of the document. + message FieldTransform { + // A value that is calculated by the server. + enum ServerValue { + // Unspecified. This value must not be used. + SERVER_VALUE_UNSPECIFIED = 0; + + // The time at which the server processed the request. + REQUEST_TIME = 1; + } + + // The path of the field. See [Document.fields][google.firestore.v1beta1.Document.fields] for the field path syntax + // reference. + string field_path = 1; + + // The transformation to apply on the field. + oneof transform_type { + // Sets the field to the given server value. + ServerValue set_to_server_value = 2; + } + } + + // The name of the document to transform. + string document = 1; + + // The list of transformations to apply to the fields of the document, in + // order. + repeated FieldTransform field_transforms = 2; +} + +// The result of applying a write. +message WriteResult { + // The last update time of the document after applying the write. Not set + // after a `delete`. + // + // If the write did not actually change the document, this will be the + // previous update_time. + google.protobuf.Timestamp update_time = 1; + + // The results of applying each [DocumentTransform.FieldTransform][google.firestore.v1beta1.DocumentTransform.FieldTransform], in the + // same order. + repeated Value transform_results = 2; +} + +// A [Document][google.firestore.v1beta1.Document] has changed. +// +// May be the result of multiple [writes][google.firestore.v1beta1.Write], including deletes, that +// ultimately resulted in a new value for the [Document][google.firestore.v1beta1.Document]. +// +// Multiple [DocumentChange][google.firestore.v1beta1.DocumentChange] messages may be returned for the same logical +// change, if multiple targets are affected. +message DocumentChange { + // The new state of the [Document][google.firestore.v1beta1.Document]. + // + // If `mask` is set, contains only fields that were updated or added. + Document document = 1; + + // A set of target IDs of targets that match this document. + repeated int32 target_ids = 5; + + // A set of target IDs for targets that no longer match this document. + repeated int32 removed_target_ids = 6; +} + +// A [Document][google.firestore.v1beta1.Document] has been deleted. +// +// May be the result of multiple [writes][google.firestore.v1beta1.Write], including updates, the +// last of which deleted the [Document][google.firestore.v1beta1.Document]. +// +// Multiple [DocumentDelete][google.firestore.v1beta1.DocumentDelete] messages may be returned for the same logical +// delete, if multiple targets are affected. +message DocumentDelete { + // The resource name of the [Document][google.firestore.v1beta1.Document] that was deleted. + string document = 1; + + // A set of target IDs for targets that previously matched this entity. + repeated int32 removed_target_ids = 6; + + // The read timestamp at which the delete was observed. + // + // Greater or equal to the `commit_time` of the delete. + google.protobuf.Timestamp read_time = 4; +} + +// A [Document][google.firestore.v1beta1.Document] has been removed from the view of the targets. +// +// Sent if the document is no longer relevant to a target and is out of view. +// Can be sent instead of a DocumentDelete or a DocumentChange if the server +// can not send the new value of the document. +// +// Multiple [DocumentRemove][google.firestore.v1beta1.DocumentRemove] messages may be returned for the same logical +// write or delete, if multiple targets are affected. +message DocumentRemove { + // The resource name of the [Document][google.firestore.v1beta1.Document] that has gone out of view. + string document = 1; + + // A set of target IDs for targets that previously matched this document. + repeated int32 removed_target_ids = 2; + + // The read timestamp at which the remove was observed. + // + // Greater or equal to the `commit_time` of the change/delete/remove. + google.protobuf.Timestamp read_time = 4; +} + +// A digest of all the documents that match a given target. +message ExistenceFilter { + // The target ID to which this filter applies. + int32 target_id = 1; + + // The total count of documents that match [target_id][google.firestore.v1beta1.ExistenceFilter.target_id]. + // + // If different from the count of documents in the client that match, the + // client must manually determine which documents no longer match the target. + int32 count = 2; +} diff --git a/Firestore/Protos/protos/google/rpc/status.proto b/Firestore/Protos/protos/google/rpc/status.proto new file mode 100644 index 0000000..0839ee9 --- /dev/null +++ b/Firestore/Protos/protos/google/rpc/status.proto @@ -0,0 +1,92 @@ +// Copyright 2017 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; +option java_multiple_files = true; +option java_outer_classname = "StatusProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + + +// The `Status` type defines a logical error model that is suitable for different +// programming environments, including REST APIs and RPC APIs. It is used by +// [gRPC](https://github.com/grpc). The error model is designed to be: +// +// - Simple to use and understand for most users +// - Flexible enough to meet unexpected needs +// +// # Overview +// +// The `Status` message contains three pieces of data: error code, error message, +// and error details. The error code should be an enum value of +// [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The +// error message should be a developer-facing English message that helps +// developers *understand* and *resolve* the error. If a localized user-facing +// error message is needed, put the localized message in the error details or +// localize it in the client. The optional error details may contain arbitrary +// information about the error. There is a predefined set of error detail types +// in the package `google.rpc` that can be used for common error conditions. +// +// # Language mapping +// +// The `Status` message is the logical representation of the error model, but it +// is not necessarily the actual wire format. When the `Status` message is +// exposed in different client libraries and different wire protocols, it can be +// mapped differently. For example, it will likely be mapped to some exceptions +// in Java, but more likely mapped to some error codes in C. +// +// # Other uses +// +// The error model and the `Status` message can be used in a variety of +// environments, either with or without APIs, to provide a +// consistent developer experience across different environments. +// +// Example uses of this error model include: +// +// - Partial errors. If a service needs to return partial errors to the client, +// it may embed the `Status` in the normal response to indicate the partial +// errors. +// +// - Workflow errors. A typical workflow has multiple steps. Each step may +// have a `Status` message for error reporting. +// +// - Batch operations. If a client uses batch request and batch response, the +// `Status` message should be used directly inside batch response, one for +// each error sub-response. +// +// - Asynchronous operations. If an API call embeds asynchronous operation +// results in its response, the status of those operations should be +// represented directly using the `Status` message. +// +// - Logging. If some API errors are stored in logs, the message `Status` could +// be used directly after any stripping needed for security/privacy reasons. +message Status { + // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + int32 code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + repeated google.protobuf.Any details = 3; +} diff --git a/Firestore/Protos/protos/google/type/latlng.proto b/Firestore/Protos/protos/google/type/latlng.proto new file mode 100644 index 0000000..4e8c65d --- /dev/null +++ b/Firestore/Protos/protos/google/type/latlng.proto @@ -0,0 +1,71 @@ +// Copyright 2016 Google Inc. +// +// 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. + +syntax = "proto3"; + +package google.type; + +option go_package = "google.golang.org/genproto/googleapis/type/latlng;latlng"; +option java_multiple_files = true; +option java_outer_classname = "LatLngProto"; +option java_package = "com.google.type"; +option objc_class_prefix = "GTP"; + + +// An object representing a latitude/longitude pair. This is expressed as a pair +// of doubles representing degrees latitude and degrees longitude. Unless +// specified otherwise, this must conform to the +// WGS84 +// standard. Values must be within normalized ranges. +// +// Example of normalization code in Python: +// +// def NormalizeLongitude(longitude): +// """Wraps decimal degrees longitude to [-180.0, 180.0].""" +// q, r = divmod(longitude, 360.0) +// if r > 180.0 or (r == 180.0 and q <= -1.0): +// return r - 360.0 +// return r +// +// def NormalizeLatLng(latitude, longitude): +// """Wraps decimal degrees latitude and longitude to +// [-90.0, 90.0] and [-180.0, 180.0], respectively.""" +// r = latitude % 360.0 +// if r <= 90.0: +// return r, NormalizeLongitude(longitude) +// elif r >= 270.0: +// return r - 360, NormalizeLongitude(longitude) +// else: +// return 180 - r, NormalizeLongitude(longitude + 180.0) +// +// assert 180.0 == NormalizeLongitude(180.0) +// assert -180.0 == NormalizeLongitude(-180.0) +// assert -179.0 == NormalizeLongitude(181.0) +// assert (0.0, 0.0) == NormalizeLatLng(360.0, 0.0) +// assert (0.0, 0.0) == NormalizeLatLng(-360.0, 0.0) +// assert (85.0, 180.0) == NormalizeLatLng(95.0, 0.0) +// assert (-85.0, -170.0) == NormalizeLatLng(-95.0, 10.0) +// assert (90.0, 10.0) == NormalizeLatLng(90.0, 10.0) +// assert (-90.0, -10.0) == NormalizeLatLng(-90.0, -10.0) +// assert (0.0, -170.0) == NormalizeLatLng(-180.0, 10.0) +// assert (0.0, -170.0) == NormalizeLatLng(180.0, 10.0) +// assert (-90.0, 10.0) == NormalizeLatLng(270.0, 10.0) +// assert (90.0, 10.0) == NormalizeLatLng(-270.0, 10.0) +message LatLng { + // The latitude in degrees. It must be in the range [-90.0, +90.0]. + double latitude = 1; + + // The longitude in degrees. It must be in the range [-180.0, +180.0]. + double longitude = 2; +} diff --git a/Firestore/Protos/strip-registry.py b/Firestore/Protos/strip-registry.py new file mode 100755 index 0000000..cd48784 --- /dev/null +++ b/Firestore/Protos/strip-registry.py @@ -0,0 +1,36 @@ +#!/usr/bin/python + +# 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. + +"""strip-registry.py removes extensionRegistry functions from objc protos. +""" + +import sys + +filename = sys.argv[1] + +with open(filename) as input: + content = [x.strip('\n') for x in input.readlines()] + +if '+ (GPBExtensionRegistry*)extensionRegistry {' in content: + new_content = [] + skip = False + for line in content: + if '+ (GPBExtensionRegistry*)extensionRegistry {' in line: + skip = True + if not skip: + new_content.append(line) + elif line == '}': + skip = False + + with open(filename, "w") as output: + output.write('\n'.join(new_content) + '\n') diff --git a/Firestore/README.md b/Firestore/README.md new file mode 100644 index 0000000..fbff1b1 --- /dev/null +++ b/Firestore/README.md @@ -0,0 +1,15 @@ +## Usage + +``` +$ cd Firestore/Example +$ pod update +$ open Firebase.xcworkspace +Select the FirestoreTests scheme +⌘-u to build and run the unit tests +``` + +### Building Protos + +Typically you should not need to worrying about regenerating the Objective-C +files from the .proto files. If you do, see instructions at +[Protos/README.md](Protos/README.md). 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 *)data { + return [self addDocumentWithData:data completion:nil]; +} + +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary *)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 *)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 *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore { + if (snapshot.oldDocuments.isEmpty) { + // Special case the first snapshot because index calculation is easy and fast + FSTDocument *_Nullable lastDocument = nil; + NSUInteger index = 0; + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + 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 *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 + +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 *)documentData { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; +} + +- (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options { + return [self setData:documentData options:options completion:nil]; +} + +- (void)setData:(NSDictionary *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; +} + +- (void)setData:(NSDictionary *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedSetData:documentData options:options]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] + completion:completion]; +} + +- (void)updateData:(NSDictionary *)fields { + return [self updateData:fields completion:nil]; +} + +- (void)updateData:(NSDictionary *)fields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key + precondition:[FSTPrecondition preconditionWithExists:YES]] + completion:completion]; +} + +- (void)deleteDocument { + return [self deleteDocumentWithCompletion:nil]; +} + +- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + FSTDeleteMutation *mutation = + [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; + return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; +} + +- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + FSTListenOptions *listenOptions = + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id listenerRegistration; + FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + if (!snapshot.exists && snapshot.metadata.fromCache) { + // TODO(dimond): Reconsider how to raise missing documents when offline. + // If we're online and the document doesn't exist then we call the completion with + // a document with document.exists set to false. If we're offline however, we call the + // completion handler with an error. Two options: + // 1) Cache the negative response from the server so we can deliver that even when you're + // offline. + // 2) Actually call the completion handler with an error if the document doesn't exist when + // you are offline. + // TODO(dimond): Use proper error domain + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get document because the client is offline.", + }]); + } else { + completion(snapshot, nil); + } + }; + + listenerRegistration = + [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRDocumentSnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; + FSTDocumentKey *key = self.key; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); + FSTDocument *document = [snapshot.documents documentForKey:key]; + + FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:key + document:document + fromCache:snapshot.fromCache]; + listener(result, nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { + return + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges + includeDocumentMetadataChanges:options.includeMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot+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 *)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 *)convertedObject:(FSTObjectValue *)objectValue { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [objectValue.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { + result[key] = [self convertedValue:value]; + }]; + return result; +} + +- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue { + NSArray *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 *)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)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 +#import +#import + +#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 credentialsProvider; +@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; + +@property(nonatomic, strong) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +@implementation FIRFirestore { + FIRFirestoreSettings *_settings; +} + ++ (NSMutableDictionary *)instances { + static dispatch_once_t token = 0; + static NSMutableDictionary *instances; + dispatch_once(&token, ^{ + instances = [NSMutableDictionary dictionary]; + }); + return instances; +} + ++ (instancetype)firestore { + FIRApp *app = [FIRApp defaultApp]; + if (!app) { + FSTThrowInvalidUsage( + @"FIRAppNotConfiguredException", + @"Failed to get 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 *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 credentialsProvider; + credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; + + NSString *persistenceKey = app.name; + + firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:database + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + instances[key] = firestore; + } + + return firestore; + } +} + +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app { + if (self = [super init]) { + _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { + if ([input isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *documentReference = (FIRDocumentReference *)input; + return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key + databaseID:documentReference.firestore.databaseID]; + } else { + return input; + } + }; + _dataConverter = + [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; + _persistenceKey = persistenceKey; + _credentialsProvider = credentialsProvider; + _workerDispatchQueue = workerDispatchQueue; + _app = app; + _settings = [[FIRFirestoreSettings alloc] init]; + } + return self; +} + +- (FIRFirestoreSettings *)settings { + // 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 + +/** 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:@"", 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 + +- (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 listenerRegistration; + FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + completion(snapshot, nil); + }; + + listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id)addSnapshotListener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRQuerySnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = self.query; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites + fromCache:snapshot.fromCache]; + + listener([FIRQuerySnapshot snapshotWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata], + nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { + return + [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field { + return + [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { + return [self queryOrderedByFieldPath:fieldPath descending:NO]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { + return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] + descending:descending]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { + [self validateNewOrderByPath:fieldPath.internalValue]; + if (self.query.startAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify a starting point before specifying the order by."); + } + if (self.query.endAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify an ending point before specifying the order by."); + } + FSTSortOrder *sortOrder = + [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] + firestore:self.firestore]; +} + +- (FIRQuery *)queryLimitedTo:(NSInteger)limit { + if (limit <= 0) { + FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", + (long)limit); + } + return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; +} + +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +#pragma mark - Private Methods + +/** Private helper for all of the queryWhereField: methods. */ +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + field:(NSString *)field + value:(id)value { + return [self queryWithFilterOperator:filterOperator + path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue + value:value]; +} + +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + path:(FSTFieldPath *)fieldPath + value:(id)value { + FSTFieldValue *fieldValue; + if ([fieldPath isKeyFieldPath]) { + if ([value isKindOfClass:[NSString class]]) { + NSString *documentKey = (NSString *)value; + if ([documentKey containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but '%@' contains a '/' character.", + documentKey); + } else if (documentKey.length == 0) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but it was an empty string."); + } + FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] + databaseID:self.firestore.databaseID]; + } else if ([value isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *ref = (FIRDocumentReference *)value; + fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + } else { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide a " + "valid string or DocumentReference, but it was of type: %@", + NSStringFromClass([value class])); + } + } else { + fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; + } + + id filter; + if ([fieldValue isEqual:[FSTNullValue nullValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on nil / " + "NSNull."); + } + filter = [[FSTNullFilter alloc] initWithField:fieldPath]; + } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on NaN."); + } + filter = [[FSTNanFilter alloc] initWithField:fieldPath]; + } else { + filter = [FSTRelationFilter filterWithField:fieldPath + filterOperator:filterOperator + value:fieldValue]; + [self validateNewRelationFilter:filter]; + } + return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] + firestore:self.firestore]; +} + +- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { + if ([filter isInequality]) { + FSTFieldPath *existingField = [self.query inequalityFilterField]; + if (existingField && ![existingField isEqual:filter.field]) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid Query. All where filters with an inequality " + "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on '%@' and '%@'", + existingField, filter.field); + } + + FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; + if (firstOrderByField) { + [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; + } + } +} + +- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { + if (![self.query firstSortOrderField]) { + // This is the first order by. It must match any inequality. + FSTFieldPath *inequalityField = [self.query inequalityFilterField]; + if (inequalityField) { + [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; + } + } +} + +- (void)validateOrderByField:(FSTFieldPath *)orderByField + matchesInequalityField:(FSTFieldPath *)inequalityField { + if (!([orderByField isEqual:inequalityField])) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You have a where filter with an " + "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " + "and so you must also use '%@' as your first queryOrderedBy field, but your first " + "queryOrderedBy is currently on field '%@' instead.", + inequalityField, inequalityField, orderByField); + } +} + +/** + * Create a FSTBound from a query given the document. + * + * Note that the FSTBound will always include the key of the document and the position will be + * unambiguous. + * + * Will throw if the document does not contain all fields of the order by of the query. + */ +- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { + if (![snapshot exists]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a document " + @"that doesn't exist."); + } + FSTDocument *document = snapshot.internalDocument; + NSMutableArray *components = [NSMutableArray array]; + + // Because people expect to continue/end a query at the exact document provided, we need to + // use the implicit sort order rather than the explicit sort order, because it's guaranteed to + // contain the document key. That way the position becomes unambiguous and the query + // continues/ends exactly at the provided document. Without the key (by using the explicit sort + // orders), multiple documents could match the position, yielding duplicate results. + for (FSTSortOrder *sortOrder in self.query.sortOrders) { + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + [components addObject:[FSTReferenceValue referenceValue:document.key + databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *value = [document fieldForPath:sortOrder.field]; + if (value != nil) { + [components addObject:value]; + } else { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a " + "document for which the field '%@' (used as the order by) " + "does not exist.", + sortOrder.field.canonicalString); + } + } + } + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts a list of field values to an FSTBound. */ +- (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isBefore { + // Use explicit sort order because it has to match the query the user made + NSArray *explicitSortOrders = self.query.explicitSortOrders; + if (fieldValues.count > explicitSortOrders.count) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using more values " + @"than were specified in the order by."); + } + + NSMutableArray *components = [NSMutableArray array]; + [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { + FSTSortOrder *sortOrder = explicitSortOrders[idx]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + if (![rawValue isKindOfClass:[NSString class]]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Expected a string for the document ID."); + } + NSString *documentID = (NSString *)rawValue; + if ([documentID containsString:@"/"]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Document ID '%@' contains a slash.", documentID); + } + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; + [components + addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; + [components addObject:fieldValue]; + } + }]; + + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { + return [[FSTListenOptions alloc] + initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges + includeDocumentMetadataChanges:options.includeDocumentMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot+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 *_documents; + + // Cached value of the documentChanges property. + NSArray *_documentChanges; +} + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + if (self = [super init]) { + _firestore = firestore; + _originalQuery = query; + _snapshot = snapshot; + _metadata = metadata; + } + return self; +} + +@dynamic empty; + +- (FIRQuery *)query { + return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; +} + +- (BOOL)isEmpty { + return self.snapshot.documents.isEmpty; +} + +// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) +// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints +// where we can. See cr/146959032 for additional context. +- (NSInteger)count { + return self.snapshot.documents.count; +} + +- (NSArray *)documents { + if (!_documents) { + FSTDocumentSet *documentSet = self.snapshot.documents; + FIRFirestore *firestore = self.firestore; + BOOL fromCache = self.metadata.fromCache; + + NSMutableArray *result = [NSMutableArray array]; + for (FSTDocument *document in documentSet.documentEnumerator) { + [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; + } + + _documents = result; + } + return _documents; +} + +- (NSArray *)documentChanges { + if (!_documentChanges) { + _documentChanges = + [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; + } + return _documentChanges; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/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 + +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 *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self validateReference:document]; + FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.internalTransaction updateData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + [self.internalTransaction deleteDocument:document.key]; + return self; +} + +- (void)getDocument:(FIRDocumentReference *)document + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + [self validateReference:document]; + [self.internalTransaction + lookupDocumentsForKeys:@[ document.key ] + completion:^(NSArray *_Nullable documents, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + FSTAssert(documents.count == 1, + @"Mismatch in docs returned from document lookup."); + FSTMaybeDocument *internalDoc = documents.firstObject; + if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { + completion(nil, nil); + return; + } + FIRDocumentSnapshot *doc = + [FIRDocumentSnapshot snapshotWithFirestore:self.firestore + documentKey:internalDoc.key + document:(FSTDocument *)internalDoc + fromCache:NO]; + completion(doc, nil); + }]; +} + +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error { + [self validateReference:document]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block FIRDocumentSnapshot *result; + // We have to explicitly assign the innerError into a local to cause it to retain correctly. + __block NSError *outerError = nil; + [self getDocument:document + completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { + result = snapshot; + outerError = innerError; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (error) { + *error = outerError; + } + return result; +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch+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 *mutations; +@property(nonatomic, assign) BOOL committed; + +@end + +@implementation FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { + return [[FIRWriteBatch alloc] initWithFirestore:firestore]; +} + +@end + +@implementation FIRWriteBatch + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _firestore = firestore; + _mutations = [NSMutableArray array]; + } + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options]; + [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (FIRWriteBatch *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition preconditionWithExists:YES]]]; + return self; +} + +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (void)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 + +@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 *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, strong, readonly, nullable) FSTFieldMask *fieldMask; +@property(nonatomic, strong, readonly) NSArray *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 *)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 *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask; +@property(nonatomic, strong, readonly) NSArray *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 *)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 *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray *mutations = [NSMutableArray array]; + if (self.fieldMask) { + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + } else { + [mutations addObject:[[FSTSetMutation alloc] initWithKey:key + value:self.data + precondition:precondition]]; + } + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +#pragma mark - FSTParsedUpdateData + +@implementation FSTParsedUpdateData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray *mutations = [NSMutableArray array]; + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +/** + * Represents what type of API method provided the data being parsed; useful for determining which + * error conditions apply during parsing and providing better error messages. + */ +typedef NS_ENUM(NSInteger, FSTUserDataSource) { + FSTUserDataSourceSet, + 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 *fieldTransforms; +@property(nonatomic, strong, readonly) NSMutableArray *fieldMask; + +- (instancetype)init NS_UNAVAILABLE; +/** + * Initializes a FSTParseContext with the given source and path. + * + * @param dataSource Indicates what kind of API method this data came from. + * @param path A path within the object being parsed. This could be an empty path (in which case + * the context represents the root of the data being parsed), or a nonempty path (indicating the + * context represents a nested location within the data). + * + * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate + * the context represents any location within an array (in which case certain features will not work + * and errors will be somewhat compromised). + */ +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + fieldTransforms:(NSMutableArray *)fieldTransforms + fieldMask:(NSMutableArray *)fieldMask + NS_DESIGNATED_INITIALIZER; + +// Helpers to get a FSTParseContext for a child field. +- (instancetype)contextForField:(NSString *)fieldName; +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; +- (instancetype)contextForArrayIndex:(NSUInteger)index; +@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 *)fieldTransforms + fieldMask:(NSMutableArray *)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 *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 *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 *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 diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.h b/Firestore/Source/Auth/FSTCredentialsProvider.h new file mode 100644 index 0000000..eb591ab --- /dev/null +++ b/Firestore/Source/Auth/FSTCredentialsProvider.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +@class FIRApp; +@class FSTDispatchQueue; +@class FSTUser; + +#pragma mark - FSTGetTokenResult + +/** + * The current FSTUser and the authentication token provided by the underlying authentication + * mechanism. This is the result of calling -[FSTCredentialsProvider getTokenForcingRefresh]. + * + * ## Portability notes: no TokenType on iOS + * + * The TypeScript client supports 1st party Oauth tokens (for the Firebase Console to auth as the + * developer) and OAuth2 tokens for the node.js sdk to auth with a service account. We don't have + * plans to support either case on mobile so there's no TokenType here. + */ +// TODO(mcg): Rename FSTToken, change parameter order to line up with the other platforms. +@interface FSTGetTokenResult : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithUser:(FSTUser *)user + token:(NSString *_Nullable)token NS_DESIGNATED_INITIALIZER; + +/** The user with which the token is associated (used for persisting user state on disk, etc.). */ +@property(nonatomic, nonnull, readonly) FSTUser *user; + +/** The actual raw token. */ +@property(nonatomic, copy, nullable, readonly) NSString *token; + +@end + +#pragma mark - Typedefs + +/** + * `FSTVoidTokenErrorBlock` is a block that gets a token or an error. + * + * @param token An auth token as a string. + * @param error The error if one occurred, or else `nil`. + */ +typedef void (^FSTVoidGetTokenResultBlock)(FSTGetTokenResult *_Nullable token, + NSError *_Nullable error); + +/** Listener block notified with an FSTUser. */ +typedef void (^FSTVoidUserBlock)(FSTUser *user); + +#pragma mark - FSTCredentialsProvider + +/** Provides methods for getting the uid and token for the current user and listen for changes. */ +@protocol FSTCredentialsProvider + +/** Requests token for the current user, optionally forcing a refreshed token to be fetched. */ +- (void)getTokenForcingRefresh:(BOOL)forceRefresh completion:(FSTVoidGetTokenResultBlock)completion; + +/** + * A listener to be notified of user changes (sign-in / sign-out). It is immediately called once + * with the initial user. + * + * Note that this block will be called back on an arbitrary thread that is not the normal Firestore + * worker thread. + */ +@property(nonatomic, copy, nullable, readwrite) FSTVoidUserBlock userChangeListener; + +@end + +#pragma mark - FSTFirebaseCredentialsProvider + +/** + * `FSTFirebaseCredentialsProvider` uses Firebase Auth via `FIRApp` to get an auth token. + * + * NOTE: To simplify the implementation, it requires that you set `userChangeListener` with a + * non-`nil` value no more than once and don't call `getTokenForcingRefresh:` after setting + * it to `nil`. + * + * This class must be implemented in a thread-safe manner since it is accessed from the thread + * backing our internal worker queue and the callbacks from FIRAuth will be executed on an + * arbitrary different thread. + */ +@interface FSTFirebaseCredentialsProvider : NSObject + +/** + * Initializes a new FSTFirebaseCredentialsProvider. + * + * @param app The Firebase app from which to get credentials. + * + * @return A new instance of FSTFirebaseCredentialsProvider. + */ +- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER; + +- (id)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.m b/Firestore/Source/Auth/FSTCredentialsProvider.m new file mode 100644 index 0000000..cec7c2b --- /dev/null +++ b/Firestore/Source/Auth/FSTCredentialsProvider.m @@ -0,0 +1,161 @@ +/* + * 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 "FSTCredentialsProvider.h" + +#import +#import +#import +#import + +// This is not an exported header so it's not visible via FirebaseCommunity +#import "FIRAppInternal.h" + +#import "FIRFirestoreErrors.h" +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDispatchQueue.h" +#import "FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTGetTokenResult + +@implementation FSTGetTokenResult +- (instancetype)initWithUser:(FSTUser *)user token:(NSString *_Nullable)token { + if (self = [super init]) { + _user = user; + _token = token; + } + return self; +} +@end + +#pragma mark - FSTFirebaseCredentialsProvider +// TODO(mikelehen): Currently, we have a strong dependency on FIRAuth but we should ideally use +// only internal APIs on FIRApp instead. However, currently the FIRApp internal APIs don't expose +// the uid of the current user and don't expose an auth state change listener. So we use FIRAuth. +@interface FSTFirebaseCredentialsProvider () + +@property(nonatomic, strong, readonly) FIRApp *app; +@property(nonatomic, strong, readonly) FIRAuth *auth; + +/** Handle used to stop receiving auth changes once userChangeListener is removed. */ +@property(nonatomic, strong, nullable, readwrite) + FIRAuthStateDidChangeListenerHandle authListenerHandle; + +/** The current user as reported to us via our AuthStateDidChangeListener. */ +@property(nonatomic, strong, nonnull, readwrite) FSTUser *currentUser; + +/** + * Counter used to detect if the user changed while a -getTokenForcingRefresh: request was + * outstanding. + */ +@property(nonatomic, assign, readwrite) int userCounter; + +@end + +@implementation FSTFirebaseCredentialsProvider { + FSTVoidUserBlock _userChangeListener; +} + +- (instancetype)initWithApp:(FIRApp *)app { + self = [super init]; + if (self) { + _app = app; + _auth = [FIRAuth authWithApp:app]; + _currentUser = [[FSTUser alloc] initWithUID:self.auth.currentUser.uid]; + _userCounter = 0; + + // Register for user changes so that we can internally track the current user. + FSTWeakify(self); + _authListenerHandle = [self.auth addAuthStateDidChangeListener:^(FIRAuth *auth, FIRUser *user) { + FSTStrongify(self); + if (self) { + @synchronized(self) { + FSTUser *newUser = [[FSTUser alloc] initWithUID:user.uid]; + if (![newUser isEqual:self.currentUser]) { + self.currentUser = newUser; + self.userCounter++; + FSTVoidUserBlock listenerBlock = self.userChangeListener; + if (listenerBlock) { + listenerBlock(self.currentUser); + } + } + } + } + }]; + } + return self; +} + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh + completion:(FSTVoidGetTokenResultBlock)completion { + FSTAssert(self.authListenerHandle, @"getToken cannot be called after listener removed."); + + // Take note of the current value of the userCounter so that this method can fail (with a + // FIRFirestoreErrorCodeAborted error) if there is a user change while the request is outstanding. + int initialUserCounter = self.userCounter; + + void (^getTokenCallback)(NSString *, NSError *) = ^(NSString *_Nullable token, + NSError *_Nullable error) { + @synchronized(self) { + if (initialUserCounter != self.userCounter) { + // Cancel the request since the user changed while the request was outstanding so the + // response is likely for a previous user (which user, we can't be sure). + NSDictionary *errorInfo = @{ @"details" : @"getToken aborted due to user change." }; + NSError *cancelError = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeAborted + userInfo:errorInfo]; + completion(nil, cancelError); + } else { + FSTGetTokenResult *result = + [[FSTGetTokenResult alloc] initWithUser:self.currentUser token:token]; + completion(result, error); + } + }; + }; + + [self.app getTokenForcingRefresh:forceRefresh withCallback:getTokenCallback]; +} + +- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { + @synchronized(self) { + if (block) { + FSTAssert(!_userChangeListener, @"UserChangeListener set twice!"); + + // Fire initial event. + block(self.currentUser); + } else { + FSTAssert(self.authListenerHandle, @"UserChangeListener removed twice!"); + FSTAssert(_userChangeListener, @"UserChangeListener removed without being set!"); + [self.auth removeAuthStateDidChangeListener:self.authListenerHandle]; + + self.authListenerHandle = nil; + } + _userChangeListener = block; + } +} + +- (nullable FSTVoidUserBlock)userChangeListener { + @synchronized(self) { + return _userChangeListener; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h new file mode 100644 index 0000000..c0074c2 --- /dev/null +++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.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 + +#import "FSTCredentialsProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +/** `FSTEmptyCredentialsProvider` always yields an empty token. */ +@interface FSTEmptyCredentialsProvider : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m new file mode 100644 index 0000000..266e09b --- /dev/null +++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m @@ -0,0 +1,47 @@ +/* + * 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 "FSTEmptyCredentialsProvider.h" + +#import "FSTUser.h" +#import "FSTAssert.h" +#import "FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTEmptyCredentialsProvider + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh + completion:(FSTVoidGetTokenResultBlock)completion { + completion(nil, nil); +} + +- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { + // Since the user never changes, we just need to fire the initial event and don't need to hang + // onto the block. + if (block) { + block([FSTUser unauthenticatedUser]); + } +} + +- (nullable FSTVoidUserBlock)userChangeListener { + // TODO(mikelehen): Implementation omitted for convenience since it's not actually required. + FSTFail(@"Not implemented."); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTUser.h b/Firestore/Source/Auth/FSTUser.h new file mode 100644 index 0000000..83b1962 --- /dev/null +++ b/Firestore/Source/Auth/FSTUser.h @@ -0,0 +1,43 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * Simple wrapper around a nullable UID. Mostly exists to make code more readable and for use as + * a key in dictionaries (since keys cannot be nil). + */ +@interface FSTUser : NSObject + +/** Returns an FSTUser with a nil UID. */ ++ (instancetype)unauthenticatedUser; + +// Porting note: no GOOGLE_CREDENTIALS or FIRST_PARTY equivalent on iOS, see FSTGetTokenResult for +// more details. + +/** Initializes an FSTUser with the given UID. */ +- (instancetype)initWithUID:(NSString *_Nullable)UID NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, copy, nullable, readonly) NSString *UID; + +@property(nonatomic, assign, readonly, getter=isUnauthenticated) BOOL unauthenticated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTUser.m b/Firestore/Source/Auth/FSTUser.m new file mode 100644 index 0000000..a7492b2 --- /dev/null +++ b/Firestore/Source/Auth/FSTUser.m @@ -0,0 +1,68 @@ +/* + * 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 "FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTUser + +@implementation FSTUser + +@dynamic unauthenticated; + ++ (instancetype)unauthenticatedUser { + return [[FSTUser alloc] initWithUID:nil]; +} + +- (instancetype)initWithUID:(NSString *_Nullable)UID { + if (self = [super init]) { + _UID = UID; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } else if (![object isKindOfClass:[FSTUser class]]) { + return NO; + } else { + FSTUser *other = object; + return (self.isUnauthenticated && other.isUnauthenticated) || + [self.UID isEqualToString:other.UID]; + } +} + +- (NSUInteger)hash { + return [self.UID hash]; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + return self; // since FSTUser objects are immutable +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.UID]; +} + +- (BOOL)isUnauthenticated { + return self.UID == nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTDatabaseInfo.h b/Firestore/Source/Core/FSTDatabaseInfo.h new file mode 100644 index 0000000..fae884f --- /dev/null +++ b/Firestore/Source/Core/FSTDatabaseInfo.h @@ -0,0 +1,55 @@ +/* + * 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 + +@class FSTDatabaseID; + +NS_ASSUME_NONNULL_BEGIN + +/** FSTDatabaseInfo contains data about the database. */ +@interface FSTDatabaseInfo : NSObject + +/** + * Creates and returns a new FSTDatabaseInfo. + * + * @param databaseID The project/database to use. + * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived + * from -[FIRApp appName]. + * @param host The hostname of the datastore backend. + * @param sslEnabled Whether to use SSL when connecting. + * @return A new instance of FSTDatabaseInfo. + */ ++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled; + +/** The database info. */ +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; + +/** The application name, taken from FIRApp. */ +@property(nonatomic, copy, readonly) NSString *persistenceKey; + +/** The hostname of the backend. */ +@property(nonatomic, copy, readonly) NSString *host; + +/** Whether to use SSL when connecting. */ +@property(nonatomic, readonly, getter=isSSLEnabled) BOOL sslEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTDatabaseInfo.m b/Firestore/Source/Core/FSTDatabaseInfo.m new file mode 100644 index 0000000..d2cd0ed --- /dev/null +++ b/Firestore/Source/Core/FSTDatabaseInfo.m @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSTDatabaseInfo.h" + +#import "FSTDatabaseID.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDatabaseInfo + +@implementation FSTDatabaseInfo + +#pragma mark - Constructors + ++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled { + return [[FSTDatabaseInfo alloc] initWithDatabaseID:databaseID + persistenceKey:persistenceKey + host:host + sslEnabled:sslEnabled]; +} + +/** + * Designated initializer. + * + * @param databaseID The database in the datastore. + * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived + * from -[FIRApp appName]. + * @param host The Firestore server hostname. + * @param sslEnabled Whether to use SSL when connecting. + */ +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled { + if (self = [super init]) { + _databaseID = databaseID; + _persistenceKey = [persistenceKey copy]; + _host = [host copy]; + _sslEnabled = sslEnabled; + } + return self; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.databaseID, self.host]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h new file mode 100644 index 0000000..43ada66 --- /dev/null +++ b/Firestore/Source/Core/FSTEventManager.h @@ -0,0 +1,88 @@ +/* + * 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 + +#import "FSTRemoteStore.h" +#import "FSTTypes.h" +#import "FSTViewSnapshot.h" + +@class FSTQuery; +@class FSTSyncEngine; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTListenOptions + +@interface FSTListenOptions : NSObject + ++ (instancetype)defaultOptions; + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges; + +@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges; + +@property(nonatomic, assign, readonly) BOOL waitForSyncWhenOnline; + +@end + +#pragma mark - FSTQueryListener + +/** + * FSTQueryListener takes a series of internal view snapshots and determines when to raise + * user-facing events. + */ +@interface FSTQueryListener : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot; +- (void)queryDidError:(NSError *)error; +- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState; + +@property(nonatomic, strong, readonly) FSTQuery *query; + +@end + +#pragma mark - FSTEventManager + +/** + * EventManager is responsible for mapping queries to query event emitters. It handles "fan-out." + * (Identical queries will re-use the same watch on the backend.) + */ +@interface FSTEventManager : NSObject + ++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +- (FSTTargetID)addListener:(FSTQueryListener *)listener; +- (void)removeListener:(FSTQueryListener *)listener; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.m b/Firestore/Source/Core/FSTEventManager.m new file mode 100644 index 0000000..17a0546 --- /dev/null +++ b/Firestore/Source/Core/FSTEventManager.m @@ -0,0 +1,335 @@ +/* + * 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 "FSTEventManager.h" + +#import "FSTAssert.h" +#import "FSTDocumentSet.h" +#import "FSTQuery.h" +#import "FSTSyncEngine.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTListenOptions + +@implementation FSTListenOptions + ++ (instancetype)defaultOptions { + static FSTListenOptions *defaultOptions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + }); + return defaultOptions; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + _waitForSyncWhenOnline = waitForSyncWhenOnline; + } + return self; +} + +- (instancetype)init { + FSTFail(@"FSTListenOptions init not supported"); + return nil; +} + +@end + +#pragma mark - FSTQueryListenersInfo + +/** + * Holds the listeners and the last received ViewSnapshot for a query being tracked by + * EventManager. + */ +@interface FSTQueryListenersInfo : NSObject +@property(nonatomic, strong, nullable, readwrite) FSTViewSnapshot *viewSnapshot; +@property(nonatomic, assign, readwrite) FSTTargetID targetID; +@property(nonatomic, strong, readonly) NSMutableArray *listeners; +@end + +@implementation FSTQueryListenersInfo +- (instancetype)init { + if (self = [super init]) { + _listeners = [NSMutableArray array]; + } + return self; +} + +@end + +#pragma mark - FSTQueryListener + +@interface FSTQueryListener () + +/** The last received view snapshot. */ +@property(nonatomic, strong, nullable) FSTViewSnapshot *snapshot; + +@property(nonatomic, strong, readonly) FSTListenOptions *options; + +/** + * Initial snapshots (e.g. from cache) may not be propagated to the FSTViewSnapshotHandler. + * This flag is set to YES once we've actually raised an event. + */ +@property(nonatomic, assign, readwrite) BOOL raisedInitialEvent; + +/** The last online state this query listener got. */ +@property(nonatomic, assign, readwrite) FSTOnlineState onlineState; + +/** The FSTViewSnapshotHandler associated with this query listener. */ +@property(nonatomic, copy, nullable) FSTViewSnapshotHandler viewSnapshotHandler; + +@end + +@implementation FSTQueryListener + +- (instancetype)initWithQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + if (self = [super init]) { + _query = query; + _options = options; + _viewSnapshotHandler = viewSnapshotHandler; + _raisedInitialEvent = NO; + } + return self; +} + +- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot { + FSTAssert(snapshot.documentChanges.count > 0 || snapshot.syncStateChanged, + @"We got a new snapshot with no changes?"); + + if (!self.options.includeDocumentMetadataChanges) { + // Remove the metadata-only changes. + NSMutableArray *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + if (change.type != FSTDocumentViewChangeTypeMetadata) { + [changes addObject:change]; + } + } + snapshot = [[FSTViewSnapshot alloc] initWithQuery:snapshot.query + documents:snapshot.documents + oldDocuments:snapshot.oldDocuments + documentChanges:changes + fromCache:snapshot.fromCache + hasPendingWrites:snapshot.hasPendingWrites + syncStateChanged:snapshot.syncStateChanged]; + } + + if (!self.raisedInitialEvent) { + if ([self shouldRaiseInitialEventForSnapshot:snapshot onlineState:self.onlineState]) { + [self raiseInitialEventForSnapshot:snapshot]; + } + } else if ([self shouldRaiseEventForSnapshot:snapshot]) { + self.viewSnapshotHandler(snapshot, nil); + } + + self.snapshot = snapshot; +} + +- (void)queryDidError:(NSError *)error { + self.viewSnapshotHandler(nil, error); +} + +- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + if (self.snapshot && !self.raisedInitialEvent && + [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { + [self raiseInitialEventForSnapshot:self.snapshot]; + } +} + +- (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot + onlineState:(FSTOnlineState)onlineState { + FSTAssert(!self.raisedInitialEvent, + @"Determining whether to raise initial event, but already had first event."); + + // Always raise the first event when we're synced + if (!snapshot.fromCache) { + return YES; + } + + // NOTE: We consider OnlineState.Unknown as online (it should become Failed + // or Online if we wait long enough). + BOOL maybeOnline = onlineState != FSTOnlineStateFailed; + // Don't raise the event if we're online, aren't synced yet (checked + // above) and are waiting for a sync. + if (self.options.waitForSyncWhenOnline && maybeOnline) { + FSTAssert(snapshot.fromCache, @"Waiting for sync, but snapshot is not from cache."); + return NO; + } + + // Raise data from cache if we have any documents or we are offline + return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateFailed; +} + +- (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { + // We don't need to handle includeDocumentMetadataChanges here because the Metadata only changes + // have already been stripped out if needed. At this point the only changes we will see are the + // ones we should propagate. + if (snapshot.documentChanges.count > 0) { + return YES; + } + + BOOL hasPendingWritesChanged = + self.snapshot && self.snapshot.hasPendingWrites != snapshot.hasPendingWrites; + if (snapshot.syncStateChanged || hasPendingWritesChanged) { + return self.options.includeQueryMetadataChanges; + } + + // Generally we should have hit one of the cases above, but it's possible to get here if there + // were only metadata docChanges and they got stripped out. + return NO; +} + +- (void)raiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot { + FSTAssert(!self.raisedInitialEvent, @"Trying to raise initial events for second time"); + snapshot = [[FSTViewSnapshot alloc] + initWithQuery:snapshot.query + documents:snapshot.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snapshot.query.comparator] + documentChanges:[FSTQueryListener getInitialViewChangesFor:snapshot] + fromCache:snapshot.fromCache + hasPendingWrites:snapshot.hasPendingWrites + syncStateChanged:YES]; + self.raisedInitialEvent = YES; + self.viewSnapshotHandler(snapshot, nil); +} + ++ (NSArray *)getInitialViewChangesFor:(FSTViewSnapshot *)snapshot { + NSMutableArray *result = [NSMutableArray array]; + for (FSTDocument *doc in snapshot.documents.documentEnumerator) { + [result addObject:[FSTDocumentViewChange changeWithDocument:doc + type:FSTDocumentViewChangeTypeAdded]]; + } + return result; +} + +@end + +#pragma mark - FSTEventManager + +@interface FSTEventManager () + +- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) + NSMutableDictionary *queries; +@property(nonatomic, assign) FSTOnlineState onlineState; + +@end + +@implementation FSTEventManager + ++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine { + return [[FSTEventManager alloc] initWithSyncEngine:syncEngine]; +} + +- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine { + if (self = [super init]) { + _syncEngine = syncEngine; + _queries = [NSMutableDictionary dictionary]; + + _syncEngine.delegate = self; + } + return self; +} + +- (FSTTargetID)addListener:(FSTQueryListener *)listener { + FSTQuery *query = listener.query; + BOOL firstListen = NO; + + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (!queryInfo) { + firstListen = YES; + queryInfo = [[FSTQueryListenersInfo alloc] init]; + self.queries[query] = queryInfo; + } + [queryInfo.listeners addObject:listener]; + + [listener clientDidChangeOnlineState:self.onlineState]; + + if (queryInfo.viewSnapshot) { + [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; + } + + if (firstListen) { + queryInfo.targetID = [self.syncEngine listenToQuery:query]; + } + return queryInfo.targetID; +} + +- (void)removeListener:(FSTQueryListener *)listener { + FSTQuery *query = listener.query; + BOOL lastListen = NO; + + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + [queryInfo.listeners removeObject:listener]; + lastListen = (queryInfo.listeners.count == 0); + } + + if (lastListen) { + [self.queries removeObjectForKey:query]; + [self.syncEngine stopListeningToQuery:query]; + } +} + +- (void)handleViewSnapshots:(NSArray *)viewSnapshots { + for (FSTViewSnapshot *viewSnapshot in viewSnapshots) { + FSTQuery *query = viewSnapshot.query; + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + for (FSTQueryListener *listener in queryInfo.listeners) { + [listener queryDidChangeViewSnapshot:viewSnapshot]; + } + queryInfo.viewSnapshot = viewSnapshot; + } + } +} + +- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query { + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + for (FSTQueryListener *listener in queryInfo.listeners) { + [listener queryDidError:error]; + } + } + + // Remove all listeners. NOTE: We don't need to call [FSTSyncEngine stopListening] after an error. + [self.queries removeObjectForKey:query]; +} + +- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { + for (FSTQueryListener *listener in info.listeners) { + [listener clientDidChangeOnlineState:onlineState]; + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h new file mode 100644 index 0000000..45f13cc --- /dev/null +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -0,0 +1,87 @@ +/* + * 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 + +#import "FSTRemoteStore.h" +#import "FSTTypes.h" +#import "FSTViewSnapshot.h" + +@class FSTDatabaseID; +@class FSTDatabaseInfo; +@class FSTDispatchQueue; +@class FSTDocument; +@class FSTListenOptions; +@class FSTMutation; +@class FSTQuery; +@class FSTQueryListener; +@class FSTTransaction; +@protocol FSTCredentialsProvider; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FirestoreClient is a top-level class that constructs and owns all of the pieces of the client + * SDK architecture. It is responsible for creating the worker queue that is shared by all of the + * other components in the system. + */ +@interface FSTFirestoreClient : NSObject + +/** + * Creates and returns a FSTFirestoreClient with the given parameters. + * + * All callbacks and events will be triggered on the provided userDispatchQueue. + */ ++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +/** Shuts down this client, cancels all writes / listeners, and releases all resources. */ +- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion; + +/** Starts listening to a query. */ +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler; + +/** Stops listening to a query previously listened to. */ +- (void)removeListener:(FSTQueryListener *)listener; + +/** Write mutations. completion will be notified when it's written to the backend. */ +- (void)writeMutations:(NSArray *)mutations + completion:(nullable FSTVoidErrorBlock)completion; + +/** Tries to execute the transaction in updateBlock up to retries times. */ +- (void)transactionWithRetries:(int)retries + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion; + +/** The database ID of the databaseInfo this client was initialized with. */ +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; + +/** + * Dispatch queue for user callbacks / events. This will often be the "Main Dispatch Queue" of the + * app but the developer can configure it to a different queue if they so choose. + */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *userDispatchQueue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.m b/Firestore/Source/Core/FSTFirestoreClient.m new file mode 100644 index 0000000..2066ce9 --- /dev/null +++ b/Firestore/Source/Core/FSTFirestoreClient.m @@ -0,0 +1,271 @@ +/* + * 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 "FSTFirestoreClient.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTCredentialsProvider.h" +#import "FSTDatabaseInfo.h" +#import "FSTDatastore.h" +#import "FSTDispatchQueue.h" +#import "FSTEagerGarbageCollector.h" +#import "FSTEventManager.h" +#import "FSTLevelDB.h" +#import "FSTLocalSerializer.h" +#import "FSTLocalStore.h" +#import "FSTLogger.h" +#import "FSTMemoryPersistence.h" +#import "FSTNoOpGarbageCollector.h" +#import "FSTRemoteStore.h" +#import "FSTSerializerBeta.h" +#import "FSTSyncEngine.h" +#import "FSTTransaction.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTFirestoreClient () +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)queue NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; +@property(nonatomic, strong, readonly) FSTEventManager *eventManager; +@property(nonatomic, strong, readonly) id persistence; +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** + * Dispatch queue responsible for all of our internal processing. When we get incoming work from + * the user (via public API) or the network (incoming GRPC messages), we should always dispatch + * onto this queue. This ensures our internal data structures are never accessed from multiple + * threads simultaneously. + */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; + +@property(nonatomic, strong, readonly) id credentialsProvider; + +@end + +@implementation FSTFirestoreClient + ++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo + usePersistence:usePersistence + credentialsProvider:credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:workerDispatchQueue]; +} + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + if (self = [super init]) { + _databaseInfo = databaseInfo; + _credentialsProvider = credentialsProvider; + _userDispatchQueue = userDispatchQueue; + _workerDispatchQueue = workerDispatchQueue; + + dispatch_semaphore_t initialUserAvailable = dispatch_semaphore_create(0); + __block FSTUser *initialUser; + FSTWeakify(self); + _credentialsProvider.userChangeListener = ^(FSTUser *user) { + FSTStrongify(self); + if (self) { + if (!initialUser) { + initialUser = user; + dispatch_semaphore_signal(initialUserAvailable); + } else { + [workerDispatchQueue dispatchAsync:^{ + [self userDidChange:user]; + }]; + } + } + }; + + // Defer initialization until we get the current user from the userChangeListener. This is + // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized + // before any subsequently queued work runs. + [_workerDispatchQueue dispatchAsync:^{ + dispatch_semaphore_wait(initialUserAvailable, DISPATCH_TIME_FOREVER); + + [self initializeWithUser:initialUser usePersistence:usePersistence]; + }]; + } + return self; +} + +- (void)initializeWithUser:(FSTUser *)user usePersistence:(BOOL)usePersistence { + // Do all of our initialization on our own dispatch queue. + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Note: The initialization work must all be synchronous (we can't dispatch more work) since + // external write/listen operations could get queued to run before that subsequent work + // completes. + id garbageCollector; + if (usePersistence) { + // TODO(http://b/33384523): For now we just disable garbage collection when persistence is + // enabled. + garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; + + NSString *dir = [FSTLevelDB storageDirectoryForDatabaseInfo:self.databaseInfo + documentsDirectory:[FSTLevelDB documentsDirectory]]; + + FSTSerializerBeta *remoteSerializer = + [[FSTSerializerBeta alloc] initWithDatabaseID:self.databaseInfo.databaseID]; + FSTLocalSerializer *serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; + + _persistence = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; + } else { + garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + _persistence = [FSTMemoryPersistence persistence]; + } + + NSError *error; + if (![_persistence start:&error]) { + // If local storage fails to start then just throw up our hands: the error is unrecoverable. + // There's nothing an end-user can do and nearly all failures indicate the developer is doing + // something grossly wrong so we should stop them cold in their tracks with a failure they + // can't ignore. + [NSException raise:NSInternalInconsistencyException format:@"Failed to open DB: %@", error]; + } + + _localStore = [[FSTLocalStore alloc] initWithPersistence:_persistence + garbageCollector:garbageCollector + initialUser:user]; + + FSTDatastore *datastore = [FSTDatastore datastoreWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentialsProvider]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:datastore]; + + _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore + remoteStore:_remoteStore + initialUser:user]; + + _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; + + // Setup wiring for remote store. + _remoteStore.syncEngine = _syncEngine; + + _remoteStore.onlineStateDelegate = _eventManager; + + // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation + // queue, etc.) so must be started after LocalStore. + [_localStore start]; + [_remoteStore start]; +} + +- (void)userDidChange:(FSTUser *)user { + [self.workerDispatchQueue verifyIsCurrentQueue]; + + FSTLog(@"User Changed: %@", user); + [self.syncEngine userDidChange:user]; +} + +- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + self.credentialsProvider.userChangeListener = nil; + + [self.remoteStore shutdown]; + [self.localStore shutdown]; + [self.persistence shutdown]; + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } + }]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query + options:options + viewSnapshotHandler:viewSnapshotHandler]; + + [self.workerDispatchQueue dispatchAsync:^{ + [self.eventManager addListener:listener]; + }]; + + return listener; +} + +- (void)removeListener:(FSTQueryListener *)listener { + [self.workerDispatchQueue dispatchAsync:^{ + [self.eventManager removeListener:listener]; + }]; +} + +- (void)writeMutations:(NSArray *)mutations + completion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + if (mutations.count == 0) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } else { + [self.syncEngine writeMutations:mutations + completion:^(NSError *error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(error); + }]; + } + }]; + } + }]; +}; + +- (void)transactionWithRetries:(int)retries + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + [self.syncEngine transactionWithRetries:retries + workerDispatchQueue:self.workerDispatchQueue + updateBlock:updateBlock + completion:^(id _Nullable result, NSError *_Nullable error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(result, error); + }]; + } + }]; + + }]; +} + +- (FSTDatabaseID *)databaseID { + return self.databaseInfo.databaseID; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTQuery.h b/Firestore/Source/Core/FSTQuery.h new file mode 100644 index 0000000..0562ae4 --- /dev/null +++ b/Firestore/Source/Core/FSTQuery.h @@ -0,0 +1,269 @@ +/* + * 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 + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTResourcePath; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTRelationFilterOperator is a value relation operator that can be used to filter documents. + * It is similar to NSPredicateOperatorType, but only has operators supported by Firestore. + */ +typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) { + FSTRelationFilterOperatorLessThan = 0, + FSTRelationFilterOperatorLessThanOrEqual, + FSTRelationFilterOperatorEqual, + FSTRelationFilterOperatorGreaterThanOrEqual, + FSTRelationFilterOperatorGreaterThan, +}; + +/** Interface used for all query filters. */ +@protocol FSTFilter + +/** Returns the field the Filter operates over. */ +- (FSTFieldPath *)field; + +/** Returns true if a document matches the filter. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** A unique ID identifying the filter; used when serializing queries. */ +- (NSString *)canonicalID; + +@end + +/** + * FSTRelationFilter is a document filter constraint on a query with a single relation operator. + * It is similar to NSComparisonPredicate, except customized for Firestore semantics. + */ +@interface FSTRelationFilter : NSObject + +/** + * Creates a new constraint for filtering documents. + * + * @param field A path to a field in the document to filter on. The LHS of the expression. + * @param filterOperator The binary operator to apply. + * @param value A constant value to compare @a field to. The RHS of the expression. + * @return A new instance of FSTRelationFilter. + */ ++ (instancetype)filterWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value; + +- (instancetype)init NS_UNAVAILABLE; + +/** Returns YES if the receiver is not an equality relation. */ +- (BOOL)isInequality; + +/** The left hand side of the relation. A path into a document field. */ +@property(nonatomic, strong, readonly) FSTFieldPath *field; + +/** The type of equality/inequality operator to use in the relation. */ +@property(nonatomic, assign, readonly) FSTRelationFilterOperator filterOperator; + +/** The right hand side of the relation. A constant value to compare to. */ +@property(nonatomic, strong, readonly) FSTFieldValue *value; + +@end + +/** Filter that matches NULL values. */ +@interface FSTNullFilter : NSObject +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithField:(FSTFieldPath *)field NS_DESIGNATED_INITIALIZER; +@end + +/** Filter that matches NAN values. */ +@interface FSTNanFilter : NSObject +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithField:(FSTFieldPath *)field NS_DESIGNATED_INITIALIZER; +@end + +/** FSTSortOrder is a field and direction to order query results by. */ +@interface FSTSortOrder : NSObject + +/** Creates a new sort order with the given field and direction. */ ++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending; + +- (instancetype)init NS_UNAVAILABLE; + +/** Compares two documents based on the field and direction of this sort order. */ +- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2; + +/** The direction of the sort. */ +@property(nonatomic, assign, readonly, getter=isAscending) BOOL ascending; + +/** The field to sort by. */ +@property(nonatomic, strong, readonly) FSTFieldPath *field; + +@end + +/** + * FSTBound represents a bound of a query. + * + * The bound is specified with the given components representing a position and whether it's just + * before or just after the position (relative to whatever the query order is). + * + * The position represents a logical index position for a query. It's a prefix of values for + * the (potentially implicit) order by clauses of a query. + * + * FSTBound provides a function to determine whether a document comes before or after a bound. + * This is influenced by whether the position is just before or just after the provided values. + */ +@interface FSTBound : NSObject + +/** + * Creates a new bound. + * + * @param position The position relative to the sort order. + * @param isBefore Whether this bound is just before or just after the position. + */ ++ (instancetype)boundWithPosition:(NSArray *)position isBefore:(BOOL)isBefore; + +/** Whether this bound is just before or just after the provided position */ +@property(nonatomic, assign, readonly, getter=isBefore) BOOL before; + +/** The index position of this bound represented as an array of field values. */ +@property(nonatomic, strong, readonly) NSArray *position; + +/** Returns YES if a document comes before a bound using the provided sort order. */ +- (BOOL)sortsBeforeDocument:(FSTDocument *)document + usingSortOrder:(NSArray *)sortOrder; + +@end + +/** FSTQuery represents the internal structure of a Firestore query. */ +@interface FSTQuery : NSObject + +- (id)init NS_UNAVAILABLE; + +/** + * Initializes a query with all of its components directly. + */ +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray> *)filters + orderBy:(NSArray *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; + +/** + * Creates and returns a new FSTQuery. + * + * @param path The path to the collection to be queried over. + * @return A new instance of FSTQuery. + */ ++ (instancetype)queryWithPath:(FSTResourcePath *)path; + +/** + * Returns the list of ordering constraints that were explicitly requested on the query by the + * user. + * + * Note that the actual query performed might add additional sort orders to match the behavior + * of the backend. + */ +- (NSArray *)explicitSortOrders; + +/** + * Returns the full list of ordering constraints on the query. + * + * This might include additional sort orders added implicitly to match the backend behavior. + */ +- (NSArray *)sortOrders; + +/** + * Creates a new FSTQuery with an additional filter. + * + * @param filter The predicate to filter by. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingFilter:(id)filter; + +/** + * Creates a new FSTQuery with an additional ordering constraint. + * + * @param sortOrder The key and direction to order by. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder; + +/** + * Returns a new FSTQuery with the given limit on how many results can be returned. + * + * @param limit The maximum number of results to return. If @a limit <= 0, behavior is unspecified. + * If @a limit == NSNotFound, then no limit is applied. + */ +- (instancetype)queryBySettingLimit:(NSInteger)limit; + +/** + * Creates a new FSTQuery starting at the provided bound. + * + * @param bound The bound to start this query at. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingStartAt:(FSTBound *)bound; + +/** + * Creates a new FSTQuery ending at the provided bound. + * + * @param bound The bound to end this query at. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingEndAt:(FSTBound *)bound; + +/** Returns YES if the receiver is query for a specific document. */ +- (BOOL)isDocumentQuery; + +/** Returns YES if the @a document matches the constraints of the receiver. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** Returns a comparator that will sort documents according to the receiver's sort order. */ +- (NSComparator)comparator; + +/** Returns the field of the first filter on the receiver that's an inequality, or nil if none. */ +- (FSTFieldPath *_Nullable)inequalityFilterField; + +/** Returns the first field in an order-by constraint, or nil if none. */ +- (FSTFieldPath *_Nullable)firstSortOrderField; + +/** The base path of the query. */ +@property(nonatomic, strong, readonly) FSTResourcePath *path; + +/** The filters on the documents returned by the query. */ +@property(nonatomic, strong, readonly) NSArray> *filters; + +/** The maximum number of results to return, or NSNotFound if no limit. */ +@property(nonatomic, assign, readonly) NSInteger limit; + +/** + * A canonical string identifying the query. Two different instances of equivalent queries will + * return the same canonicalID. + */ +@property(nonatomic, strong, readonly) NSString *canonicalID; + +/** An optional bound to start the query at. */ +@property(nonatomic, nullable, strong, readonly) FSTBound *startAt; + +/** An optional bound to end the query at. */ +@property(nonatomic, nullable, strong, readonly) FSTBound *endAt; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTQuery.m b/Firestore/Source/Core/FSTQuery.m new file mode 100644 index 0000000..b220c7c --- /dev/null +++ b/Firestore/Source/Core/FSTQuery.m @@ -0,0 +1,759 @@ +/* + * 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 "FSTQuery.h" + +#import "FIRFirestore+Internal.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTRelationFilterOperator functions + +NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOperator) { + switch (filterOperator) { + case FSTRelationFilterOperatorLessThan: + return @"<"; + case FSTRelationFilterOperatorLessThanOrEqual: + return @"<="; + case FSTRelationFilterOperatorEqual: + return @"=="; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return @">="; + case FSTRelationFilterOperatorGreaterThan: + return @">"; + default: + FSTCFail(@"Unknown FSTRelationFilterOperator %lu", (unsigned long)filterOperator); + } +} + +#pragma mark - FSTRelationFilter + +@interface FSTRelationFilter () + +/** + * Initializes the receiver relation filter. + * + * @param field A path to a field in the document to filter on. The LHS of the expression. + * @param filterOperator The binary operator to apply. + * @param value A constant value to compare @a field to. The RHS of the expression. + */ +- (instancetype)initWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value NS_DESIGNATED_INITIALIZER; + +/** Returns YES if @a document matches the receiver's constraint. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** + * A canonical string identifying the filter. Two different instances of equivalent filters will + * return the same canonicalID. + */ +- (NSString *)canonicalID; + +@end + +@implementation FSTRelationFilter + +#pragma mark - Constructor methods + ++ (instancetype)filterWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value { + return [[FSTRelationFilter alloc] initWithField:field filterOperator:filterOperator value:value]; +} + +- (instancetype)initWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value { + self = [super init]; + if (self) { + _field = field; + _filterOperator = filterOperator; + _value = value; + } + return self; +} + +#pragma mark - Public Methods + +- (BOOL)isInequality { + return self.filterOperator != FSTRelationFilterOperatorEqual; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %@ %@", [self.field canonicalString], + FSTStringFromQueryRelationOperator(self.filterOperator), + self.value]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTRelationFilter class]]) { + return NO; + } + return [self isEqualToFilter:(FSTRelationFilter *)other]; +} + +#pragma mark - Private methods + +- (BOOL)matchesDocument:(FSTDocument *)document { + if ([self.field isKeyFieldPath]) { + FSTAssert([self.value isKindOfClass:[FSTReferenceValue class]], + @"Comparing on key, but filter value not a FSTReferenceValue."); + FSTReferenceValue *refValue = (FSTReferenceValue *)self.value; + NSComparisonResult comparison = FSTDocumentKeyComparator(document.key, refValue.value); + return [self matchesComparison:comparison]; + } else { + return [self matchesValue:[document fieldForPath:self.field]]; + } +} + +- (NSString *)canonicalID { + // TODO(b/37283291): This should be collision robust and avoid relying on |description| methods. + return [NSString stringWithFormat:@"%@%@%@", [self.field canonicalString], + FSTStringFromQueryRelationOperator(self.filterOperator), + [self.value value]]; +} + +- (BOOL)isEqualToFilter:(FSTRelationFilter *)other { + if (self.filterOperator != other.filterOperator) { + return NO; + } + if (![self.field isEqual:other.field]) { + return NO; + } + if (![self.value isEqual:other.value]) { + return NO; + } + return YES; +} + +/** Returns YES if receiver is true with the given value as its LHS. */ +- (BOOL)matchesValue:(FSTFieldValue *)other { + // Only compare types with matching backend order (such as double and int). + return self.value.typeOrder == other.typeOrder && + [self matchesComparison:[other compare:self.value]]; +} + +- (BOOL)matchesComparison:(NSComparisonResult)comparison { + switch (self.filterOperator) { + case FSTRelationFilterOperatorLessThan: + return comparison == NSOrderedAscending; + case FSTRelationFilterOperatorLessThanOrEqual: + return comparison == NSOrderedAscending || comparison == NSOrderedSame; + case FSTRelationFilterOperatorEqual: + return comparison == NSOrderedSame; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return comparison == NSOrderedDescending || comparison == NSOrderedSame; + case FSTRelationFilterOperatorGreaterThan: + return comparison == NSOrderedDescending; + default: + FSTFail(@"Unknown operator: %ld", (long)self.filterOperator); + } +} + +@end + +#pragma mark - FSTNullFilter + +@interface FSTNullFilter () +@property(nonatomic, strong, readonly) FSTFieldPath *field; +@end + +@implementation FSTNullFilter +- (instancetype)initWithField:(FSTFieldPath *)field { + if (self = [super init]) { + _field = field; + } + return self; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + FSTFieldValue *fieldValue = [document fieldForPath:self.field]; + return fieldValue != nil && [fieldValue isEqual:[FSTNullValue nullValue]]; +} + +- (NSString *)canonicalID { + return [NSString stringWithFormat:@"%@ IS NULL", [self.field canonicalString]]; +} + +- (NSString *)description { + return [self canonicalID]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + return [self.field isEqual:((FSTNullFilter *)other).field]; +} + +- (NSUInteger)hash { + return [self.field hash]; +} + +@end + +#pragma mark - FSTNanFilter + +@interface FSTNanFilter () +@property(nonatomic, strong, readonly) FSTFieldPath *field; +@end + +@implementation FSTNanFilter + +- (instancetype)initWithField:(FSTFieldPath *)field { + if (self = [super init]) { + _field = field; + } + return self; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + FSTFieldValue *fieldValue = [document fieldForPath:self.field]; + return fieldValue != nil && [fieldValue isEqual:[FSTDoubleValue nanValue]]; +} + +- (NSString *)canonicalID { + return [NSString stringWithFormat:@"%@ IS NaN", [self.field canonicalString]]; +} + +- (NSString *)description { + return [self canonicalID]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + return [self.field isEqual:((FSTNanFilter *)other).field]; +} + +- (NSUInteger)hash { + return [self.field hash]; +} +@end + +#pragma mark - FSTSortOrder + +@interface FSTSortOrder () + +/** Creates a new sort order with the given field and direction. */ +- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending; + +- (NSString *)canonicalID; + +@end + +@implementation FSTSortOrder + +#pragma mark - Constructor methods + ++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { + return [[FSTSortOrder alloc] initWithFieldPath:fieldPath ascending:ascending]; +} + +- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { + self = [super init]; + if (self) { + _field = fieldPath; + _ascending = ascending; + } + return self; +} + +#pragma mark - Public methods + +- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2 { + int modifier = self.isAscending ? 1 : -1; + if ([self.field isEqual:[FSTFieldPath keyFieldPath]]) { + return (NSComparisonResult)(modifier * FSTDocumentKeyComparator(document1.key, document2.key)); + } else { + FSTFieldValue *value1 = [document1 fieldForPath:self.field]; + FSTFieldValue *value2 = [document2 fieldForPath:self.field]; + FSTAssert(value1 != nil && value2 != nil, + @"Trying to compare documents on fields that don't exist."); + return modifier * [value1 compare:value2]; + } +} + +- (NSString *)canonicalID { + return [NSString + stringWithFormat:@"%@%@", self.field.canonicalString, self.isAscending ? @"asc" : @"desc"]; +} + +- (BOOL)isEqualToSortOrder:(FSTSortOrder *)other { + return [self.field isEqual:other.field] && self.isAscending == other.isAscending; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.field, + self.ascending ? @"asc" : @"desc"]; +} + +- (BOOL)isEqual:(NSObject *)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTSortOrder class]]) { + return NO; + } + return [self isEqualToSortOrder:(FSTSortOrder *)other]; +} + +- (NSUInteger)hash { + return [self.canonicalID hash]; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +@end + +#pragma mark - FSTBound + +@implementation FSTBound + +- (instancetype)initWithPosition:(NSArray *)position isBefore:(BOOL)isBefore { + if (self = [super init]) { + _position = position; + _before = isBefore; + } + return self; +} + ++ (instancetype)boundWithPosition:(NSArray *)position isBefore:(BOOL)isBefore { + return [[FSTBound alloc] initWithPosition:position isBefore:isBefore]; +} + +- (NSString *)canonicalString { + // TODO(b/29183165): Make this collision robust. + NSMutableString *string = [NSMutableString string]; + if (self.isBefore) { + [string appendString:@"b:"]; + } else { + [string appendString:@"a:"]; + } + for (FSTFieldValue *component in self.position) { + [string appendFormat:@"%@", component]; + } + return string; +} + +- (BOOL)sortsBeforeDocument:(FSTDocument *)document + usingSortOrder:(NSArray *)sortOrder { + FSTAssert(self.position.count <= sortOrder.count, + @"FSTIndexPosition has more components than provided sort order."); + __block NSComparisonResult result = NSOrderedSame; + [self.position enumerateObjectsUsingBlock:^(FSTFieldValue *fieldValue, NSUInteger idx, + BOOL *stop) { + FSTSortOrder *sortOrderComponent = sortOrder[idx]; + NSComparisonResult comparison; + if ([sortOrderComponent.field isEqual:[FSTFieldPath keyFieldPath]]) { + FSTAssert([fieldValue isKindOfClass:[FSTReferenceValue class]], + @"FSTBound has a non-key value where the key path is being used %@", fieldValue); + comparison = [fieldValue.value compare:document.key]; + } else { + FSTFieldValue *docValue = [document fieldForPath:sortOrderComponent.field]; + FSTAssert(docValue != nil, @"Field should exist since document matched the orderBy already."); + comparison = [fieldValue compare:docValue]; + } + + if (!sortOrderComponent.isAscending) { + comparison = comparison * -1; + } + + if (comparison != 0) { + result = comparison; + *stop = YES; + } + }]; + + return self.isBefore ? result <= NSOrderedSame : result < NSOrderedSame; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.position, + self.isBefore ? @"YES" : @"NO"]; +} + +- (BOOL)isEqual:(NSObject *)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTBound class]]) { + return NO; + } + + FSTBound *otherBound = (FSTBound *)other; + + return [self.position isEqualToArray:otherBound.position] && self.isBefore == otherBound.isBefore; +} + +- (NSUInteger)hash { + return 31 * self.position.hash + (self.isBefore ? 0 : 1); +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +@end + +#pragma mark - FSTQuery + +@interface FSTQuery () { + // Cached value of the canonicalID property. + NSString *_canonicalID; +} + +/** + * Initializes the receiver with the given query constraints. + * + * @param path The base path of the query. + * @param filters Filters specify which documents to include in the results. + * @param sortOrders The fields and directions to sort the results. + * @param limit If not NSNotFound, only this many results will be returned. + */ +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray> *)filters + orderBy:(NSArray *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; + +/** A list of fields given to sort by. This does not include the implicit key sort at the end. */ +@property(nonatomic, strong, readonly) NSArray *explicitSortOrders; + +/** The memoized list of sort orders */ +@property(nonatomic, nullable, strong, readwrite) NSArray *memoizedSortOrders; + +@end + +@implementation FSTQuery + +#pragma mark - Constructors + ++ (instancetype)queryWithPath:(FSTResourcePath *)path { + return [[FSTQuery alloc] initWithPath:path + filterBy:@[] + orderBy:@[] + limit:NSNotFound + startAt:nil + endAt:nil]; +} + +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray> *)filters + orderBy:(NSArray *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound { + if (self = [super init]) { + _path = path; + _filters = filters; + _explicitSortOrders = sortOrders; + _limit = limit; + _startAt = startAtBound; + _endAt = endAtBound; + } + return self; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.canonicalID]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTQuery class]]) { + return NO; + } + return [self isEqualToQuery:(FSTQuery *)object]; +} + +- (NSUInteger)hash { + return [self.canonicalID hash]; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +#pragma mark - Public methods + +- (NSArray *)sortOrders { + if (self.memoizedSortOrders == nil) { + FSTFieldPath *_Nullable inequalityField = [self inequalityFilterField]; + FSTFieldPath *_Nullable firstSortOrderField = [self firstSortOrderField]; + if (inequalityField && !firstSortOrderField) { + // In order to implicitly add key ordering, we must also add the inequality filter field for + // it to be a valid query. Note that the default inequality field and key ordering is + // ascending. + if ([inequalityField isKeyFieldPath]) { + self.memoizedSortOrders = + @[ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] ]; + } else { + self.memoizedSortOrders = @[ + [FSTSortOrder sortOrderWithFieldPath:inequalityField ascending:YES], + [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] + ]; + } + } else { + FSTAssert(!inequalityField || [inequalityField isEqual:firstSortOrderField], + @"First orderBy %@ should match inequality field %@.", firstSortOrderField, + inequalityField); + + __block BOOL foundKeyOrder = NO; + + NSMutableArray *result = [NSMutableArray array]; + for (FSTSortOrder *sortOrder in self.explicitSortOrders) { + [result addObject:sortOrder]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + foundKeyOrder = YES; + } + } + + if (!foundKeyOrder) { + // The direction of the implicit key ordering always matches the direction of the last + // explicit sort order + BOOL lastIsAscending = + self.explicitSortOrders.count > 0 ? self.explicitSortOrders.lastObject.ascending : YES; + [result addObject:[FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] + ascending:lastIsAscending]]; + } + + self.memoizedSortOrders = result; + } + } + return self.memoizedSortOrders; +} + +- (instancetype)queryByAddingFilter:(id)filter { + FSTAssert(![FSTDocumentKey isDocumentKey:self.path], @"No filtering allowed for document query"); + + FSTFieldPath *_Nullable newInequalityField = nil; + if ([filter isKindOfClass:[FSTRelationFilter class]] && + [((FSTRelationFilter *)filter)isInequality]) { + newInequalityField = filter.field; + } + FSTFieldPath *_Nullable queryInequalityField = [self inequalityFilterField]; + FSTAssert(!queryInequalityField || !newInequalityField || + [queryInequalityField isEqual:newInequalityField], + @"Query must only have one inequality field."); + + return [[FSTQuery alloc] initWithPath:self.path + filterBy:[self.filters arrayByAddingObject:filter] + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder { + FSTAssert(![FSTDocumentKey isDocumentKey:self.path], + @"No ordering is allowed for a document query."); + + // TODO(klimt): Validate that the same key isn't added twice. + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder] + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryBySettingLimit:(NSInteger)limit { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryByAddingStartAt:(FSTBound *)bound { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:bound + endAt:self.endAt]; +} + +- (instancetype)queryByAddingEndAt:(FSTBound *)bound { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:bound]; +} + +- (BOOL)isDocumentQuery { + return [FSTDocumentKey isDocumentKey:self.path] && self.filters.count == 0; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] && + [self filtersMatchDocument:document] && [self boundsMatchDocument:document]; +} + +- (NSComparator)comparator { + return ^NSComparisonResult(id document1, id document2) { + BOOL didCompareOnKeyField = NO; + for (FSTSortOrder *orderBy in self.sortOrders) { + NSComparisonResult comp = [orderBy compareDocument:document1 toDocument:document2]; + if (comp != NSOrderedSame) { + return comp; + } + didCompareOnKeyField = + didCompareOnKeyField || [orderBy.field isEqual:[FSTFieldPath keyFieldPath]]; + } + FSTAssert(didCompareOnKeyField, @"sortOrder of query did not include key ordering"); + return NSOrderedSame; + }; +} + +- (FSTFieldPath *_Nullable)inequalityFilterField { + for (id filter in self.filters) { + if ([filter isKindOfClass:[FSTRelationFilter class]] && + ((FSTRelationFilter *)filter).filterOperator != FSTRelationFilterOperatorEqual) { + return filter.field; + } + } + return nil; +} + +- (FSTFieldPath *_Nullable)firstSortOrderField { + return self.explicitSortOrders.firstObject.field; +} + +#pragma mark - Private properties + +- (NSString *)canonicalID { + if (_canonicalID) { + return _canonicalID; + } + + NSMutableString *canonicalID = [[self.path canonicalString] mutableCopy]; + + // Add filters. + [canonicalID appendString:@"|f:"]; + for (id predicate in self.filters) { + [canonicalID appendFormat:@"%@", [predicate canonicalID]]; + } + + // Add order by. + [canonicalID appendString:@"|ob:"]; + for (FSTSortOrder *orderBy in self.sortOrders) { + [canonicalID appendString:orderBy.canonicalID]; + } + + // Add limit. + if (self.limit != NSNotFound) { + [canonicalID appendFormat:@"|l:%ld", (long)self.limit]; + } + + if (self.startAt) { + [canonicalID appendFormat:@"|lb:%@", self.startAt.canonicalString]; + } + + if (self.endAt) { + [canonicalID appendFormat:@"|ub:%@", self.endAt.canonicalString]; + } + + _canonicalID = canonicalID; + return canonicalID; +} + +#pragma mark - Private methods + +- (BOOL)isEqualToQuery:(FSTQuery *)other { + return [self.path isEqual:other.path] && self.limit == other.limit && + [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] && + (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) && + (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]); +} + +/* Returns YES if the document matches the path for the receiver. */ +- (BOOL)pathMatchesDocument:(FSTDocument *)document { + FSTResourcePath *documentPath = document.key.path; + if ([FSTDocumentKey isDocumentKey:self.path]) { + // Exact match for document queries. + return [self.path isEqual:documentPath]; + } else { + // Shallow ancestor queries by default. + return [self.path isPrefixOfPath:documentPath] && self.path.length == documentPath.length - 1; + } +} + +/** + * A document must have a value for every ordering clause in order to show up in the results. + */ +- (BOOL)orderByMatchesDocument:(FSTDocument *)document { + for (FSTSortOrder *orderBy in self.explicitSortOrders) { + FSTFieldPath *fieldPath = orderBy.field; + // order by key always matches + if (![fieldPath isEqual:[FSTFieldPath keyFieldPath]] && + [document fieldForPath:fieldPath] == nil) { + return NO; + } + } + return YES; +} + +/** Returns YES if the document matches all of the filters in the receiver. */ +- (BOOL)filtersMatchDocument:(FSTDocument *)document { + for (id filter in self.filters) { + if (![filter matchesDocument:document]) { + return NO; + } + } + return YES; +} + +- (BOOL)boundsMatchDocument:(FSTDocument *)document { + if (self.startAt && ![self.startAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { + return NO; + } + if (self.endAt && [self.endAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { + return NO; + } + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.h b/Firestore/Source/Core/FSTSnapshotVersion.h new file mode 100644 index 0000000..b72e4a2 --- /dev/null +++ b/Firestore/Source/Core/FSTSnapshotVersion.h @@ -0,0 +1,43 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@class FSTTimestamp; + +/** + * A version of a document in Firestore. This corresponds to the version timestamp, such as + * update_time or read_time. + */ +@interface FSTSnapshotVersion : NSObject + +/** Creates a new version that is smaller than all other versions. */ ++ (instancetype)noVersion; + +/** Creates a new version representing the given timestamp. */ ++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp; + +- (instancetype)init NS_UNAVAILABLE; + +- (NSComparisonResult)compare:(FSTSnapshotVersion *)other; + +@property(nonatomic, strong, readonly) FSTTimestamp *timestamp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.m b/Firestore/Source/Core/FSTSnapshotVersion.m new file mode 100644 index 0000000..68d5d7f --- /dev/null +++ b/Firestore/Source/Core/FSTSnapshotVersion.m @@ -0,0 +1,80 @@ +/* + * 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 "FSTSnapshotVersion.h" + +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTSnapshotVersion + ++ (instancetype)noVersion { + static FSTSnapshotVersion *min; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:0]; + min = [FSTSnapshotVersion versionWithTimestamp:timestamp]; + }); + return min; +} + ++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp { + return [[FSTSnapshotVersion alloc] initWithTimestamp:timestamp]; +} + +- (instancetype)initWithTimestamp:(FSTTimestamp *)timestamp { + self = [super init]; + if (self) { + _timestamp = timestamp; + } + return self; +} + +#pragma mark - NSObject methods + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTSnapshotVersion class]]) { + return NO; + } + return [self.timestamp isEqual:((FSTSnapshotVersion *)object).timestamp]; +} + +- (NSUInteger)hash { + return self.timestamp.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.timestamp]; +} + +- (id)copyWithZone:(NSZone *_Nullable)zone { + // Implements NSCopying without actually copying because timestamps are immutable. + return self; +} + +#pragma mark - Public methods + +- (NSComparisonResult)compare:(FSTSnapshotVersion *)other { + return [self.timestamp compare:other.timestamp]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h new file mode 100644 index 0000000..1348ce1 --- /dev/null +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -0,0 +1,105 @@ +/* + * 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 + +#import "FSTRemoteStore.h" +#import "FSTTypes.h" + +@class FSTDispatchQueue; +@class FSTLocalStore; +@class FSTMutation; +@class FSTQuery; +@class FSTRemoteEvent; +@class FSTRemoteStore; +@class FSTUser; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTSyncEngineDelegate + +/** A Delegate to be notified when the sync engine produces new view snapshots or errors. */ +@protocol FSTSyncEngineDelegate +- (void)handleViewSnapshots:(NSArray *)viewSnapshots; +- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query; +@end + +/** + * SyncEngine is the central controller in the client SDK architecture. It is the glue code + * between the EventManager, LocalStore, and RemoteStore. Some of SyncEngine's responsibilities + * include: + * 1. Coordinating client requests and remote events between the EventManager and the local and + * remote data stores. + * 2. Managing a View object for each query, providing the unified view between the local and + * remote data stores. + * 3. Notifying the RemoteStore when the LocalStore has new mutations in its queue that need + * sending to the backend. + * + * The SyncEngine’s methods should only ever be called by methods running on our own worker + * dispatch queue. + */ +@interface FSTSyncEngine : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + remoteStore:(FSTRemoteStore *)remoteStore + initialUser:(FSTUser *)user NS_DESIGNATED_INITIALIZER; + +/** + * A delegate to be notified when queries being listened to produce new view snapshots or + * errors. + */ +@property(nonatomic, weak) id delegate; + +/** + * Initiates a new listen. The FSTLocalStore will be queried for initial data and the listen will + * be sent to the FSTRemoteStore to get remote data. The registered FSTSyncEngineDelegate will be + * notified of resulting view snapshots and/or listen errors. + * + * @return the target ID assigned to the query. + */ +- (FSTTargetID)listenToQuery:(FSTQuery *)query; + +/** Stops listening to a query previously listened to via listenToQuery:. */ +- (void)stopListeningToQuery:(FSTQuery *)query; + +/** + * Initiates the write of local mutation batch which involves adding the writes to the mutation + * queue, notifying the remote store about new mutations, and raising events for any changes this + * write caused. The provided completion block will be called once the write has been acked or + * rejected by the backend (or failed locally for any other reason). + */ +- (void)writeMutations:(NSArray *)mutations completion:(FSTVoidErrorBlock)completion; + +/** + * Runs the given transaction block up to retries times and then calls completion. + * + * @param retries The number of times to try before giving up. + * @param workerDispatchQueue The queue to dispatch sync engine calls to. + * @param updateBlock The block to call to execute the user's transaction. + * @param completion The block to call when the transaction is finished or failed. + */ +- (void)transactionWithRetries:(int)retries + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion; + +- (void)userDidChange:(FSTUser *)user; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.m b/Firestore/Source/Core/FSTSyncEngine.m new file mode 100644 index 0000000..8698a97 --- /dev/null +++ b/Firestore/Source/Core/FSTSyncEngine.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 "FSTSyncEngine.h" + +#import + +#import "FIRFirestoreErrors.h" +#import "FSTAssert.h" +#import "FSTDispatchQueue.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTEagerGarbageCollector.h" +#import "FSTLocalStore.h" +#import "FSTLocalViewChanges.h" +#import "FSTLocalWriteResult.h" +#import "FSTLogger.h" +#import "FSTMutationBatch.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTReferenceSet.h" +#import "FSTRemoteEvent.h" +#import "FSTSnapshotVersion.h" +#import "FSTTargetIDGenerator.h" +#import "FSTTransaction.h" +#import "FSTUser.h" +#import "FSTView.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTQueryView + +/** + * FSTQueryView contains all of the info that FSTSyncEngine needs to track for a particular + * query and view. + */ +@interface FSTQueryView : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + resumeToken:(NSData *)resumeToken + view:(FSTView *)view NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The query itself. */ +@property(nonatomic, strong, readonly) FSTQuery *query; + +/** The targetID created by the client that is used in the watch stream to identify this query. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** + * An identifier from the datastore backend that indicates the last state of the results that + * was received. This can be used to indicate where to continue receiving new doc changes for the + * query. + */ +@property(nonatomic, copy, readonly) NSData *resumeToken; + +/** + * The view is responsible for computing the final merged truth of what docs are in the query. + * It gets notified of local and remote changes, and applies the query filters and limits to + * determine the most correct possible results. + */ +@property(nonatomic, strong, readonly) FSTView *view; + +@end + +@implementation FSTQueryView + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + resumeToken:(NSData *)resumeToken + view:(FSTView *)view { + if (self = [super init]) { + _query = query; + _targetID = targetID; + _resumeToken = resumeToken; + _view = view; + } + return self; +} + +@end + +#pragma mark - FSTSyncEngine + +@interface FSTSyncEngine () + +/** The local store, used to persist mutations and cached documents. */ +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** The remote store for sending writes, watches, etc. to the backend. */ +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; + +/** FSTQueryViews for all active queries, indexed by query. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *queryViewsByQuery; + +/** FSTQueryViews for all active queries, indexed by target ID. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *queryViewsByTarget; + +/** + * When a document is in limbo, we create a special listen to resolve it. This maps the + * FSTDocumentKey of each limbo document to the FSTTargetID of the listen resolving it. + */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *limboTargetsByKey; + +/** The inverse of limboTargetsByKey, a map of FSTTargetID to the key of the limbo doc. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *limboKeysByTarget; + +/** Used to track any documents that are currently in limbo. */ +@property(nonatomic, strong, readonly) FSTReferenceSet *limboDocumentRefs; + +/** The garbage collector used to collect documents that are no longer in limbo. */ +@property(nonatomic, strong, readonly) FSTEagerGarbageCollector *limboCollector; + +/** Stores user completion blocks, indexed by user and FSTBatchID. */ +@property(nonatomic, strong) + NSMutableDictionary *> + *mutationCompletionBlocks; + +/** Used for creating the FSTTargetIDs for the listens used to resolve limbo documents. */ +@property(nonatomic, strong, readonly) FSTTargetIDGenerator *targetIdGenerator; + +@property(nonatomic, strong) FSTUser *currentUser; + +@end + +@implementation FSTSyncEngine + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + remoteStore:(FSTRemoteStore *)remoteStore + initialUser:(FSTUser *)initialUser { + if (self = [super init]) { + _localStore = localStore; + _remoteStore = remoteStore; + + _queryViewsByQuery = [NSMutableDictionary dictionary]; + _queryViewsByTarget = [NSMutableDictionary dictionary]; + + _limboTargetsByKey = [NSMutableDictionary dictionary]; + _limboKeysByTarget = [NSMutableDictionary dictionary]; + _limboCollector = [[FSTEagerGarbageCollector alloc] init]; + _limboDocumentRefs = [[FSTReferenceSet alloc] init]; + [_limboCollector addGarbageSource:_limboDocumentRefs]; + + _mutationCompletionBlocks = [NSMutableDictionary dictionary]; + _targetIdGenerator = [FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0]; + _currentUser = initialUser; + } + return self; +} + +- (FSTTargetID)listenToQuery:(FSTQuery *)query { + [self assertDelegateExistsForSelector:_cmd]; + FSTAssert(self.queryViewsByQuery[query] == nil, @"We already listen to query: %@", query); + + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + FSTDocumentKeySet *remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:remoteKeys]; + FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; + FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; + FSTAssert(viewChange.limboChanges.count == 0, + @"View returned limbo docs before target ack from the server."); + + FSTQueryView *queryView = [[FSTQueryView alloc] initWithQuery:query + targetID:queryData.targetID + resumeToken:queryData.resumeToken + view:view]; + self.queryViewsByQuery[query] = queryView; + self.queryViewsByTarget[@(queryData.targetID)] = queryView; + [self.delegate handleViewSnapshots:@[ viewChange.snapshot ]]; + + [self.remoteStore listenToTargetWithQueryData:queryData]; + return queryData.targetID; +} + +- (void)stopListeningToQuery:(FSTQuery *)query { + [self assertDelegateExistsForSelector:_cmd]; + + FSTQueryView *queryView = self.queryViewsByQuery[query]; + FSTAssert(queryView, @"Trying to stop listening to a query not found"); + + [self.localStore releaseQuery:query]; + [self.remoteStore stopListeningToTargetID:queryView.targetID]; + [self removeAndCleanupQuery:queryView]; + [self.localStore collectGarbage]; +} + +- (void)writeMutations:(NSArray *)mutations + completion:(FSTVoidErrorBlock)completion { + [self assertDelegateExistsForSelector:_cmd]; + + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + [self addMutationCompletionBlock:completion batchID:result.batchID]; + + [self emitNewSnapshotsWithChanges:result.changes remoteEvent:nil]; + [self.remoteStore fillWritePipeline]; +} + +- (void)addMutationCompletionBlock:(FSTVoidErrorBlock)completion batchID:(FSTBatchID)batchID { + NSMutableDictionary *completionBlocks = + self.mutationCompletionBlocks[self.currentUser]; + if (!completionBlocks) { + completionBlocks = [NSMutableDictionary dictionary]; + self.mutationCompletionBlocks[self.currentUser] = completionBlocks; + } + [completionBlocks setObject:completion forKey:@(batchID)]; +} + +/** + * Takes an updateBlock in which a set of reads and writes can be performed atomically. In the + * updateBlock, user code can read and write values using a transaction object. After the + * updateBlock, all changes will be committed. If someone else has changed any of the data + * referenced, then the updateBlock will be called again. If the updateBlock still fails after the + * given number of retries, then the transaction will be rejected. + * + * The transaction object passed to the updateBlock contains methods for accessing documents + * and collections. Unlike other firestore access, data accessed with the transaction will not + * reflect local changes that have not been committed. For this reason, it is required that all + * reads are performed before any writes. Transactions must be performed while online. + */ +- (void)transactionWithRetries:(int)retries + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion { + [workerDispatchQueue verifyIsCurrentQueue]; + FSTAssert(retries >= 0, @"Got negative number of retries for transaction"); + FSTTransaction *transaction = [self.remoteStore transaction]; + updateBlock(transaction, ^(id _Nullable result, NSError *_Nullable error) { + [workerDispatchQueue dispatchAsync:^{ + if (error) { + completion(nil, error); + return; + } + [transaction commitWithCompletion:^(NSError *_Nullable transactionError) { + if (!transactionError) { + completion(result, nil); + return; + } + // TODO(b/35201829): Only retry on real transaction failures. + if (retries == 0) { + NSError *wrappedError = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : @"Transaction failed all retries.", + NSUnderlyingErrorKey : transactionError + }]; + completion(nil, wrappedError); + return; + } + [workerDispatchQueue verifyIsCurrentQueue]; + return [self transactionWithRetries:(retries - 1) + workerDispatchQueue:workerDispatchQueue + updateBlock:updateBlock + completion:completion]; + }]; + }]; + }); +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + [self assertDelegateExistsForSelector:_cmd]; + + // Make sure limbo documents are deleted if there were no results + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + FSTBoxedTargetID *_Nonnull targetID, + FSTTargetChange *_Nonnull targetChange, BOOL *_Nonnull stop) { + FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID]; + if (limboKey && targetChange.currentStatusUpdate == FSTCurrentStatusUpdateMarkCurrent && + remoteEvent.documentUpdates[limboKey] == nil) { + // When listening to a query the server responds with a snapshot containing documents + // matching the query and a current marker telling us we're now in sync. It's possible for + // these to arrive as separate remote events or as a single remote event. For a document + // query, there will be no documents sent in the response if the document doesn't exist. + // + // If the snapshot arrives separately from the current marker, we handle it normally and + // updateTrackedLimboDocumentsWithChanges:targetID: will resolve the limbo status of the + // document, removing it from limboDocumentRefs. This works because clients only initiate + // limbo resolution when a target is current and because all current targets are always at a + // consistent snapshot. + // + // However, if the document doesn't exist and the current marker arrives, the document is + // not present in the snapshot and our normal view handling would consider the document to + // remain in limbo indefinitely because there are no updates to the document. To avoid this, + // we specially handle this just this case here: synthesizing a delete. + // + // TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an + // explicit delete message and we could remove this special logic. + [remoteEvent + addDocumentUpdate:[FSTDeletedDocument documentWithKey:limboKey + version:remoteEvent.snapshotVersion]]; + } + }]; + + FSTMaybeDocumentDictionary *changes = [self.localStore applyRemoteEvent:remoteEvent]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent]; +} + +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { + [self assertDelegateExistsForSelector:_cmd]; + + FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID]; + if (limboKey) { + // Since this query failed, we won't want to manually unlisten to it. + // So go ahead and remove it from bookkeeping. + [self.limboTargetsByKey removeObjectForKey:limboKey]; + [self.limboKeysByTarget removeObjectForKey:targetID]; + + // TODO(dimond): Retry on transient errors? + + // It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack. + // Ideally, we would have a method in the local store to purge a document. However, it would + // be tricky to keep all of the local store's invariants with another method. + NSMutableDictionary *targetChanges = + [NSMutableDictionary dictionary]; + FSTDeletedDocument *doc = + [FSTDeletedDocument documentWithKey:limboKey version:[FSTSnapshotVersion noVersion]]; + NSMutableDictionary *docUpdate = + [NSMutableDictionary dictionaryWithObject:doc forKey:limboKey]; + FSTRemoteEvent *event = [FSTRemoteEvent eventWithSnapshotVersion:[FSTSnapshotVersion noVersion] + targetChanges:targetChanges + documentUpdates:docUpdate]; + [self applyRemoteEvent:event]; + } else { + FSTQueryView *queryView = self.queryViewsByTarget[targetID]; + FSTAssert(queryView, @"Unknown targetId: %@", targetID); + [self.localStore releaseQuery:queryView.query]; + [self removeAndCleanupQuery:queryView]; + [self.delegate handleError:error forQuery:queryView.query]; + } +} + +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { + [self assertDelegateExistsForSelector:_cmd]; + + // The local store may or may not be able to apply the write result and raise events immediately + // (depending on whether the watcher is caught up), so we raise user callbacks first so that they + // consistently happen before listen events. + [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil]; + + FSTMaybeDocumentDictionary *changes = [self.localStore acknowledgeBatchWithResult:batchResult]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:nil]; +} + +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error { + [self assertDelegateExistsForSelector:_cmd]; + + // The local store may or may not be able to apply the write result and raise events immediately + // (depending on whether the watcher is caught up), so we raise user callbacks first so that they + // consistently happen before listen events. + [self processUserCallbacksForBatchID:batchID error:error]; + + FSTMaybeDocumentDictionary *changes = [self.localStore rejectBatchID:batchID]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:nil]; +} + +- (void)processUserCallbacksForBatchID:(FSTBatchID)batchID error:(NSError *_Nullable)error { + NSMutableDictionary *completionBlocks = + self.mutationCompletionBlocks[self.currentUser]; + + // NOTE: Mutations restored from persistence won't have completion blocks, so it's okay for + // this (or the completion below) to be nil. + if (completionBlocks) { + NSNumber *boxedBatchID = @(batchID); + FSTVoidErrorBlock completion = completionBlocks[boxedBatchID]; + if (completion) { + completion(error); + [completionBlocks removeObjectForKey:boxedBatchID]; + } + } +} + +- (void)assertDelegateExistsForSelector:(SEL)methodSelector { + FSTAssert(self.delegate, @"Tried to call '%@' before delegate was registered.", + NSStringFromSelector(methodSelector)); +} + +- (void)removeAndCleanupQuery:(FSTQueryView *)queryView { + [self.queryViewsByQuery removeObjectForKey:queryView.query]; + [self.queryViewsByTarget removeObjectForKey:@(queryView.targetID)]; + + [self.limboDocumentRefs removeReferencesForID:queryView.targetID]; + [self garbageCollectLimboDocuments]; +} + +/** + * Computes a new snapshot from the changes and calls the registered callback with the new snapshot. + */ +- (void)emitNewSnapshotsWithChanges:(FSTMaybeDocumentDictionary *)changes + remoteEvent:(FSTRemoteEvent *_Nullable)remoteEvent { + NSMutableArray *newSnapshots = [NSMutableArray array]; + NSMutableArray *documentChangesInAllViews = [NSMutableArray array]; + + [self.queryViewsByQuery + enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) { + FSTView *view = queryView.view; + FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:changes]; + if (viewDocChanges.needsRefill) { + // The query has a limit and some docs were removed/updated, so we need to re-run the + // query against the local store to make sure we didn't lose any good docs that had been + // past the limit. + FSTDocumentDictionary *docs = [self.localStore executeQuery:queryView.query]; + viewDocChanges = [view computeChangesWithDocuments:docs previousChanges:viewDocChanges]; + } + FSTTargetChange *_Nullable targetChange = remoteEvent.targetChanges[@(queryView.targetID)]; + FSTViewChange *viewChange = + [queryView.view applyChangesToDocuments:viewDocChanges targetChange:targetChange]; + + [self updateTrackedLimboDocumentsWithChanges:viewChange.limboChanges + targetID:queryView.targetID]; + + if (viewChange.snapshot) { + [newSnapshots addObject:viewChange.snapshot]; + FSTLocalViewChanges *docChanges = + [FSTLocalViewChanges changesForViewSnapshot:viewChange.snapshot]; + [documentChangesInAllViews addObject:docChanges]; + } + }]; + + [self.delegate handleViewSnapshots:newSnapshots]; + [self.localStore notifyLocalViewChanges:documentChangesInAllViews]; + [self.localStore collectGarbage]; +} + +/** Updates the limbo document state for the given targetID. */ +- (void)updateTrackedLimboDocumentsWithChanges:(NSArray *)limboChanges + targetID:(FSTTargetID)targetID { + for (FSTLimboDocumentChange *limboChange in limboChanges) { + switch (limboChange.type) { + case FSTLimboDocumentChangeTypeAdded: + [self.limboDocumentRefs addReferenceToKey:limboChange.key forID:targetID]; + [self trackLimboChange:limboChange]; + break; + + case FSTLimboDocumentChangeTypeRemoved: + FSTLog(@"Document no longer in limbo: %@", limboChange.key); + [self.limboDocumentRefs removeReferenceToKey:limboChange.key forID:targetID]; + break; + + default: + FSTFail(@"Unknown limbo change type: %ld", (long)limboChange.type); + } + } + [self garbageCollectLimboDocuments]; +} + +- (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange { + FSTDocumentKey *key = limboChange.key; + + if (!self.limboTargetsByKey[key]) { + FSTLog(@"New document in limbo: %@", key); + FSTTargetID limboTargetID = [self.targetIdGenerator nextID]; + FSTQuery *query = [FSTQuery queryWithPath:key.path]; + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:limboTargetID + purpose:FSTQueryPurposeLimboResolution]; + self.limboKeysByTarget[@(limboTargetID)] = key; + [self.remoteStore listenToTargetWithQueryData:queryData]; + self.limboTargetsByKey[key] = @(limboTargetID); + } +} + +/** Garbage collect the limbo documents that we no longer need to track. */ +- (void)garbageCollectLimboDocuments { + NSSet *garbage = [self.limboCollector collectGarbage]; + for (FSTDocumentKey *key in garbage) { + FSTBoxedTargetID *limboTarget = self.limboTargetsByKey[key]; + if (!limboTarget) { + // This target already got removed, because the query failed. + return; + } + FSTTargetID limboTargetID = limboTarget.intValue; + [self.remoteStore stopListeningToTargetID:limboTargetID]; + [self.limboTargetsByKey removeObjectForKey:key]; + [self.limboKeysByTarget removeObjectForKey:limboTarget]; + } +} + +// Used for testing +- (NSDictionary *)currentLimboDocuments { + // Return defensive copy + return [self.limboTargetsByKey copy]; +} + +- (void)userDidChange:(FSTUser *)user { + self.currentUser = user; + + // Notify local store and emit any resulting events from swapping out the mutation queue. + FSTMaybeDocumentDictionary *changes = [self.localStore userDidChange:user]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:nil]; + + // Notify remote store so it can restart its streams. + [self.remoteStore userDidChange:user]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTargetIDGenerator.h b/Firestore/Source/Core/FSTTargetIDGenerator.h new file mode 100644 index 0000000..5b9db10 --- /dev/null +++ b/Firestore/Source/Core/FSTTargetIDGenerator.h @@ -0,0 +1,55 @@ +/* + * 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 + +#import "FSTTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTTargetIDGenerator generates monotonically increasing integer IDs. There are separate + * generators for different scopes. While these generators will operate independently of each + * other, they are scoped, such that no two generators will ever produce the same ID. This is + * useful, because sometimes the backend may group IDs from separate parts of the client into the + * same ID space. + */ +@interface FSTTargetIDGenerator : NSObject + +/** + * Creates and returns the FSTTargetIDGenerator for the local store. + * + * @param after An ID to start at. Every call to nextID will return an ID > @a after. + * @return A shared instance of FSTTargetIDGenerator. + */ ++ (instancetype)generatorForLocalStoreStartingAfterID:(FSTTargetID)after; + +/** + * Creates and returns the FSTTargetIDGenerator for the sync engine. + * + * @param after An ID to start at. Every call to nextID will return an ID > @a after. + * @return A shared instance of FSTTargetIDGenerator. + */ ++ (instancetype)generatorForSyncEngineStartingAfterID:(FSTTargetID)after; + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + +/** Returns the next ID in the sequence. */ +- (FSTTargetID)nextID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTargetIDGenerator.m b/Firestore/Source/Core/FSTTargetIDGenerator.m new file mode 100644 index 0000000..86ded30 --- /dev/null +++ b/Firestore/Source/Core/FSTTargetIDGenerator.m @@ -0,0 +1,105 @@ +/* + * 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 "FSTTargetIDGenerator.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetIDGenerator + +static const int kReservedBits = 1; + +/** FSTTargetIDGeneratorID is the set of all valid generators. */ +typedef NS_ENUM(NSInteger, FSTTargetIDGeneratorID) { + FSTTargetIDGeneratorIDLocalStore = 0, + FSTTargetIDGeneratorIDSyncEngine = 1 +}; + +@interface FSTTargetIDGenerator () { + // This is volatile so it can be used with OSAtomicAdd32. + volatile FSTTargetID _previousID; +} + +/** + * Initializes the generator. + * + * @param generatorID A unique ID indicating which generator this is. + * @param after Every call to nextID will return a number > @a after. + */ +- (instancetype)initWithGeneratorID:(FSTTargetIDGeneratorID)generatorID + startingAfterID:(FSTTargetID)after NS_DESIGNATED_INITIALIZER; + +// This is typed as FSTTargetID because we need to do bitwise operations with them together. +@property(nonatomic, assign) FSTTargetID generatorID; +@end + +@implementation FSTTargetIDGenerator + +#pragma mark - Constructors + +- (instancetype)initWithGeneratorID:(FSTTargetIDGeneratorID)generatorID + startingAfterID:(FSTTargetID)after { + self = [super init]; + if (self) { + _generatorID = generatorID; + + // Replace the generator part of |after| with this generator's ID. + FSTTargetID afterWithoutGenerator = (after >> kReservedBits) << kReservedBits; + FSTTargetID afterGenerator = after - afterWithoutGenerator; + if (afterGenerator >= _generatorID) { + // For example, if: + // self.generatorID = 0b0000 + // after = 0b1011 + // afterGenerator = 0b0001 + // Then: + // previous = 0b1010 + // next = 0b1100 + _previousID = afterWithoutGenerator | self.generatorID; + } else { + // For example, if: + // self.generatorID = 0b0001 + // after = 0b1010 + // afterGenerator = 0b0000 + // Then: + // previous = 0b1001 + // next = 0b1011 + _previousID = (afterWithoutGenerator | self.generatorID) - (1 << kReservedBits); + } + } + return self; +} + ++ (instancetype)generatorForLocalStoreStartingAfterID:(FSTTargetID)after { + return [[FSTTargetIDGenerator alloc] initWithGeneratorID:FSTTargetIDGeneratorIDLocalStore + startingAfterID:after]; +} + ++ (instancetype)generatorForSyncEngineStartingAfterID:(FSTTargetID)after { + return [[FSTTargetIDGenerator alloc] initWithGeneratorID:FSTTargetIDGeneratorIDSyncEngine + startingAfterID:after]; +} + +#pragma mark - Public methods + +- (FSTTargetID)nextID { + return OSAtomicAdd32(1 << kReservedBits, &_previousID); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTimestamp.h b/Firestore/Source/Core/FSTTimestamp.h new file mode 100644 index 0000000..f86779d --- /dev/null +++ b/Firestore/Source/Core/FSTTimestamp.h @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An FSTTimestamp represents an absolute time from the backend at up to nanosecond precision. + * An FSTTimestamp is represented in terms of UTC and does not have an associated timezone. + */ +@interface FSTTimestamp : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a new timestamp. + * + * @param seconds the number of seconds since epoch. + * @param nanos the number of nanoseconds after the seconds. + */ +- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos NS_DESIGNATED_INITIALIZER; + +/** Creates a new timestamp with the current date / time. */ ++ (instancetype)timestamp; + +/** Creates a new timestamp from the given date. */ ++ (instancetype)timestampWithDate:(NSDate *)date; + +/** Returns a new NSDate corresponding to this timestamp. This may lose precision. */ +- (NSDate *)approximateDateValue; + +/** + * Converts the given date to a an ISO 8601 timestamp string, useful for rendering in JSON. + * + * ISO 8601 dates times in UTC look like this: "1912-04-14T23:40:00.000000000Z". + * + * @see http://www.ecma-international.org/ecma-262/6.0/#sec-date-time-string-format + */ +- (NSString *)ISO8601String; + +- (NSComparisonResult)compare:(FSTTimestamp *)other; + +/** + * Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. + * Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. + */ +@property(nonatomic, assign, readonly) int64_t seconds; + +/** + * Non-negative fractions of a second at nanosecond resolution. Negative second values with + * fractions must still have non-negative nanos values that count forward in time. + * Must be from 0 to 999,999,999 inclusive. + */ +@property(nonatomic, assign, readonly) int32_t nanos; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTimestamp.m b/Firestore/Source/Core/FSTTimestamp.m new file mode 100644 index 0000000..941217a --- /dev/null +++ b/Firestore/Source/Core/FSTTimestamp.m @@ -0,0 +1,122 @@ +/* + * 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 "FSTTimestamp.h" + +#import "FSTAssert.h" +#import "FSTComparison.h" + +NS_ASSUME_NONNULL_BEGIN + +static const int kNanosPerSecond = 1000000000; + +@implementation FSTTimestamp + +#pragma mark - Constructors + ++ (instancetype)timestamp { + return [FSTTimestamp timestampWithDate:[NSDate date]]; +} + ++ (instancetype)timestampWithDate:(NSDate *)date { + double secondsDouble; + double fraction = modf(date.timeIntervalSince1970, &secondsDouble); + // GCP Timestamps always have non-negative nanos. + if (fraction < 0) { + fraction += 1.0; + secondsDouble -= 1.0; + } + int64_t seconds = (int64_t)secondsDouble; + int32_t nanos = (int32_t)(fraction * kNanosPerSecond); + return [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; +} + +- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos { + self = [super init]; + if (self) { + FSTAssert(nanos >= 0, @"timestamp nanoseconds out of range: %d", nanos); + FSTAssert(nanos < 1e9, @"timestamp nanoseconds out of range: %d", nanos); + // Midnight at the beginning of 1/1/1 is the earliest timestamp Firestore supports. + FSTAssert(seconds >= -62135596800L, @"timestamp seconds out of range: %lld", seconds); + // This will break in the year 10,000. + FSTAssert(seconds < 253402300800L, @"timestamp seconds out of range: %lld", seconds); + + _seconds = seconds; + _nanos = nanos; + } + return self; +} + +#pragma mark - NSObject methods + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTTimestamp class]]) { + return NO; + } + return [self isEqualToTimestamp:(FSTTimestamp *)object]; +} + +- (NSUInteger)hash { + return (NSUInteger)((self.seconds >> 32) ^ self.seconds ^ self.nanos); +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.seconds, self.nanos]; +} + +/** Implements NSCopying without actually copying because timestamps are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +#pragma mark - Public methods + +- (NSDate *)approximateDateValue { + NSTimeInterval interval = (NSTimeInterval)self.seconds + ((NSTimeInterval)self.nanos) / 1e9; + return [NSDate dateWithTimeIntervalSince1970:interval]; +} + +- (BOOL)isEqualToTimestamp:(FSTTimestamp *)other { + return [self compare:other] == NSOrderedSame; +} + +- (NSString *)ISO8601String { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; + formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + NSDate *secondsDate = [NSDate dateWithTimeIntervalSince1970:self.seconds]; + NSString *secondsString = [formatter stringFromDate:secondsDate]; + FSTAssert(secondsString.length == 19, @"Invalid ISO string: %@", secondsString); + + NSString *nanosString = [NSString stringWithFormat:@"%09d", self.nanos]; + return [NSString stringWithFormat:@"%@.%@Z", secondsString, nanosString]; +} + +- (NSComparisonResult)compare:(FSTTimestamp *)other { + NSComparisonResult result = FSTCompareInt64s(self.seconds, other.seconds); + if (result != NSOrderedSame) { + return result; + } + return FSTCompareInt32s(self.nanos, other.nanos); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.h b/Firestore/Source/Core/FSTTransaction.h new file mode 100644 index 0000000..7fa3a10 --- /dev/null +++ b/Firestore/Source/Core/FSTTransaction.h @@ -0,0 +1,73 @@ +/* + * 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 + +#import "FSTTypes.h" + +@class FIRSetOptions; +@class FSTDatastore; +@class FSTDocumentKey; +@class FSTFieldMask; +@class FSTFieldTransform; +@class FSTMaybeDocument; +@class FSTObjectValue; +@class FSTParsedSetData; +@class FSTParsedUpdateData; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTransaction + +/** Provides APIs to use in a transaction context. */ +@interface FSTTransaction : NSObject + +/** Creates a new transaction object, which can only be used for one transaction attempt. **/ ++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore; + +/** + * Takes a set of keys and asynchronously attempts to fetch all the documents from the backend, + * ignoring any local changes. + */ +- (void)lookupDocumentsForKeys:(NSArray *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion; + +/** + * Stores mutation for the given key and set data, to be committed when commitWithCompletion is + * called. + */ +- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key; + +/** + * Stores mutations for the given key and update data, to be committed when commitWithCompletion + * is called. + */ +- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key; + +/** + * Stores a delete mutation for the given key, to be committed when commitWithCompletion is called. + */ +- (void)deleteDocument:(FSTDocumentKey *)key; + +/** + * Attempts to commit the mutations set on this transaction. Calls the given completion block when + * finished. Once this is called, no other mutations or commits are allowed on the transaction. + */ +- (void)commitWithCompletion:(FSTVoidErrorBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.m b/Firestore/Source/Core/FSTTransaction.m new file mode 100644 index 0000000..26c69e0 --- /dev/null +++ b/Firestore/Source/Core/FSTTransaction.m @@ -0,0 +1,250 @@ +/* + * 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 "FSTTransaction.h" + +#import + +#import "FIRFirestoreErrors.h" +#import "FIRSetOptions.h" +#import "FSTAssert.h" +#import "FSTDatastore.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentKeySet.h" +#import "FSTMutation.h" +#import "FSTSnapshotVersion.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTransaction + +@interface FSTTransaction () +@property(nonatomic, strong, readonly) FSTDatastore *datastore; +@property(nonatomic, strong, readonly) + NSMutableDictionary *readVersions; +@property(nonatomic, strong, readonly) NSMutableArray *mutations; +@property(nonatomic, assign) BOOL commitCalled; +/** + * An error that may have occurred as a consequence of a write. If set, needs to be raised in the + * completion handler instead of trying to commit. + */ +@property(nonatomic, strong, nullable) NSError *lastWriteError; +@end + +@implementation FSTTransaction + ++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore { + return [[FSTTransaction alloc] initWithDatastore:datastore]; +} + +- (instancetype)initWithDatastore:(FSTDatastore *)datastore { + self = [super init]; + if (self) { + _datastore = datastore; + _readVersions = [NSMutableDictionary dictionary]; + _mutations = [NSMutableArray array]; + _commitCalled = NO; + } + return self; +} + +/** + * Every time a document is read, this should be called to record its version. If we read two + * different versions of the same document, this will return an error through its out parameter. + * When the transaction is committed, the versions recorded will be set as preconditions on the + * writes sent to the backend. + */ +- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { + FSTAssert(error != nil, @"nil error parameter"); + *error = nil; + FSTSnapshotVersion *docVersion = doc.version; + if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + // For deleted docs, we must record an explicit no version to build the right precondition + // when writing. + docVersion = [FSTSnapshotVersion noVersion]; + } + FSTSnapshotVersion *existingVersion = self.readVersions[doc.key]; + if (existingVersion) { + if (error) { + *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : + @"A document cannot be read twice within a single transaction." + }]; + } + return NO; + } else { + self.readVersions[doc.key] = docVersion; + return YES; + } +} + +- (void)lookupDocumentsForKeys:(NSArray *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { + [self ensureCommitNotCalled]; + if (self.mutations.count) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"All reads in a transaction must be done before any writes."); + } + [self.datastore + lookupDocuments:keys + completion:^(NSArray *_Nullable documents, NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + for (FSTMaybeDocument *doc in documents) { + NSError *recordError = nil; + if (![self recordVersionForDocument:doc error:&recordError]) { + completion(nil, recordError); + return; + } + } + completion(documents, nil); + }]; +} + +/** Stores mutations to be written when commitWithCompletion is called. */ +- (void)writeMutations:(NSArray *)mutations { + [self ensureCommitNotCalled]; + [self.mutations addObjectsFromArray:mutations]; +} + +/** + * Returns version of this doc when it was read in this transaction as a precondition, or no + * precondition if it was not read. + */ +- (FSTPrecondition *)preconditionForDocumentKey:(FSTDocumentKey *)key { + FSTSnapshotVersion *_Nullable snapshotVersion = self.readVersions[key]; + if (snapshotVersion) { + return [FSTPrecondition preconditionWithUpdateTime:snapshotVersion]; + } else { + return [FSTPrecondition none]; + } +} + +/** + * Returns the precondition for a document if the operation is an update, based on the provided + * UpdateOptions. Will return nil if an error occurred, in which case it sets the error parameter. + */ +- (nullable FSTPrecondition *)preconditionForUpdateWithDocumentKey:(FSTDocumentKey *)key + error:(NSError **)error { + FSTSnapshotVersion *_Nullable version = self.readVersions[key]; + if (version && [version isEqual:[FSTSnapshotVersion noVersion]]) { + // The document was read, but doesn't exist. + // Return an error because the precondition is impossible + if (error) { + *error = [NSError + errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeAborted + userInfo:@{ + NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist." + }]; + } + return nil; + } else if (version) { + // Document exists, just base precondition on document update time. + return [FSTPrecondition preconditionWithUpdateTime:version]; + } else { + // Document was not read, so we just use the preconditions for an update. + return [FSTPrecondition preconditionWithExists:YES]; + } +} + +- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key { + [self writeMutations:[data mutationsWithKey:key + precondition:[self preconditionForDocumentKey:key]]]; +} + +- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key { + NSError *error = nil; + FSTPrecondition *_Nullable precondition = + [self preconditionForUpdateWithDocumentKey:key error:&error]; + if (precondition) { + [self writeMutations:[data mutationsWithKey:key precondition:precondition]]; + } else { + FSTAssert(error, @"Got nil precondition, but error was not set"); + self.lastWriteError = error; + } +} + +- (void)deleteDocument:(FSTDocumentKey *)key { + [self writeMutations:@[ [[FSTDeleteMutation alloc] + initWithKey:key + precondition:[self preconditionForDocumentKey:key]] ]]; + // Since the delete will be applied before all following writes, we need to ensure that the + // precondition for the next write will be exists: false. + self.readVersions[key] = [FSTSnapshotVersion noVersion]; +} + +- (void)commitWithCompletion:(FSTVoidErrorBlock)completion { + [self ensureCommitNotCalled]; + // Once commitWithCompletion is called once, mark this object so it can't be used again. + self.commitCalled = YES; + + // If there was an error writing, raise that error now + if (self.lastWriteError) { + completion(self.lastWriteError); + return; + } + + // Make a list of read documents that haven't been written. + __block FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet]; + [self.readVersions enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTSnapshotVersion *version, BOOL *stop) { + unwritten = [unwritten setByAddingObject:key]; + }]; + // For each mutation, note that the doc was written. + for (FSTMutation *mutation in self.mutations) { + unwritten = [unwritten setByRemovingObject:mutation.key]; + } + if (unwritten.count) { + // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. + completion([NSError + errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : @"Every document read in a transaction must also be " + @"written in that transaction." + }]); + } else { + [self.datastore commitMutations:self.mutations + completion:^(NSError *_Nullable error) { + if (error) { + completion(error); + } else { + completion(nil); + } + }]; + } +} + +- (void)ensureCommitNotCalled { + if (self.commitCalled) { + FSTThrowInvalidUsage( + @"FIRIllegalStateException", + @"A transaction object cannot be used after its update block has completed."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h new file mode 100644 index 0000000..8f1183c --- /dev/null +++ b/Firestore/Source/Core/FSTTypes.h @@ -0,0 +1,90 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@class FSTMaybeDocument; +@class FSTTransaction; + +/** FSTBatchID is a locally assigned ID for a batch of mutations that have been applied. */ +typedef int32_t FSTBatchID; + +typedef int32_t FSTTargetID; + +typedef NSNumber FSTBoxedTargetID; + +/** + * FSTVoidBlock is a block that's called when a specific event happens but that otherwise has + * no information associated with it. + */ +typedef void (^FSTVoidBlock)(); + +/** + * FSTVoidErrorBlock is a block that gets an error, if one occurred. + * + * @param error The error if it occurred, or nil. + */ +typedef void (^FSTVoidErrorBlock)(NSError *_Nullable error); + +/** FSTVoidIDErrorBlock is a block that takes an optional value and error. */ +typedef void (^FSTVoidIDErrorBlock)(id _Nullable, NSError *_Nullable); + +/** + * FSTVoidMaybeDocumentErrorBlock is a block that gets either a list of documents or an error. + * + * @param documents The documents, if no error occurred, or nil. + * @param error The error, if one occurred, or nil. + */ +typedef void (^FSTVoidMaybeDocumentArrayErrorBlock)( + NSArray *_Nullable documents, NSError *_Nullable error); + +/** + * FSTTransactionBlock is a block that wraps a user's transaction update block internally. + * + * @param transaction An object with methods for performing reads and writes within the + * transaction. + * @param completion To be called by the block once the user's code is finished. + */ +typedef void (^FSTTransactionBlock)(FSTTransaction *transaction, + void (^completion)(id _Nullable, NSError *_Nullable)); + +/** Describes the online state of the Firestore client */ +typedef NS_ENUM(NSUInteger, FSTOnlineState) { + /** + * The Firestore client is in an unknown online state. This means the client is either not + * actively trying to establish a connection or it was previously in an unknown state and is + * trying to establish a connection. + */ + FSTOnlineStateUnknown, + + /** + * The client is connected and the connections are healthy. This state is reached after a + * successful connection and there has been at least one successful message received from the + * backends. + */ + FSTOnlineStateHealthy, + + /** + * The client has tried to establish a connection but has failed. + * This state is reached after either a connection attempt failed or a healthy stream was closed + * for unexpected reasons. + */ + FSTOnlineStateFailed +}; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h new file mode 100644 index 0000000..2dbfac2 --- /dev/null +++ b/Firestore/Source/Core/FSTView.h @@ -0,0 +1,143 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" + +@class FSTDocumentKey; +@class FSTDocumentSet; +@class FSTDocumentViewChangeSet; +@class FSTMaybeDocument; +@class FSTQuery; +@class FSTTargetChange; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTViewDocumentChanges + +/** The result of applying a set of doc changes to a view. */ +@interface FSTViewDocumentChanges : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** The new set of docs that should be in the view. */ +@property(nonatomic, strong, readonly) FSTDocumentSet *documentSet; + +/** The diff of this these docs with the previous set of docs. */ +@property(nonatomic, strong, readonly) FSTDocumentViewChangeSet *changeSet; + +/** + * Whether the set of documents passed in was not sufficient to calculate the new state of the view + * and there needs to be another pass based on the local cache. + */ +@property(nonatomic, assign, readonly) BOOL needsRefill; + +@property(nonatomic, strong, readonly) FSTDocumentKeySet *mutatedKeys; + +@end + +#pragma mark - FSTLimboDocumentChange + +typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { + FSTLimboDocumentChangeTypeAdded = 0, + FSTLimboDocumentChangeTypeRemoved, +}; + +// A change to a particular document wrt to whether it is in "limbo". +@interface FSTLimboDocumentChange : NSObject + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key; + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + +@property(nonatomic, assign, readonly) FSTLimboDocumentChangeType type; +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +#pragma mark - FSTViewChange + +// A set of changes to a view. +@interface FSTViewChange : NSObject + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + +@property(nonatomic, strong, readonly, nullable) FSTViewSnapshot *snapshot; +@property(nonatomic, strong, readonly) NSArray *limboChanges; +@end + +#pragma mark - FSTView + +/** + * View is responsible for computing the final merged truth of what docs are in a query. It gets + * notified of local and remote changes to docs, and applies the query filters and limits to + * determine the most correct possible results. + */ +@interface FSTView : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithQuery:(FSTQuery *)query + remoteDocuments:(FSTDocumentKeySet *)remoteDocuments NS_DESIGNATED_INITIALIZER; + +/** + * Iterates over a set of doc changes, applies the query limit, and computes what the new results + * should be, what the changes were, and whether we may need to go back to the local cache for + * more results. Does not make any changes to the view. + * + * @param docChanges The doc changes to apply to this view. + * @return a new set of docs, changes, and refill flag. + */ +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges; + +/** + * Iterates over a set of doc changes, applies the query limit, and computes what the new results + * should be, what the changes were, and whether we may need to go back to the local cache for + * more results. Does not make any changes to the view. + * + * @param docChanges The doc changes to apply to this view. + * @param previousChanges If this is being called with a refill, then start with this set of docs + * and changes instead of the current view. + * @return a new set of docs, changes, and refill flag. + */ +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges + previousChanges: + (nullable FSTViewDocumentChanges *)previousChanges; + +/** + * Updates the view with the given ViewDocumentChanges. + * + * @param docChanges The set of changes to make to the view's docs. + * @return A new FSTViewChange with the given docs, changes, and sync state. + */ +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges; + +/** + * Updates the view with the given FSTViewDocumentChanges and updates limbo docs and sync state from + * the given (optional) target change. + * + * @param docChanges The set of changes to make to the view's docs. + * @param targetChange A target change to apply for computing limbo docs and sync state. + * @return A new FSTViewChange with the given docs, changes, and sync state. + */ +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange:(nullable FSTTargetChange *)targetChange; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.m b/Firestore/Source/Core/FSTView.m new file mode 100644 index 0000000..719e303 --- /dev/null +++ b/Firestore/Source/Core/FSTView.m @@ -0,0 +1,451 @@ +/* + * 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 "FSTView.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTFieldValue.h" +#import "FSTQuery.h" +#import "FSTRemoteEvent.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTViewDocumentChanges + +/** The result of applying a set of doc changes to a view. */ +@interface FSTViewDocumentChanges () + +- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet + changeSet:(FSTDocumentViewChangeSet *)changeSet + needsRefill:(BOOL)needsRefill + mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTViewDocumentChanges + +- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet + changeSet:(FSTDocumentViewChangeSet *)changeSet + needsRefill:(BOOL)needsRefill + mutatedKeys:(FSTDocumentKeySet *)mutatedKeys { + self = [super init]; + if (self) { + _documentSet = documentSet; + _changeSet = changeSet; + _needsRefill = needsRefill; + _mutatedKeys = mutatedKeys; + } + return self; +} + +@end + +#pragma mark - FSTLimboDocumentChange + +@interface FSTLimboDocumentChange () + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key; + +- (instancetype)initWithType:(FSTLimboDocumentChangeType)type + key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTLimboDocumentChange + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { + return [[FSTLimboDocumentChange alloc] initWithType:type key:key]; +} + +- (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { + self = [super init]; + if (self) { + _type = type; + _key = key; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTLimboDocumentChange class]]) { + return NO; + } + FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other; + return self.type == otherChange.type && [self.key isEqual:otherChange.key]; +} + +@end + +#pragma mark - FSTViewChange + +@interface FSTViewChange () + ++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges; + +- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTViewChange + ++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges { + return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges]; +} + +- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray *)limboChanges { + self = [super init]; + if (self) { + _snapshot = snapshot; + _limboChanges = limboChanges; + } + return self; +} + +@end + +#pragma mark - FSTView + +static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, + FSTDocumentViewChangeType c2); + +@interface FSTView () + +@property(nonatomic, strong, readonly) FSTQuery *query; + +@property(nonatomic, assign) FSTSyncState syncState; + +/** + * A flag whether the view is current with the backend. A view is considered current after it + * has seen the current flag from the backend and did not lose consistency within the watch stream + * (e.g. because of an existence filter mismatch). + */ +@property(nonatomic, assign, getter=isCurrent) BOOL current; + +@property(nonatomic, strong) FSTDocumentSet *documentSet; + +/** Documents included in the remote target. */ +@property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments; + +/** Documents in the view but not in the remote target */ +@property(nonatomic, strong) FSTDocumentKeySet *limboDocuments; + +/** Document Keys that have local changes. */ +@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys; + +@end + +@implementation FSTView + +- (instancetype)initWithQuery:(FSTQuery *)query + remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments { + self = [super init]; + if (self) { + _query = query; + _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; + _syncedDocuments = remoteDocuments; + _limboDocuments = [FSTDocumentKeySet keySet]; + _mutatedKeys = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges { + return [self computeChangesWithDocuments:docChanges previousChanges:nil]; +} + +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges + previousChanges: + (nullable FSTViewDocumentChanges *)previousChanges { + FSTDocumentViewChangeSet *changeSet = + previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; + FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; + + __block FSTDocumentKeySet *newMutatedKeys = + previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys; + __block FSTDocumentSet *newDocumentSet = oldDocumentSet; + __block BOOL needsRefill = NO; + + // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an + // update moving a doc past the old limit) might mean there is some other document in the local + // cache that either should come (1) between the old last limit doc and the new last document, + // in the case of updates, or (2) after the new last document, in the case of deletes. So we + // keep this doc at the old limit to compare the updates to. + // + // Note that this should never get used in a refill (when previousChanges is set), because there + // will only be adds -- no deletes or updates. + FSTDocument *_Nullable lastDocInLimit = + (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument + : nil; + + [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTMaybeDocument *maybeNewDoc, BOOL *stop) { + FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key]; + FSTDocument *_Nullable newDoc = nil; + if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) { + newDoc = (FSTDocument *)maybeNewDoc; + } + if (newDoc) { + FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key, + newDoc.key); + if (![self.query matchesDocument:newDoc]) { + newDoc = nil; + } + } + if (newDoc) { + newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; + if (newDoc.hasLocalMutations) { + newMutatedKeys = [newMutatedKeys setByAddingObject:key]; + } else { + newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + } + } else { + newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; + newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + } + + // Calculate change + if (oldDoc && newDoc) { + BOOL docsEqual = [oldDoc.data isEqual:newDoc.data]; + if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) { + // only report a change if document actually changed. + if (docsEqual) { + [changeSet addChange:[FSTDocumentViewChange + changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeMetadata]]; + } else { + [changeSet addChange:[FSTDocumentViewChange + changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeModified]]; + } + + if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) { + // This doc moved from inside the limit to after the limit. That means there may be some + // doc in the local cache that's actually less than this one. + needsRefill = YES; + } + } + } else if (!oldDoc && newDoc) { + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeAdded]]; + } else if (oldDoc && !newDoc) { + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:oldDoc + type:FSTDocumentViewChangeTypeRemoved]]; + if (lastDocInLimit) { + // A doc was removed from a full limit query. We'll need to re-query from the local cache + // to see if we know about some other doc that should be in the results. + needsRefill = YES; + } + } + }]; + if (self.query.limit) { + // TODO(klimt): Make DocumentSet size be constant time. + while (newDocumentSet.count > self.query.limit) { + FSTDocument *oldDoc = [newDocumentSet lastDocument]; + newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key]; + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:oldDoc + type:FSTDocumentViewChangeTypeRemoved]]; + } + } + + FSTAssert(!needsRefill || !previousChanges, + @"View was refilled using docs that themselves needed refilling."); + + return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet + changeSet:changeSet + needsRefill:needsRefill + mutatedKeys:newMutatedKeys]; +} + +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { + return [self applyChangesToDocuments:docChanges targetChange:nil]; +} + +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange:(nullable FSTTargetChange *)targetChange { + FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill"); + + FSTDocumentSet *oldDocuments = self.documentSet; + self.documentSet = docChanges.documentSet; + self.mutatedKeys = docChanges.mutatedKeys; + + // Sort changes based on type and query comparator. + NSArray *changes = [docChanges.changeSet changes]; + changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1, + FSTDocumentViewChange *c2) { + NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type); + if (typeComparison != NSOrderedSame) { + return typeComparison; + } + return self.query.comparator(c1.document, c2.document); + }]; + + NSArray *limboChanges = [self applyTargetChange:targetChange]; + BOOL synced = self.limboDocuments.count == 0 && self.isCurrent; + FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; + BOOL syncStateChanged = newSyncState != self.syncState; + self.syncState = newSyncState; + + if (changes.count == 0 && !syncStateChanged) { + // No changes. + return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges]; + } else { + FSTViewSnapshot *snapshot = + [[FSTViewSnapshot alloc] initWithQuery:self.query + documents:docChanges.documentSet + oldDocuments:oldDocuments + documentChanges:changes + fromCache:newSyncState == FSTSyncStateLocal + hasPendingWrites:!docChanges.mutatedKeys.isEmpty + syncStateChanged:syncStateChanged]; + + return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; + } +} + +#pragma mark - Private methods + +/** Returns whether the doc for the given key should be in limbo. */ +- (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key { + // If the remote end says it's part of this query, it's not in limbo. + if ([self.syncedDocuments containsObject:key]) { + return NO; + } + // The local store doesn't think it's a result, so it shouldn't be in limbo. + if (![self.documentSet containsKey:key]) { + return NO; + } + // If there are local changes to the doc, they might explain why the server doesn't know that it's + // part of the query. So don't put it in limbo. + // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific + // query. + if ([self.documentSet documentForKey:key].hasLocalMutations) { + return NO; + } + // Everything else is in limbo. + return YES; +} + +/** + * Updates syncedDocuments, isAcked, and limbo docs based on the given change. + * @return the list of changes to which docs are in limbo. + */ +- (NSArray *)applyTargetChange:(nullable FSTTargetChange *)targetChange { + if (targetChange) { + FSTTargetMapping *targetMapping = targetChange.mapping; + if ([targetMapping isKindOfClass:[FSTResetMapping class]]) { + self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents; + } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) { + [((FSTUpdateMapping *)targetMapping).addedDocuments + enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + self.syncedDocuments = [self.syncedDocuments setByAddingObject:key]; + }]; + [((FSTUpdateMapping *)targetMapping).removedDocuments + enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key]; + }]; + } + + switch (targetChange.currentStatusUpdate) { + case FSTCurrentStatusUpdateMarkCurrent: + self.current = YES; + break; + case FSTCurrentStatusUpdateMarkNotCurrent: + self.current = NO; + break; + case FSTCurrentStatusUpdateNone: + break; + } + } + + // Recompute the set of limbo docs. + // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. + FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments; + self.limboDocuments = [FSTDocumentKeySet keySet]; + if (self.isCurrent) { + for (FSTDocument *doc in self.documentSet.documentEnumerator) { + if ([self shouldBeLimboDocumentKey:doc.key]) { + self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key]; + } + } + } + + // Diff the new limbo docs with the old limbo docs. + NSMutableArray *changes = + [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)]; + [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + if (![self.limboDocuments containsObject:key]) { + [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved + key:key]]; + } + }]; + [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + if (![oldLimboDocuments containsObject:key]) { + [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded + key:key]]; + } + }]; + return changes; +} + +@end + +static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) { + switch (changeType) { + case FSTDocumentViewChangeTypeRemoved: + return 0; + case FSTDocumentViewChangeTypeAdded: + return 1; + case FSTDocumentViewChangeTypeModified: + return 2; + case FSTDocumentViewChangeTypeMetadata: + // A metadata change is converted to a modified change at the public API layer. Since we sort + // by document key and then change type, metadata and modified changes must be sorted + // equivalently. + return 2; + default: + FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType); + } +} + +static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, + FSTDocumentViewChangeType c2) { + int pos1 = DocumentViewChangeTypePosition(c1); + int pos2 = DocumentViewChangeTypePosition(c2); + if (pos1 == pos2) { + return NSOrderedSame; + } else if (pos1 < pos2) { + return NSOrderedAscending; + } else { + return NSOrderedDescending; + } +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.h b/Firestore/Source/Core/FSTViewSnapshot.h new file mode 100644 index 0000000..3db6108 --- /dev/null +++ b/Firestore/Source/Core/FSTViewSnapshot.h @@ -0,0 +1,117 @@ +/* + * 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 + +@class FSTDocument; +@class FSTQuery; +@class FSTDocumentSet; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDocumentViewChange + +/** + * The types of changes that can happen to a document with respect to a view. + * NOTE: We sort document changes by their type, so the ordering of this enum is significant. + */ +typedef NS_ENUM(NSInteger, FSTDocumentViewChangeType) { + FSTDocumentViewChangeTypeRemoved = 0, + FSTDocumentViewChangeTypeAdded, + FSTDocumentViewChangeTypeModified, + FSTDocumentViewChangeTypeMetadata, +}; + +/** A change to a single document's state within a view. */ +@interface FSTDocumentViewChange : NSObject + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type; + +/** The type of change for the document. */ +@property(nonatomic, assign, readonly) FSTDocumentViewChangeType type; +/** The document whose status changed. */ +@property(nonatomic, strong, readonly) FSTDocument *document; + +@end + +#pragma mark - FSTDocumentChangeSet + +/** The possibly states a document can be in w.r.t syncing from local storage to the backend. */ +typedef NS_ENUM(NSInteger, FSTSyncState) { + FSTSyncStateNone = 0, + FSTSyncStateLocal, + FSTSyncStateSynced, +}; + +/** A set of changes to documents with respect to a view. This set is mutable. */ +@interface FSTDocumentViewChangeSet : NSObject + +/** Returns a new empty change set. */ ++ (instancetype)changeSet; + +/** Takes a new change and applies it to the set. */ +- (void)addChange:(FSTDocumentViewChange *)change; + +/** Returns the set of all changes tracked in this set. */ +- (NSArray *)changes; + +@end + +#pragma mark - FSTViewSnapshot + +typedef void (^FSTViewSnapshotHandler)(FSTViewSnapshot *_Nullable snapshot, + NSError *_Nullable error); + +/** A view snapshot is an immutable capture of the results of a query and the changes to them. */ +@interface FSTViewSnapshot : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + documents:(FSTDocumentSet *)documents + oldDocuments:(FSTDocumentSet *)oldDocuments + documentChanges:(NSArray *)documentChanges + fromCache:(BOOL)fromCache + hasPendingWrites:(BOOL)hasPendingWrites + syncStateChanged:(BOOL)syncStateChanged NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The query this view is tracking the results for. */ +@property(nonatomic, strong, readonly) FSTQuery *query; + +/** The documents currently known to be results of the query. */ +@property(nonatomic, strong, readonly) FSTDocumentSet *documents; + +/** The documents of the last snapshot. */ +@property(nonatomic, strong, readonly) FSTDocumentSet *oldDocuments; + +/** The set of changes that have been applied to the documents. */ +@property(nonatomic, strong, readonly) NSArray *documentChanges; + +/** Whether any document in the snapshot was served from the local cache. */ +@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache; + +/** Whether any document in the snapshot has pending local writes. */ +@property(nonatomic, assign, readonly) BOOL hasPendingWrites; + +/** Whether the sync state changed as part of this snapshot. */ +@property(nonatomic, assign, readonly) BOOL syncStateChanged; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.m b/Firestore/Source/Core/FSTViewSnapshot.m new file mode 100644 index 0000000..016f890 --- /dev/null +++ b/Firestore/Source/Core/FSTViewSnapshot.m @@ -0,0 +1,231 @@ +/* + * 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 "FSTViewSnapshot.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTImmutableSortedDictionary.h" +#import "FSTQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDocumentViewChange + +@interface FSTDocumentViewChange () + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type; + +- (instancetype)initWithDocument:(FSTDocument *)document + type:(FSTDocumentViewChangeType)type NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTDocumentViewChange + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { + return [[FSTDocumentViewChange alloc] initWithDocument:document type:type]; +} + +- (instancetype)initWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { + self = [super init]; + if (self) { + _document = document; + _type = type; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTDocumentViewChange class]]) { + return NO; + } + FSTDocumentViewChange *otherChange = (FSTDocumentViewChange *)other; + return [self.document isEqual:otherChange.document] && self.type == otherChange.type; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", (long)self.type, self.document]; +} + +@end + +#pragma mark - FSTDocumentViewChangeSet + +@interface FSTDocumentViewChangeSet () + +/** The set of all changes tracked so far, with redundant changes merged. */ +@property(nonatomic, strong) + FSTImmutableSortedDictionary *changeMap; + +@end + +@implementation FSTDocumentViewChangeSet + ++ (instancetype)changeSet { + return [[FSTDocumentViewChangeSet alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _changeMap = [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + } + return self; +} + +- (NSString *)description { + return [self.changeMap description]; +} + +- (void)addChange:(FSTDocumentViewChange *)change { + FSTDocumentKey *key = change.document.key; + FSTDocumentViewChange *oldChange = [self.changeMap objectForKey:key]; + if (!oldChange) { + self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; + return; + } + + // Merge the new change with the existing change. + if (change.type != FSTDocumentViewChangeTypeAdded && + oldChange.type == FSTDocumentViewChangeTypeMetadata) { + self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; + + } else if (change.type == FSTDocumentViewChangeTypeMetadata && + oldChange.type != FSTDocumentViewChangeTypeRemoved) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document type:oldChange.type]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + + } else if (change.type == FSTDocumentViewChangeTypeModified && + oldChange.type == FSTDocumentViewChangeTypeModified) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeModified]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeModified && + oldChange.type == FSTDocumentViewChangeTypeAdded) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeAdded]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeRemoved && + oldChange.type == FSTDocumentViewChangeTypeAdded) { + self.changeMap = [self.changeMap dictionaryByRemovingObjectForKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeRemoved && + oldChange.type == FSTDocumentViewChangeTypeModified) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:oldChange.document + type:FSTDocumentViewChangeTypeRemoved]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeAdded && + oldChange.type == FSTDocumentViewChangeTypeRemoved) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeModified]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else { + // This includes these cases, which don't make sense: + // Added -> Added + // Removed -> Removed + // Modified -> Added + // Removed -> Modified + // Metadata -> Added + // Removed -> Metadata + FSTFail(@"Unsupported combination of changes: %ld after %ld", (long)change.type, + (long)oldChange.type); + } +} + +- (NSArray *)changes { + NSMutableArray *changes = [NSMutableArray array]; + [self.changeMap enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTDocumentViewChange *change, BOOL *stop) { + [changes addObject:change]; + }]; + return changes; +} + +@end + +#pragma mark - FSTViewSnapshot + +@implementation FSTViewSnapshot + +- (instancetype)initWithQuery:(FSTQuery *)query + documents:(FSTDocumentSet *)documents + oldDocuments:(FSTDocumentSet *)oldDocuments + documentChanges:(NSArray *)documentChanges + fromCache:(BOOL)fromCache + hasPendingWrites:(BOOL)hasPendingWrites + syncStateChanged:(BOOL)syncStateChanged { + self = [super init]; + if (self) { + _query = query; + _documents = documents; + _oldDocuments = oldDocuments; + _documentChanges = documentChanges; + _fromCache = fromCache; + _hasPendingWrites = hasPendingWrites; + _syncStateChanged = syncStateChanged; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat: + @"", + self.query, self.documents, self.oldDocuments, self.documentChanges, + (self.fromCache ? @"YES" : @"NO"), (self.hasPendingWrites ? @"YES" : @"NO"), + (self.syncStateChanged ? @"YES" : @"NO")]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } else if (![object isKindOfClass:[FSTViewSnapshot class]]) { + return NO; + } + + FSTViewSnapshot *other = object; + return [self.query isEqual:other.query] && [self.documents isEqual:other.documents] && + [self.oldDocuments isEqual:other.oldDocuments] && + [self.documentChanges isEqualToArray:other.documentChanges] && + self.fromCache == other.fromCache && self.hasPendingWrites == other.hasPendingWrites && + self.syncStateChanged == other.syncStateChanged; +} + +- (NSUInteger)hash { + NSUInteger result = [self.query hash]; + result = 31 * result + [self.documents hash]; + result = 31 * result + [self.oldDocuments hash]; + result = 31 * result + [self.documentChanges hash]; + result = 31 * result + (self.fromCache ? 1231 : 1237); + result = 31 * result + (self.hasPendingWrites ? 1231 : 1237); + result = 31 * result + (self.syncStateChanged ? 1231 : 1237); + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTDocumentReference.h b/Firestore/Source/Local/FSTDocumentReference.h new file mode 100644 index 0000000..eff60e4 --- /dev/null +++ b/Firestore/Source/Local/FSTDocumentReference.h @@ -0,0 +1,61 @@ +/* + * 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 + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * An immutable value used to keep track of an association between some referencing target or batch + * and a document key that the target or batch references. + * + * A reference can be from either listen targets (identified by their FSTTargetID) or mutation + * batches (identified by their FSTBatchID). See FSTGarbageCollector for more details. + * + * Not to be confused with FIRDocumentReference. + */ +@interface FSTDocumentReference : NSObject + +/** Initializes the document reference with the given key and ID. */ +- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The document key that's the target of this reference. */ +@property(nonatomic, strong, readonly) FSTDocumentKey *key; + +/** + * The targetID of a referring target or the batchID of a referring mutation batch. (Which this + * is depends upon which FSTReferenceSet this reference is a part of.) + */ +@property(nonatomic, assign, readonly) int ID; + +@end + +#pragma mark Comparators + +/** Sorts document references by key then ID. */ +extern const NSComparator FSTDocumentReferenceComparatorByKey; + +/** Sorts document references by ID then key. */ +extern const NSComparator FSTDocumentReferenceComparatorByID; + +/** A callback for use when enumerating an FSTImmutableSortedSet of FSTDocumentReferences. */ +typedef void (^FSTDocumentReferenceBlock)(FSTDocumentReference *reference, BOOL *stop); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTDocumentReference.m b/Firestore/Source/Local/FSTDocumentReference.m new file mode 100644 index 0000000..7d9e3db --- /dev/null +++ b/Firestore/Source/Local/FSTDocumentReference.m @@ -0,0 +1,83 @@ +/* + * 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 "FSTDocumentReference.h" + +#import "FSTComparison.h" +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTDocumentReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID { + self = [super init]; + if (self) { + _key = key; + _ID = ID; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + FSTDocumentReference *reference = (FSTDocumentReference *)other; + + return [self.key isEqualToKey:reference.key] && self.ID == reference.ID; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31u + self.ID; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.key, self.ID]; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + // FSTDocumentReference is immutable + return self; +} + +@end + +#pragma mark Comparators + +/** Sorts document references by key then ID. */ +const NSComparator FSTDocumentReferenceComparatorByKey = + ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { + NSComparisonResult result = FSTDocumentKeyComparator(left.key, right.key); + if (result != NSOrderedSame) { + return result; + } + return FSTCompareInts(left.ID, right.ID); + }; + +/** Sorts document references by ID then key. */ +const NSComparator FSTDocumentReferenceComparatorByID = + ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { + NSComparisonResult result = FSTCompareInts(left.ID, right.ID); + if (result != NSOrderedSame) { + return result; + } + return FSTDocumentKeyComparator(left.key, right.key); + }; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.h b/Firestore/Source/Local/FSTEagerGarbageCollector.h new file mode 100644 index 0000000..f2f373c --- /dev/null +++ b/Firestore/Source/Local/FSTEagerGarbageCollector.h @@ -0,0 +1,36 @@ +/* + * 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 + +#import "FSTGarbageCollector.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A garbage collector implementation that eagerly collects documents as soon as they're no longer + * referenced in any of its registered FSTGarbageSources. + * + * This implementation keeps track of a set of keys that are potentially garbage without keeping + * an exact reference count. During -collectGarbage, the collector verifies that all potential + * garbage keys actually have no references by consulting its list of garbage sources. + */ +@interface FSTEagerGarbageCollector : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.m b/Firestore/Source/Local/FSTEagerGarbageCollector.m new file mode 100644 index 0000000..971f368 --- /dev/null +++ b/Firestore/Source/Local/FSTEagerGarbageCollector.m @@ -0,0 +1,89 @@ +/* + * 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 "FSTEagerGarbageCollector.h" + +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMultiReferenceSet + +@interface FSTEagerGarbageCollector () + +/** The garbage collectible sources to double-check during garbage collection. */ +@property(nonatomic, strong, readonly) NSMutableArray> *sources; + +/** A set of potentially garbage keys. */ +@property(nonatomic, strong, readonly) NSMutableSet *potentialGarbage; + +@end + +@implementation FSTEagerGarbageCollector + +- (instancetype)init { + self = [super init]; + if (self) { + _sources = [NSMutableArray array]; + _potentialGarbage = [[NSMutableSet alloc] init]; + } + return self; +} + +- (BOOL)isEager { + return YES; +} + +- (void)addGarbageSource:(id)garbageSource { + [self.sources addObject:garbageSource]; + garbageSource.garbageCollector = self; +} + +- (void)removeGarbageSource:(id)garbageSource { + [self.sources removeObject:garbageSource]; + garbageSource.garbageCollector = nil; +} + +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { + [self.potentialGarbage addObject:key]; +} + +- (NSMutableSet *)collectGarbage { + NSMutableArray> *sources = self.sources; + + NSMutableSet *actualGarbage = [NSMutableSet set]; + for (FSTDocumentKey *key in self.potentialGarbage) { + BOOL isGarbage = YES; + for (id source in sources) { + if ([source containsKey:key]) { + isGarbage = NO; + break; + } + } + + if (isGarbage) { + [actualGarbage addObject:key]; + } + } + + // Clear locally retained potential keys and returned confirmed garbage. + [self.potentialGarbage removeAllObjects]; + return actualGarbage; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTGarbageCollector.h b/Firestore/Source/Local/FSTGarbageCollector.h new file mode 100644 index 0000000..c999f66 --- /dev/null +++ b/Firestore/Source/Local/FSTGarbageCollector.h @@ -0,0 +1,95 @@ +/* + * 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 + +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTDocumentReference; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A pseudo-collection that maintains references to documents. FSTGarbageSource collections + * notify the FSTGarbageCollector when references to documents change through the + * -addPotentialGarbageKey: message. + */ +@protocol FSTGarbageSource + +/** + * The garbage collector to which this collection should send -addPotentialGarbageKey: messages. + */ +@property(nonatomic, weak, readwrite, nullable) id garbageCollector; + +/** + * Checks to see if there are any references to a document with the given key. This can be used by + * garbage collectors to double-check if a key exists in this collection when it was released + * elsewhere. + */ +- (BOOL)containsKey:(FSTDocumentKey *)key; + +@end + +/** + * Tracks different kinds of references to a document, for all the different ways the client + * needs to retain a document. + * + * Usually the local store this means tracking of three different types of references to a + * document: + * 1. RemoteTarget reference identified by a target ID. + * 2. LocalView reference identified also by a target ID. + * 3. Local mutation reference identified by a batch ID. + * + * The idea is that we want to keep a document around at least as long as any remote target or + * local (latency compensated) view is referencing it, or there's an outstanding local mutation to + * that document. + */ +@protocol FSTGarbageCollector + +/** + * A property that describes whether or not the collector wants to eagerly collect keys. + * + * TODO(b/33384523) Delegate deleting released queries to the GC. + * This flag is a temporary workaround for dealing with a persistent query cache. The collector + * really should have an API for releasing queries that does the right thing for its policy. + */ +@property(nonatomic, assign, readonly, getter=isEager) BOOL eager; + +/** Adds a garbage source to the collector. */ +- (void)addGarbageSource:(id)garbageSource; + +/** Removes a garbage source from the collector. */ +- (void)removeGarbageSource:(id)garbageSource; + +/** + * Notifies the garbage collector that a document with the given key may have become garbage. + * + * This is useful in both when a document has definitely been released (for example when removed + * from a garbage source) but also when a document has been updated. Documents should be marked in + * this way because the client accepts updates for documents even after the document no longer + * matches any active targets. This behavior allows the client to avoid re-showing an old document + * in the next latency-compensated view. + */ +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key; + +/** Returns the contents of the garbage bin and clears it. */ +- (NSSet *)collectGarbage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.h b/Firestore/Source/Local/FSTLevelDB.h new file mode 100644 index 0000000..a2c838d --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDB.h @@ -0,0 +1,105 @@ +/* + * 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 + +#import "FSTPersistence.h" + +#ifdef __cplusplus +#include + +namespace leveldb { +class DB; +class Status; +} +#endif + +@class FSTDatabaseInfo; +@class FSTLocalSerializer; + +NS_ASSUME_NONNULL_BEGIN + +/** A LevelDB-backed instance of FSTPersistence. */ +// TODO(mikelehen): Rename to FSTLevelDBPersistence. +@interface FSTLevelDB : NSObject + +/** + * Initializes the LevelDB in the given directory. Note that all expensive startup work including + * opening any database files is deferred until -[FSTPersistence start] is called. + */ +- (instancetype)initWithDirectory:(NSString *)directory + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __attribute__((unavailable("Use -initWithDirectory: instead."))); + +/** Finds a suitable directory to serve as the root of all Firestore local storage. */ ++ (NSString *)documentsDirectory; + +/** + * Computes a unique storage directory for the given identifying components of local storage. + * + * @param databaseInfo The identifying information for the local storage instance. + * @param documentsDirectory The root document directory relative to which the storage directory + * will be created. Usually just +[FSTLevelDB documentsDir]. + * @return A storage directory unique to the instance identified by databaseInfo. + */ ++ (NSString *)storageDirectoryForDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + documentsDirectory:(NSString *)documentsDirectory; + +/** + * Starts LevelDB-backed persistent storage by opening the database files, creating the DB if it + * does not exist. + * + * The leveldb directory is created relative to the appropriate document storage directory for the + * platform: NSDocumentDirectory on iOS or $HOME/.firestore on macOS. + */ +- (BOOL)start:(NSError **)error; + +#ifdef __cplusplus +// What follows is the Objective-C++ extension to the API. + +/** + * Creates an NSError based on the given status if the status is not ok. + * + * @param status The status of the preceding LevelDB operation. + * @param description A printf-style format string describing what kind of failure happened if + * @a status is not ok. Additional parameters are substituted into the placeholders in this + * format string. + * + * @return An NSError with its localizedDescription composed from the description format and its + * localizedFailureReason composed from any error message embedded in @a status. + */ ++ (nullable NSError *)errorWithStatus:(leveldb::Status)status + description:(NSString *)description, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * Converts the given @a status to an NSString describing the status condition, suitable for + * logging or inclusion in an NSError. + * + * @param status The status of the preceding LevelDB operation. + * + * @return An NSString describing the status (even if the status was ok). + */ ++ (NSString *)descriptionOfStatus:(leveldb::Status)status; + +/** The native db pointer, allocated during start. */ +@property(nonatomic, assign, readonly) std::shared_ptr ptr; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm new file mode 100644 index 0000000..81e1064 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -0,0 +1,246 @@ +/* + * 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 "FSTLevelDB.h" + +#include + +#import "FIRFirestoreErrors.h" +#import "FSTAssert.h" +#import "FSTDatabaseID.h" +#import "FSTDatabaseInfo.h" +#import "FSTLevelDBMutationQueue.h" +#import "FSTLevelDBQueryCache.h" +#import "FSTLevelDBRemoteDocumentCache.h" +#import "FSTLogger.h" +#import "FSTSerializerBeta.h" +#import "FSTWriteGroup.h" +#import "FSTWriteGroupTracker.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kReservedPathComponent = @"firestore"; + +using leveldb::DB; +using leveldb::Options; +using leveldb::Status; +using leveldb::WriteOptions; + +@interface FSTLevelDB () + +@property(nonatomic, copy) NSString *directory; +@property(nonatomic, strong) FSTWriteGroupTracker *writeGroupTracker; +@property(nonatomic, assign, getter=isStarted) BOOL started; +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +@implementation FSTLevelDB + +- (instancetype)initWithDirectory:(NSString *)directory + serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + _directory = [directory copy]; + _writeGroupTracker = [FSTWriteGroupTracker tracker]; + _serializer = serializer; + } + return self; +} + ++ (NSString *)documentsDirectory { +#if TARGET_OS_IPHONE + NSArray *directories = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + return [directories[0] stringByAppendingPathComponent:kReservedPathComponent]; + +#elif TARGET_OS_MAC + NSString *dotPrefixed = [@"." stringByAppendingString:kReservedPathComponent]; + return [NSHomeDirectory() stringByAppendingPathComponent:dotPrefixed]; + +#else +#error "local storage on tvOS" +// TODO(mcg): Writing to NSDocumentsDirectory on tvOS will fail; we need to write to Caches +// https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/ + +#endif +} + ++ (NSString *)storageDirectoryForDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + documentsDirectory:(NSString *)documentsDirectory { + // Use two different path formats: + // + // * persistenceKey / projectID . databaseID / name + // * persistenceKey / projectID / name + // + // projectIDs are DNS-compatible names and cannot contain dots so there's + // no danger of collisions. + NSString *directory = documentsDirectory; + directory = [directory stringByAppendingPathComponent:databaseInfo.persistenceKey]; + + NSString *segment = databaseInfo.databaseID.projectID; + if (![databaseInfo.databaseID isDefaultDatabase]) { + segment = [NSString stringWithFormat:@"%@.%@", segment, databaseInfo.databaseID.databaseID]; + } + directory = [directory stringByAppendingPathComponent:segment]; + + // Reserve one additional path component to allow multiple physical databases + directory = [directory stringByAppendingPathComponent:@"main"]; + return directory; +} + +#pragma mark - Startup + +- (BOOL)start:(NSError **)error { + FSTAssert(!self.isStarted, @"FSTLevelDB double-started!"); + self.started = YES; + NSString *directory = self.directory; + if (![self ensureDirectory:directory error:error]) { + return NO; + } + + DB *database = [self createDBWithDirectory:directory error:error]; + if (!database) { + return NO; + } + + _ptr.reset(database); + return YES; +} + +/** Creates the directory at @a directory and marks it as excluded from iCloud backup. */ +- (BOOL)ensureDirectory:(NSString *)directory error:(NSError **)error { + NSError *localError; + NSFileManager *files = [NSFileManager defaultManager]; + + BOOL success = [files createDirectoryAtPath:directory + withIntermediateDirectories:YES + attributes:nil + error:&localError]; + if (!success) { + *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:@{ + NSLocalizedDescriptionKey : @"Failed to create persistence directory", + NSUnderlyingErrorKey : localError + }]; + return NO; + } + + NSURL *dirURL = [NSURL fileURLWithPath:directory]; + success = [dirURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&localError]; + if (!success) { + *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed mark persistence directory as excluded from backups", + NSUnderlyingErrorKey : localError + }]; + return NO; + } + + return YES; +} + +/** Opens the database within the given directory. */ +- (nullable DB *)createDBWithDirectory:(NSString *)directory error:(NSError **)error { + Options options; + options.create_if_missing = true; + + DB *database; + Status status = DB::Open(options, [directory UTF8String], &database); + if (!status.ok()) { + if (error) { + NSString *name = [directory lastPathComponent]; + *error = + [FSTLevelDB errorWithStatus:status + description:@"Failed to create database %@ at path %@", name, directory]; + } + return nullptr; + } + + return database; +} + +#pragma mark - Persistence Factory methods + +- (id)mutationQueueForUser:(FSTUser *)user { + return [FSTLevelDBMutationQueue mutationQueueWithUser:user db:_ptr serializer:self.serializer]; +} + +- (id)queryCache { + return [[FSTLevelDBQueryCache alloc] initWithDB:_ptr serializer:self.serializer]; +} + +- (id)remoteDocumentCache { + return [[FSTLevelDBRemoteDocumentCache alloc] initWithDB:_ptr serializer:self.serializer]; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + return [self.writeGroupTracker startGroupWithAction:action]; +} + +- (void)commitGroup:(FSTWriteGroup *)group { + [self.writeGroupTracker endGroup:group]; + + NSString *description = [group description]; + FSTLog(@"Committing %@", description); + + Status status = [group writeToDB:_ptr]; + if (!status.ok()) { + FSTFail(@"%@ failed with status: %s, description: %@", group.action, status.ToString().c_str(), + description); + } +} + +- (void)shutdown { + FSTAssert(self.isStarted, @"FSTLevelDB shutdown without start!"); + self.started = NO; + _ptr.reset(); +} + +#pragma mark - Error and Status + ++ (nullable NSError *)errorWithStatus:(Status)status description:(NSString *)description, ... { + if (status.ok()) { + return nil; + } + + va_list args; + va_start(args, description); + + NSString *message = [[NSString alloc] initWithFormat:description arguments:args]; + NSString *reason = [self descriptionOfStatus:status]; + NSError *result = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:@{ + NSLocalizedDescriptionKey : message, + NSLocalizedFailureReasonErrorKey : reason + }]; + + va_end(args); + + return result; +} + ++ (NSString *)descriptionOfStatus:(Status)status { + return [NSString stringWithCString:status.ToString().c_str() encoding:NSUTF8StringEncoding]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBKey.h b/Firestore/Source/Local/FSTLevelDBKey.h new file mode 100644 index 0000000..bad7829 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBKey.h @@ -0,0 +1,344 @@ +/* + * 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 __cplusplus +#error "FSTLevelDBKey is Objective-C++ and can only be included from .mm files" +#endif + +#import + +#import "FSTTypes.h" + +#import "StringView.h" + +@class FSTDocumentKey; +@class FSTResourcePath; + +NS_ASSUME_NONNULL_BEGIN + +// All leveldb logical tables should have their keys structures described in this file. +// +// mutations: +// - tableName: string = "mutation" +// - userID: string +// - batchID: FSTBatchID +// +// document_mutations: +// - tableName: string = "document_mutation" +// - userID: string +// - path: FSTResourcePath +// - batchID: FSTBatchID +// +// mutation_queues: +// - tableName: string = "mutation_queue" +// - userID: string +// +// targets: +// - tableName: string = "target" +// - targetId: FSTTargetID +// +// target_globals: +// - tableName: string = "target_global" +// +// query_targets: +// - tableName: string = "query_target" +// - canonicalID: string +// - targetId: FSTTargetID +// +// target_documents: +// - tableName: string = "target_document" +// - targetID: FSTTargetID +// - path: FSTResourcePath +// +// document_targets: +// - tableName: string = "document_target" +// - path: FSTResourcePath +// - targetID: FSTTargetID +// +// remote_documents: +// - tableName: string = "remote_document" +// - path: FSTResourcePath + +/** Helpers for any LevelDB key. */ +@interface FSTLevelDBKey : NSObject + +/** + * Parses the given key and returns a human readable description of its contents, suitable for + * error messages and logging. + */ ++ (NSString *)descriptionForKey:(Firestore::StringView)key; + +@end + +/** A key in the mutations table. */ +@interface FSTLevelDBMutationKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a key prefix that points just before the first key for the given userID. */ ++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID; + +/** Creates a complete key that points to a specific userID and batchID. */ ++ (std::string)keyWithUserID:(Firestore::StringView)userID batchID:(FSTBatchID)batchID; + +/** + * Decodes the given complete key, storing the decoded values as properties of the receiver. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The user that owns the mutation batches. */ +@property(nonatomic, assign, readonly) const std::string &userID; + +/** The batchID of the batch. */ +@property(nonatomic, assign, readonly) FSTBatchID batchID; + +@end + +/** + * A key in the document mutations index, which stores the batches in which documents are mutated. + */ +@interface FSTLevelDBDocumentMutationKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a key prefix that points just before the first key for the given userID. */ ++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID; + +/** + * Creates a key prefix that points just before the first key for the userID and resource path. + * + * Note that this uses an FSTResourcePath rather than an FSTDocumentKey in order to allow prefix + * scans over a collection. However a naive scan over those results isn't useful since it would + * match both immediate children of the collection and any subcollections. + */ ++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID + resourcePath:(FSTResourcePath *)resourcePath; + +/** Creates a complete key that points to a specific userID, document key, and batchID. */ ++ (std::string)keyWithUserID:(Firestore::StringView)userID + documentKey:(FSTDocumentKey *)documentKey + batchID:(FSTBatchID)batchID; + +/** + * Decodes the given complete key, storing the decoded values as properties of the receiver. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The user that owns the mutation batches. */ +@property(nonatomic, assign, readonly) const std::string &userID; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +/** The batchID in which the document participates. */ +@property(nonatomic, assign, readonly) FSTBatchID batchID; + +@end + +/** + * A key in the mutation_queues table. + * + * Note that where mutation_queues contains one row about each queue, mutations contains the actual + * mutation batches themselves. + */ +@interface FSTLevelDBMutationQueueKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a complete key that points to a specific mutation queue entry for the given userID. */ ++ (std::string)keyWithUserID:(Firestore::StringView)userID; + +/** + * Decodes the given complete key, storing the decoded values as properties of the receiver. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +@property(nonatomic, assign, readonly) const std::string &userID; + +@end + +/** A key in the target globals table, a record of global values across all targets. */ +@interface FSTLevelDBTargetGlobalKey : NSObject + +/** Creates a key that points to the single target global row. */ ++ (std::string)key; + +/** + * Decodes the contents of a target global key, essentially just verifying that the key has the + * correct table name. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +@end + +/** A key in the targets table. */ +@interface FSTLevelDBTargetKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a complete key that points to a specific target, by targetID. */ ++ (std::string)keyWithTargetID:(FSTTargetID)targetID; + +/** + * Decodes the contents of a target key into properties on this instance. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +@end + +/** + * A key in the query targets table, an index of canonicalIDs to the targets they may match. This + * is not a unique mapping because canonicalID does not promise a unique name for all possible + * queries. + */ +@interface FSTLevelDBQueryTargetKey : NSObject + +/** + * Creates a key that contains just the query targets table prefix and points just before the + * first key. + */ ++ (std::string)keyPrefix; + +/** Creates a key that points to the first query-target association for a canonicalID. */ ++ (std::string)keyPrefixWithCanonicalID:(Firestore::StringView)canonicalID; + +/** Creates a key that points to a specific query-target entry. */ ++ (std::string)keyWithCanonicalID:(Firestore::StringView)canonicalID targetID:(FSTTargetID)targetID; + +/** Decodes the contents of a query target key into properties on this instance. */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The canonicalID derived from the query. */ +@property(nonatomic, assign, readonly) const std::string &canonicalID; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +@end + +/** + * A key in the target documents table, an index of targetIDs to the documents they contain. + */ +@interface FSTLevelDBTargetDocumentKey : NSObject + +/** + * Creates a key that contains just the target documents table prefix and points just before the + * first key. + */ ++ (std::string)keyPrefix; + +/** Creates a key that points to the first target-document association for a targetID. */ ++ (std::string)keyPrefixWithTargetID:(FSTTargetID)targetID; + +/** Creates a key that points to a specific target-document entry. */ ++ (std::string)keyWithTargetID:(FSTTargetID)targetID documentKey:(FSTDocumentKey *)documentKey; + +/** Decodes the contents of a target document key into properties on this instance. */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +@end + +/** + * A key in the document targets table, an index from documents to the targets that contain them. + */ +@interface FSTLevelDBDocumentTargetKey : NSObject + +/** + * Creates a key that contains just the document targets table prefix and points just before the + * first key. + */ ++ (std::string)keyPrefix; + +/** Creates a key that points to the first document-target association for document. */ ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath; + +/** Creates a key that points to a specific document-target entry. */ ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)documentKey targetID:(FSTTargetID)targetID; + +/** Decodes the contents of a document target key into properties on this instance. */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +@end + +/** A key in the remote documents table. */ +@interface FSTLevelDBRemoteDocumentKey : NSObject + +/** + * Creates a key that contains just the remote documents table prefix and points just before the + * first remote document key. + */ ++ (std::string)keyPrefix; + +/** + * Creates a complete key that points to a specific document. The documentKey must have an even + * number of path segments. + */ ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)key; + +/** + * Creates a key prefix that contains a part of a document path. Odd numbers of segments create a + * collection key prefix, while an even number of segments create a document key prefix. Note that + * a document key prefix will match the document itself and any documents that exist in its + * subcollections. + */ ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath; + +/** + * Decodes the contents of a remote document key into properties on this instance. This can only + * decode complete document paths (i.e. the result of +keyWithDocumentKey:). + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBKey.mm b/Firestore/Source/Local/FSTLevelDBKey.mm new file mode 100644 index 0000000..ee3e270 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBKey.mm @@ -0,0 +1,757 @@ +/* + * 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 "FSTLevelDBKey.h" + +#include + +#include "ordered_code.h" +#include "string_util.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using Firestore::PrefixSuccessor; +using Firestore::StringView; +using leveldb::Slice; + +static const char *kMutationsTable = "mutation"; +static const char *kDocumentMutationsTable = "document_mutation"; +static const char *kMutationQueuesTable = "mutation_queue"; +static const char *kTargetGlobalTable = "target_global"; +static const char *kTargetsTable = "target"; +static const char *kQueryTargetsTable = "query_target"; +static const char *kTargetDocumentsTable = "target_document"; +static const char *kDocumentTargetsTable = "document_target"; +static const char *kRemoteDocumentsTable = "remote_document"; + +/** + * Labels for the components of keys. These serve to make keys self-describing. + * + * These are intended to sort similarly to keys in the server storage format. + * + * Note that the server writes component labels using the equivalent to + * OrderedCode::WriteSignedNumDecreasing. This means that despite the higher numeric value, a + * terminator sorts before a path segment. In order to avoid needing the WriteSignedNumDecreasing + * code just for these values, this enum's values are in the reverse order to the server side. + * + * Most server-side values don't apply here. For example, the server embeds projects, databases, + * namespaces and similar values in its entity keys where the clients just open a different + * leveldb. Similarly, many of these values don't apply to the server since the server is backed + * by spanner which natively has concepts of tables and indexes. Where there's overlap, a comment + * denotes the server value from the storage_format_internal.proto. + */ +typedef NS_ENUM(int64_t, FSTComponentLabel) { + /** + * A terminator is the final component of a key. All complete keys have a terminator and a key + * is known to be a key prefix if it doesn't have a terminator. + */ + FSTComponentLabelTerminator = 0, // TERMINATOR_COMPONENT = 63, server-side + + /** A table name component names the logical table to which the key belongs. */ + FSTComponentLabelTableName = 5, + + /** A component containing the batch ID of a mutation. */ + FSTComponentLabelBatchID = 10, + + /** A component containing the canonical ID of a query. */ + FSTComponentLabelCanonicalID = 11, + + /** A component containing the target ID of a query. */ + FSTComponentLabelTargetID = 12, + + /** A component containing a user ID. */ + FSTComponentLabelUserID = 13, + + /** + * A path segment describes just a single segment in a resource path. Path segments that occur + * sequentially in a key represent successive segments in a single path. + * + * This value must be greater than FSTComponentLabelTerminator to ensure that longer paths sort + * after paths that are prefixes of them. + * + * This value must also be larger than other separators so that path suffixes sort after other + * key components. + */ + FSTComponentLabelPathSegment = 62, // PATH = 60, server-side + + /** The maximum value that can be encoded by WriteSignedNumIncreasing in a single byte. */ + FSTComponentLabelUnknown = 63, +}; + +namespace { + +/** Writes a component label to the given key destination. */ +void WriteComponentLabel(std::string *dest, FSTComponentLabel label) { + OrderedCode::WriteSignedNumIncreasing(dest, label); +} + +/** + * Reads a component label from the given key contents. + * + * If the read is unsuccessful, returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * label will be set to the decoded label value. + */ +BOOL ReadComponentLabel(leveldb::Slice *contents, FSTComponentLabel *label) { + int64_t rawResult = 0; + Slice tmp = *contents; + if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) { + if (rawResult >= FSTComponentLabelTerminator && rawResult <= FSTComponentLabelUnknown) { + *label = static_cast(rawResult); + *contents = tmp; + return YES; + } + } + return NO; +} + +/** + * Reads a component label from the given key contents. + * + * If the read is unsuccessful or if the read was successful but the label that was read did not + * match the expectedLabel returns NO and changes none of its arguments. + * + * If the read is successful, returns YES and contents will be updated to the next unread byte. + */ +BOOL ReadComponentLabelMatching(Slice *contents, FSTComponentLabel expectedLabel) { + int64_t rawResult = 0; + Slice tmp = *contents; + if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) { + if (rawResult == expectedLabel) { + *contents = tmp; + return YES; + } + } + return NO; +} + +/** + * Reads a signed number from the given key contents and verifies that the value fits in a 32-bit + * integer. + * + * If the read is unsuccessful or the number that was read was out of bounds for an int32_t, + * returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * result will be set to the decoded integer value. + */ +BOOL ReadInt32(Slice *contents, int32_t *result) { + int64_t rawResult = 0; + Slice tmp = *contents; + if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) { + if (rawResult >= INT32_MIN && rawResult <= INT32_MAX) { + *contents = tmp; + *result = static_cast(rawResult); + return YES; + } + } + return NO; +} + +/** Writes a component label and a signed integer to the given key destination. */ +void WriteLabeledInt32(std::string *dest, FSTComponentLabel label, int32_t value) { + WriteComponentLabel(dest, label); + OrderedCode::WriteSignedNumIncreasing(dest, value); +} + +/** + * Reads a component label and signed number from the given key contents and verifies that the + * label matches the expectedLabel and the value fits in a 32-bit integer. + * + * If the read is unsuccessful, the label didn't match, or the number that was read was out of + * bounds for an int32_t, returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * value will be set to the decoded integer value. + */ +BOOL ReadLabeledInt32(Slice *contents, FSTComponentLabel expectedLabel, int32_t *value) { + Slice tmp = *contents; + if (ReadComponentLabelMatching(&tmp, expectedLabel)) { + if (ReadInt32(&tmp, value)) { + *contents = tmp; + return YES; + } + } + return NO; +} + +/** Writes a component label and an encoded string to the given key destination. */ +void WriteLabeledString(std::string *dest, FSTComponentLabel label, StringView value) { + WriteComponentLabel(dest, label); + OrderedCode::WriteString(dest, value); +} + +/** + * Reads a component label and a string from the given key contents and verifies that the label + * matches the expectedLabel. + * + * If the read is unsuccessful or the label didn't match, returns NO, and changes none of its + * arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * value will be set to the decoded string value. + */ +BOOL ReadLabeledString(Slice *contents, FSTComponentLabel expectedLabel, std::string *value) { + Slice tmp = *contents; + if (ReadComponentLabelMatching(&tmp, expectedLabel)) { + if (OrderedCode::ReadString(&tmp, value)) { + *contents = tmp; + return YES; + } + } + + return NO; +} + +/** + * Reads a component label and a string from the given key contents and verifies that the label + * matches the expectedLabel and the string matches the expectedValue. + * + * If the read is unsuccessful, the label or didn't match, or the string value didn't match, + * returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte. + */ +BOOL ReadLabeledStringMatching(Slice *contents, + FSTComponentLabel expectedLabel, + const char *expectedValue) { + std::string value; + Slice tmp = *contents; + if (ReadLabeledString(&tmp, expectedLabel, &value)) { + if (value == expectedValue) { + *contents = tmp; + return YES; + } + } + + return NO; +} + +/** + * For each segment in the given resource path writes an FSTComponentLabelPathSegment component + * label and a string containing the path segment. + */ +void WriteResourcePath(std::string *dest, FSTResourcePath *path) { + for (int i = 0; i < path.length; i++) { + WriteComponentLabel(dest, FSTComponentLabelPathSegment); + OrderedCode::WriteString(dest, StringView([path segmentAtIndex:i])); + } +} + +/** + * Reads component labels and strings from the given key contents until it finds a component label + * other that FSTComponentLabelPathSegment. All matched path segments are assembled into a resource + * path and wrapped in an FSTDocumentKey. + * + * If the read is unsuccessful or the document key is invalid, returns NO, and changes none of its + * arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * value will be set to the decoded document key. + */ +BOOL ReadDocumentKey(Slice *contents, FSTDocumentKey *__strong *result) { + Slice completeSegments = *contents; + + std::string segment; + NSMutableArray *pathSegments = [NSMutableArray array]; + for (;;) { + // Advance a temporary slice to avoid advancing contents into the next key component which may + // not be a path segment. + Slice readPosition = completeSegments; + if (!ReadComponentLabelMatching(&readPosition, FSTComponentLabelPathSegment)) { + break; + } + if (!OrderedCode::ReadString(&readPosition, &segment)) { + return NO; + } + + NSString *pathSegment = [[NSString alloc] initWithUTF8String:segment.c_str()]; + [pathSegments addObject:pathSegment]; + segment.clear(); + + completeSegments = readPosition; + } + + FSTResourcePath *path = [FSTResourcePath pathWithSegments:pathSegments]; + if (path.length > 0 && [FSTDocumentKey isDocumentKey:path]) { + *contents = completeSegments; + *result = [FSTDocumentKey keyWithPath:path]; + return YES; + } + + return NO; +} + +// Trivial shortcuts that make reading and writing components type-safe. + +inline void WriteTerminator(std::string *dest) { + OrderedCode::WriteSignedNumIncreasing(dest, FSTComponentLabelTerminator); +} + +inline BOOL ReadTerminator(Slice *contents) { + return ReadComponentLabelMatching(contents, FSTComponentLabelTerminator); +} + +inline void WriteTableName(std::string *dest, const char *tableName) { + WriteLabeledString(dest, FSTComponentLabelTableName, tableName); +} + +inline BOOL ReadTableNameMatching(Slice *contents, const char *expectedTableName) { + return ReadLabeledStringMatching(contents, FSTComponentLabelTableName, expectedTableName); +} + +inline void WriteBatchID(std::string *dest, FSTBatchID batchID) { + WriteLabeledInt32(dest, FSTComponentLabelBatchID, batchID); +} + +inline BOOL ReadBatchID(Slice *contents, FSTBatchID *batchID) { + return ReadLabeledInt32(contents, FSTComponentLabelBatchID, batchID); +} + +inline void WriteCanonicalID(std::string *dest, StringView canonicalID) { + WriteLabeledString(dest, FSTComponentLabelCanonicalID, canonicalID); +} + +inline BOOL ReadCanonicalID(Slice *contents, std::string *canonicalID) { + return ReadLabeledString(contents, FSTComponentLabelCanonicalID, canonicalID); +} + +inline void WriteTargetID(std::string *dest, FSTTargetID targetID) { + WriteLabeledInt32(dest, FSTComponentLabelTargetID, targetID); +} + +inline BOOL ReadTargetID(Slice *contents, FSTTargetID *targetID) { + return ReadLabeledInt32(contents, FSTComponentLabelTargetID, targetID); +} + +inline void WriteUserID(std::string *dest, StringView userID) { + WriteLabeledString(dest, FSTComponentLabelUserID, userID); +} + +inline BOOL ReadUserID(Slice *contents, std::string *userID) { + return ReadLabeledString(contents, FSTComponentLabelUserID, userID); +} + +/** Returns a base64-encoded string for an invalid key, used for debug-friendly description text. */ +NSString *InvalidKey(const Slice &key) { + NSData *keyData = + [[NSData alloc] initWithBytesNoCopy:(void *)key.data() length:key.size() freeWhenDone:NO]; + return [keyData base64EncodedStringWithOptions:0]; +} + +} // namespace + +@implementation FSTLevelDBKey + ++ (NSString *)descriptionForKey:(StringView)key { + Slice contents = key; + BOOL isTerminated = NO; + + NSMutableString *description = [NSMutableString string]; + [description appendString:@"["]; + while (contents.size() > 0) { + Slice tmp = contents; + FSTComponentLabel label = FSTComponentLabelUnknown; + if (!ReadComponentLabel(&tmp, &label)) { + break; + } + + if (label == FSTComponentLabelTerminator) { + isTerminated = YES; + contents = tmp; + break; + } + + // Reset tmp since all the different read routines expect to see the separator first + tmp = contents; + + if (label == FSTComponentLabelPathSegment) { + FSTDocumentKey *documentKey = nil; + if (!ReadDocumentKey(&tmp, &documentKey)) { + break; + } + [description appendFormat:@" key=%@", [documentKey.path description]]; + + } else if (label == FSTComponentLabelTableName) { + std::string table; + if (!ReadLabeledString(&tmp, FSTComponentLabelTableName, &table)) { + break; + } + [description appendFormat:@"%s:", table.c_str()]; + + } else if (label == FSTComponentLabelBatchID) { + FSTBatchID batchID; + if (!ReadBatchID(&tmp, &batchID)) { + break; + } + [description appendFormat:@" batchID=%d", batchID]; + + } else if (label == FSTComponentLabelCanonicalID) { + std::string canonicalID; + if (!ReadCanonicalID(&tmp, &canonicalID)) { + break; + } + [description appendFormat:@" canonicalID=%s", canonicalID.c_str()]; + + } else if (label == FSTComponentLabelTargetID) { + FSTTargetID targetID; + if (!ReadTargetID(&tmp, &targetID)) { + break; + } + [description appendFormat:@" targetID=%d", targetID]; + + } else if (label == FSTComponentLabelUserID) { + std::string userID; + if (!ReadUserID(&tmp, &userID)) { + break; + } + [description appendFormat:@" userID=%s", userID.c_str()]; + + } else { + [description appendFormat:@" unknown label=%d", (int)label]; + break; + } + + contents = tmp; + } + + if (contents.size() > 0) { + [description appendFormat:@" invalid key=<%@>", InvalidKey(key)]; + + } else if (!isTerminated) { + [description appendFormat:@" incomplete key"]; + } + + [description appendString:@"]"]; + return description; +} + +@end + +@implementation FSTLevelDBMutationKey { + std::string _userID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kMutationsTable); + return result; +} + ++ (std::string)keyPrefixWithUserID:(StringView)userID { + std::string result; + WriteTableName(&result, kMutationsTable); + WriteUserID(&result, userID); + return result; +} + ++ (std::string)keyWithUserID:(StringView)userID batchID:(FSTBatchID)batchID { + std::string result; + WriteTableName(&result, kMutationsTable); + WriteUserID(&result, userID); + WriteBatchID(&result, batchID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)userID { + return _userID; +} + +- (BOOL)decodeKey:(StringView)key { + _userID.clear(); + + Slice contents = key; + return ReadTableNameMatching(&contents, kMutationsTable) && ReadUserID(&contents, &_userID) && + ReadBatchID(&contents, &_batchID) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBDocumentMutationKey { + std::string _userID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + return result; +} + ++ (std::string)keyPrefixWithUserID:(StringView)userID { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + WriteUserID(&result, userID); + return result; +} + ++ (std::string)keyPrefixWithUserID:(StringView)userID resourcePath:(FSTResourcePath *)resourcePath { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + WriteUserID(&result, userID); + WriteResourcePath(&result, resourcePath); + return result; +} + ++ (std::string)keyWithUserID:(StringView)userID + documentKey:(FSTDocumentKey *)documentKey + batchID:(FSTBatchID)batchID { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + WriteUserID(&result, userID); + WriteResourcePath(&result, documentKey.path); + WriteBatchID(&result, batchID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)userID { + return _userID; +} + +- (BOOL)decodeKey:(StringView)key { + _userID.clear(); + _documentKey = nil; + + Slice contents = key; + return ReadTableNameMatching(&contents, kDocumentMutationsTable) && + ReadUserID(&contents, &_userID) && ReadDocumentKey(&contents, &_documentKey) && + ReadBatchID(&contents, &_batchID) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBMutationQueueKey { + std::string _userID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kMutationQueuesTable); + return result; +} + ++ (std::string)keyWithUserID:(StringView)userID { + std::string result; + WriteTableName(&result, kMutationQueuesTable); + WriteUserID(&result, userID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)userID { + return _userID; +} + +- (BOOL)decodeKey:(StringView)key { + _userID.clear(); + + Slice contents = key; + return ReadTableNameMatching(&contents, kMutationQueuesTable) && + ReadUserID(&contents, &_userID) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBTargetGlobalKey + ++ (std::string)key { + std::string result; + WriteTableName(&result, kTargetGlobalTable); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(StringView)key { + Slice contents = key; + return ReadTableNameMatching(&contents, kTargetGlobalTable) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBTargetKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kTargetsTable); + return result; +} + ++ (std::string)keyWithTargetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kTargetsTable); + WriteTargetID(&result, targetID); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(StringView)key { + Slice contents = key; + return ReadTableNameMatching(&contents, kTargetsTable) && ReadTargetID(&contents, &_targetID) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBQueryTargetKey { + std::string _canonicalID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kQueryTargetsTable); + return result; +} + ++ (std::string)keyPrefixWithCanonicalID:(StringView)canonicalID { + std::string result; + WriteTableName(&result, kQueryTargetsTable); + WriteCanonicalID(&result, canonicalID); + return result; +} + ++ (std::string)keyWithCanonicalID:(StringView)canonicalID targetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kQueryTargetsTable); + WriteCanonicalID(&result, canonicalID); + WriteTargetID(&result, targetID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)canonicalID { + return _canonicalID; +} + +- (BOOL)decodeKey:(StringView)key { + _canonicalID.clear(); + + Slice contents = key; + return ReadTableNameMatching(&contents, kQueryTargetsTable) && + ReadCanonicalID(&contents, &_canonicalID) && ReadTargetID(&contents, &_targetID) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBTargetDocumentKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kTargetDocumentsTable); + return result; +} + ++ (std::string)keyPrefixWithTargetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kTargetDocumentsTable); + WriteTargetID(&result, targetID); + return result; +} + ++ (std::string)keyWithTargetID:(FSTTargetID)targetID documentKey:(FSTDocumentKey *)documentKey { + std::string result; + WriteTableName(&result, kTargetDocumentsTable); + WriteTargetID(&result, targetID); + WriteResourcePath(&result, documentKey.path); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(Firestore::StringView)key { + _documentKey = nil; + + leveldb::Slice contents = key; + return ReadTableNameMatching(&contents, kTargetDocumentsTable) && + ReadTargetID(&contents, &_targetID) && ReadDocumentKey(&contents, &_documentKey) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBDocumentTargetKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kDocumentTargetsTable); + return result; +} + ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath { + std::string result; + WriteTableName(&result, kDocumentTargetsTable); + WriteResourcePath(&result, resourcePath); + return result; +} + ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)documentKey targetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kDocumentTargetsTable); + WriteResourcePath(&result, documentKey.path); + WriteTargetID(&result, targetID); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(Firestore::StringView)key { + _documentKey = nil; + + leveldb::Slice contents = key; + return ReadTableNameMatching(&contents, kDocumentTargetsTable) && + ReadDocumentKey(&contents, &_documentKey) && ReadTargetID(&contents, &_targetID) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBRemoteDocumentKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kRemoteDocumentsTable); + return result; +} + ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)path { + std::string result; + WriteTableName(&result, kRemoteDocumentsTable); + WriteResourcePath(&result, path); + return result; +} + ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)key { + std::string result; + WriteTableName(&result, kRemoteDocumentsTable); + WriteResourcePath(&result, key.path); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(StringView)key { + _documentKey = nil; + + Slice contents = key; + return ReadTableNameMatching(&contents, kRemoteDocumentsTable) && + ReadDocumentKey(&contents, &_documentKey) && ReadTerminator(&contents); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h new file mode 100644 index 0000000..c9b5166 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.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 + +#import "FSTMutationQueue.h" + +#ifdef __cplusplus +#include + +namespace leveldb { +class DB; +} +#endif + +@class FSTLevelDB; +@class FSTLocalSerializer; +@class FSTUser; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** A mutation queue for a specific user, backed by LevelDB. */ +@interface FSTLevelDBMutationQueue : NSObject + +- (instancetype)init __attribute__((unavailable("Use a static constructor"))); + +/** The garbage collector to notify about potential garbage keys. */ +@property(nonatomic, weak, readwrite, nullable) id garbageCollector; + +#ifdef __cplusplus +/** + * Creates a new mutation queue for the given user, in the given LevelDB. + * + * @param user The user for which to create a mutation queue. + * @param db The LevelDB in which to create the queue. + */ ++ (instancetype)mutationQueueWithUser:(FSTUser *)user + db:(std::shared_ptr)db + serializer:(FSTLocalSerializer *)serializer; + +/** + * Returns one larger than the largest batch ID that has been stored. If there are no mutations + * returns 0. Note that batch IDs are global. + */ ++ (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr)db; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm new file mode 100644 index 0000000..d57a15d --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm @@ -0,0 +1,637 @@ +/* + * 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 "FSTLevelDBMutationQueue.h" + +#include +#include +#include +#include + +#import "Mutation.pbobjc.h" +#import "FSTUser.h" +#import "FSTQuery.h" +#import "FSTLevelDB.h" +#import "FSTLevelDBKey.h" +#import "FSTLocalSerializer.h" +#import "FSTWriteGroup.h" +#import "FSTDocumentKey.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTPath.h" +#import "FSTAssert.h" + +#include "ordered_code.h" +#include "string_util.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using Firestore::StringView; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::ReadOptions; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteBatch; +using leveldb::WriteOptions; + +@interface FSTLevelDBMutationQueue () + +- (instancetype)initWithUserID:(NSString *)userID + db:(std::shared_ptr)db + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; + +/** The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB keys. */ +@property(nonatomic, strong, readonly) NSString *userID; + +/** + * Next value to use when assigning sequential IDs to each mutation batch. + * + * NOTE: There can only be one FSTLevelDBMutationQueue for a given db at a time, hence it is safe + * to track nextBatchID as an instance-level property. Should we ever relax this constraint we'll + * need to revisit this. + */ +@property(nonatomic, assign) FSTBatchID nextBatchID; + +/** A write-through cache copy of the metadata describing the current queue. */ +@property(nonatomic, strong, nullable) FSTPBMutationQueue *metadata; + +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +/** + * Returns a standard set of read options. + * + * For now this is paranoid, but perhaps disable that in production builds. + */ +static ReadOptions StandardReadOptions() { + ReadOptions options; + options.verify_checksums = true; + return options; +} + +@implementation FSTLevelDBMutationQueue { + // The DB pointer is shared with all cooperating LevelDB-related objects. + std::shared_ptr _db; +} + ++ (instancetype)mutationQueueWithUser:(FSTUser *)user + db:(std::shared_ptr)db + serializer:(FSTLocalSerializer *)serializer { + FSTAssert(![user.UID isEqual:@""], @"UserID must not be an empty string."); + NSString *userID = user.isUnauthenticated ? @"" : user.UID; + + return [[FSTLevelDBMutationQueue alloc] initWithUserID:userID db:db serializer:serializer]; +} + +- (instancetype)initWithUserID:(NSString *)userID + db:(std::shared_ptr)db + serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + _userID = userID; + _db = db; + _serializer = serializer; + } + return self; +} + +- (void)startWithGroup:(FSTWriteGroup *)group { + FSTBatchID nextBatchID = [FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db]; + + // On restart, nextBatchId may end up lower than lastAcknowledgedBatchId since it's computed from + // the queue contents, and there may be no mutations in the queue. In this case, we need to reset + // lastAcknowledgedBatchId (which is safe since the queue must be empty). + std::string key = [self keyForCurrentMutationQueue]; + FSTPBMutationQueue *metadata = [self metadataForKey:key]; + if (!metadata) { + metadata = [FSTPBMutationQueue message]; + + // proto3's default value for lastAcknowledgedBatchId is zero, but that would consider the first + // entry in the queue to be acknowledged without that acknowledgement actually happening. + metadata.lastAcknowledgedBatchId = kFSTBatchIDUnknown; + } else { + FSTBatchID lastAcked = metadata.lastAcknowledgedBatchId; + if (lastAcked >= nextBatchID) { + FSTAssert([self isEmpty], @"Reset nextBatchID is only possible when the queue is empty"); + lastAcked = kFSTBatchIDUnknown; + + metadata.lastAcknowledgedBatchId = lastAcked; + [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]]; + } + } + + self.nextBatchID = nextBatchID; + self.metadata = metadata; +} + +- (void)shutdown { + _db.reset(); +} + ++ (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr)db { + std::unique_ptr it(db->NewIterator(StandardReadOptions())); + + auto tableKey = [FSTLevelDBMutationKey keyPrefix]; + + FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init]; + FSTBatchID maxBatchID = kFSTBatchIDUnknown; + + BOOL moreUserIDs = NO; + std::string nextUserID; + + it->Seek(tableKey); + if (it->Valid() && [rowKey decodeKey:it->key()]) { + moreUserIDs = YES; + nextUserID = rowKey.userID; + } + + // This loop assumes that nextUserId contains the next username at the start of the iteration. + while (moreUserIDs) { + // Compute the first key after the last mutation for nextUserID. + auto userEnd = [FSTLevelDBMutationKey keyPrefixWithUserID:nextUserID]; + userEnd = Firestore::PrefixSuccessor(userEnd); + + // Seek to that key with the intent of finding the boundary between nextUserID's mutations + // and the one after that (if any). + it->Seek(userEnd); + + // At this point there are three possible cases to handle differently. Each case must prepare + // the next iteration (by assigning to nextUserID or setting moreUserIDs = NO) and seek the + // iterator to the last row in the current user's mutation sequence. + if (!it->Valid()) { + // The iterator is past the last row altogether (there are no additional userIDs and now + // rows in any table after mutations). The last row will have the highest batchID. + moreUserIDs = NO; + it->SeekToLast(); + + } else if ([rowKey decodeKey:it->key()]) { + // The iterator is valid and the key decoded successfully so the next user was just decoded. + nextUserID = rowKey.userID; + it->Prev(); + + } else { + // The iterator is past the end of the mutations table but there are other rows. + moreUserIDs = NO; + it->Prev(); + } + + // In all the cases above there was at least one row for the current user and each case has + // set things up such that iterator points to it. + if (![rowKey decodeKey:it->key()]) { + FSTFail(@"There should have been a key previous to %s", userEnd.c_str()); + } + + if (rowKey.batchID > maxBatchID) { + maxBatchID = rowKey.batchID; + } + } + + return maxBatchID + 1; +} + +- (BOOL)isEmpty { + std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID]; + + std::unique_ptr it(_db->NewIterator(StandardReadOptions())); + it->Seek(userKey); + + BOOL empty = YES; + if (it->Valid() && it->key().starts_with(userKey)) { + empty = NO; + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"isEmpty failed with status: %s", status.ToString().c_str()); + } + + return empty; +} + +- (FSTBatchID)highestAcknowledgedBatchID { + return self.metadata.lastAcknowledgedBatchId; +} + +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(FSTWriteGroup *)group { + FSTBatchID batchID = batch.batchID; + FSTAssert(batchID > self.highestAcknowledgedBatchID, + @"Mutation batchIDs must be acknowledged in order"); + + FSTPBMutationQueue *metadata = self.metadata; + metadata.lastAcknowledgedBatchId = batchID; + metadata.lastStreamToken = streamToken; + + [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]]; +} + +- (nullable NSData *)lastStreamToken { + return self.metadata.lastStreamToken; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(FSTWriteGroup *)group { + FSTPBMutationQueue *metadata = self.metadata; + metadata.lastStreamToken = streamToken; + + [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]]; +} + +- (std::string)keyForCurrentMutationQueue { + return [FSTLevelDBMutationQueueKey keyWithUserID:self.userID]; +} + +- (nullable FSTPBMutationQueue *)metadataForKey:(const std::string &)key { + std::string value; + Status status = _db->Get(StandardReadOptions(), key, &value); + if (status.ok()) { + return [self parsedMetadata:value]; + } else if (status.IsNotFound()) { + return nil; + } else { + FSTFail(@"metadataForKey: failed loading key %s with status: %s", key.c_str(), + status.ToString().c_str()); + } +} + +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations + group:(FSTWriteGroup *)group { + FSTBatchID batchID = self.nextBatchID; + self.nextBatchID += 1; + + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; + std::string key = [self mutationKeyForBatch:batch]; + [group setMessage:[self.serializer encodedMutationBatch:batch] forKey:key]; + + NSString *userID = self.userID; + + // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the + // future if we wanted to store some other kind of value here, we can parse these empty values as + // with some other protocol buffer (and the parser will see all default values). + std::string emptyBuffer; + + for (FSTMutation *mutation in mutations) { + key = [FSTLevelDBDocumentMutationKey keyWithUserID:userID + documentKey:mutation.key + batchID:batchID]; + [group setData:emptyBuffer forKey:key]; + } + + return batch; +} + +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID { + std::string key = [self mutationKeyForBatchID:batchID]; + + std::string value; + Status status = _db->Get(StandardReadOptions(), key, &value); + if (!status.ok()) { + if (status.IsNotFound()) { + return nil; + } + FSTFail(@"Lookup mutation batch (%@, %d) failed with status: %s", self.userID, batchID, + status.ToString().c_str()); + } + + return [self decodedMutationBatch:value]; +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + std::string key = [self mutationKeyForBatchID:batchID + 1]; + std::unique_ptr it(_db->NewIterator(StandardReadOptions())); + it->Seek(key); + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Seek to mutation batch (%@, %d) failed with status: %s", self.userID, batchID, + status.ToString().c_str()); + } + + FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init]; + if (!it->Valid() || ![rowKey decodeKey:it->key()]) { + // Past the last row in the DB or out of the mutations table + return nil; + } + + if (rowKey.userID != [self.userID UTF8String]) { + // Jumped past the last mutation for this user + return nil; + } + + FSTAssert(rowKey.batchID > batchID, @"Should have found mutation after %d", batchID); + return [self decodedMutationBatch:it->value()]; +} + +- (NSArray *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID { + std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID]; + const char *userID = [self.userID UTF8String]; + + std::unique_ptr it(_db->NewIterator(StandardReadOptions())); + it->Seek(userKey); + + NSMutableArray *result = [NSMutableArray array]; + FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init]; + for (; it->Valid() && [rowKey decodeKey:it->key()]; it->Next()) { + if (rowKey.userID != userID) { + // End of this user's mutations + break; + } else if (rowKey.batchID > batchID) { + // This mutation is past what we're looking for + break; + } + + [result addObject:[self decodedMutationBatch:it->value()]]; + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Find all mutations through mutation batch (%@, %d) failed with status: %s", + self.userID, batchID, status.ToString().c_str()); + } + + return result; +} + +- (NSArray *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey { + NSString *userID = self.userID; + + // Scan the document-mutation index starting with a prefix starting with the given documentKey. + std::string indexPrefix = + [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:documentKey.path]; + std::unique_ptr indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + // Simultaneously scan the mutation queue. This works because each (key, batchID) pair is unique + // and ordered, so when scanning a table prefixed by exactly key, all the batchIDs encountered + // will be unique and in order. + std::string mutationsPrefix = [FSTLevelDBMutationKey keyPrefixWithUserID:userID]; + std::unique_ptr mutationIterator(_db->NewIterator(StandardReadOptions())); + + NSMutableArray *result = [NSMutableArray array]; + FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init]; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching exactly the specific key of interest. Note that because we order + // by path first, and we order terminators before path separators, we'll encounter all the + // index rows for documentKey contiguously. In particular, all the rows for documentKey will + // occur before any rows for documents nested in a subcollection beneath documentKey so we can + // stop as soon as we hit any such row. + if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey] || + ![rowKey.documentKey isEqualToKey:documentKey]) { + break; + } + + // Each row is a unique combination of key and batchID, so this foreign key reference can + // only occur once. + std::string mutationKey = [FSTLevelDBMutationKey keyWithUserID:userID batchID:rowKey.batchID]; + mutationIterator->Seek(mutationKey); + if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) { + NSString *foundKeyDescription = @"the end of the table"; + if (mutationIterator->Valid()) { + foundKeyDescription = [FSTLevelDBKey descriptionForKey:mutationIterator->key()]; + } + FSTFail(@"Dangling document-mutation reference found: " + @"%@ points to %@; seeking there found %@", + [FSTLevelDBKey descriptionForKey:indexKey], + [FSTLevelDBKey descriptionForKey:mutationKey], foundKeyDescription); + } + + [result addObject:[self decodedMutationBatch:mutationIterator->value()]]; + } + return result; +} + +- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { + FSTAssert(![query isDocumentQuery], @"Document queries shouldn't go down this path"); + NSString *userID = self.userID; + + FSTResourcePath *queryPath = query.path; + int immediateChildrenPathLength = queryPath.length + 1; + + // TODO(mcg): Actually implement a single-collection query + // + // This is actually executing an ancestor query, traversing the whole subtree below the + // collection which can be horrifically inefficient for some structures. The right way to + // solve this is to implement the full value index, but that's not in the cards in the near + // future so this is the best we can do for the moment. + // + // Since we don't yet index the actual properties in the mutations, our current approach is to + // just return all mutation batches that affect documents in the collection being queried. + // + // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the document-mutation + // index for more than a single document so the associated batchIDs will be neither necessarily + // unique nor in order. This means an efficient simultaneous scan isn't possible. + std::string indexPrefix = + [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:queryPath]; + std::unique_ptr indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + NSMutableArray *result = [NSMutableArray array]; + FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init]; + + // Collect up unique batchIDs encountered during a scan of the index. Use a set to + // accumulate batch IDs so they can be traversed in order in a scan of the main table. + // + // This method is faster than performing lookups of the keys with _db->Get and keeping a hash of + // batchIDs that have already been looked up. The performance difference is minor for small + // numbers of keys but > 30% faster for larger numbers of keys. + std::set uniqueBatchIds; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey]) { + break; + } + + // Rows with document keys more than one segment longer than the query path can't be matches. + // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. + // TODO(mcg): we'll need a different scanner when we implement ancestor queries. + if (rowKey.documentKey.path.length != immediateChildrenPathLength) { + continue; + } + + uniqueBatchIds.insert(rowKey.batchID); + } + + // Given an ordered set of unique batchIDs perform a skipping scan over the main table to find + // the mutation batches. + std::unique_ptr mutationIterator(_db->NewIterator(StandardReadOptions())); + + for (FSTBatchID batchID : uniqueBatchIds) { + std::string mutationKey = [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID]; + mutationIterator->Seek(mutationKey); + if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) { + NSString *foundKeyDescription = @"the end of the table"; + if (mutationIterator->Valid()) { + foundKeyDescription = [FSTLevelDBKey descriptionForKey:mutationIterator->key()]; + } + FSTFail(@"Dangling document-mutation reference found: " + @"Missing batch %@; seeking there found %@", + [FSTLevelDBKey descriptionForKey:mutationKey], foundKeyDescription); + } + + [result addObject:[self decodedMutationBatch:mutationIterator->value()]]; + } + return result; +} + +- (NSArray *)allMutationBatches { + std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID]; + + std::unique_ptr it(_db->NewIterator(StandardReadOptions())); + it->Seek(userKey); + + NSMutableArray *result = [NSMutableArray array]; + for (; it->Valid() && it->key().starts_with(userKey); it->Next()) { + [result addObject:[self decodedMutationBatch:it->value()]]; + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Find all mutation batches failed with status: %s", status.ToString().c_str()); + } + + return result; +} + +- (void)removeMutationBatches:(NSArray *)batches group:(FSTWriteGroup *)group { + NSString *userID = self.userID; + id garbageCollector = self.garbageCollector; + + std::unique_ptr checkIterator(_db->NewIterator(StandardReadOptions())); + + for (FSTMutationBatch *batch in batches) { + FSTBatchID batchID = batch.batchID; + std::string key = [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID]; + + // As a sanity check, verify that the mutation batch exists before deleting it. + checkIterator->Seek(key); + FSTAssert(checkIterator->Valid(), @"Mutation batch %@ did not exist", + [FSTLevelDBKey descriptionForKey:key]); + + FSTAssert(key == checkIterator->key(), @"Mutation batch %@ not found; found %@", + [FSTLevelDBKey descriptionForKey:key], + [FSTLevelDBKey descriptionForKey:checkIterator->key()]); + + [group removeMessageForKey:key]; + + for (FSTMutation *mutation in batch.mutations) { + key = [FSTLevelDBDocumentMutationKey keyWithUserID:userID + documentKey:mutation.key + batchID:batchID]; + [group removeMessageForKey:key]; + [garbageCollector addPotentialGarbageKey:mutation.key]; + } + } +} + +- (void)performConsistencyCheck { + if (![self isEmpty]) { + return; + } + + // Verify that there are no entries in the document-mutation index if the queue is empty. + std::string indexPrefix = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID]; + std::unique_ptr indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + NSMutableArray *danglingMutationReferences = [NSMutableArray array]; + + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching this index prefix for the current user. + if (!indexKey.starts_with(indexPrefix)) { + break; + } + + [danglingMutationReferences addObject:[FSTLevelDBKey descriptionForKey:indexKey]]; + } + + FSTAssert(danglingMutationReferences.count == 0, + @"Document leak -- detected dangling mutation references when queue " + @"is empty. Dangling keys: %@", + danglingMutationReferences); +} + +- (std::string)mutationKeyForBatch:(FSTMutationBatch *)batch { + return [FSTLevelDBMutationKey keyWithUserID:self.userID batchID:batch.batchID]; +} + +- (std::string)mutationKeyForBatchID:(FSTBatchID)batchID { + return [FSTLevelDBMutationKey keyWithUserID:self.userID batchID:batchID]; +} + +/** Parses the MutationQueue metadata from the given LevelDB row contents. */ +- (FSTPBMutationQueue *)parsedMetadata:(Slice)slice { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBMutationQueue *proto = [FSTPBMutationQueue parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBMutationQueue failed to parse: %@", error); + } + + return proto; +} + +- (FSTMutationBatch *)decodedMutationBatch:(Slice)slice { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBWriteBatch *proto = [FSTPBWriteBatch parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBMutationBatch failed to parse: %@", error); + } + + return [self.serializer decodedMutationBatch:proto]; +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)documentKey { + std::string indexPrefix = + [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:documentKey.path]; + std::unique_ptr indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + if (indexIterator->Valid()) { + FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init]; + Slice iteratorKey = indexIterator->key(); + + // Check both that the key prefix matches and that the decoded document key is exactly the key + // we're looking for. + if (iteratorKey.starts_with(indexPrefix) && [rowKey decodeKey:iteratorKey] && + [rowKey.documentKey isEqualToKey:documentKey]) { + return YES; + } + } + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.h b/Firestore/Source/Local/FSTLevelDBQueryCache.h new file mode 100644 index 0000000..3f24e6a --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.h @@ -0,0 +1,54 @@ +/* + * 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 + +#import "FSTQueryCache.h" + +#ifdef __cplusplus +#include + +namespace leveldb { +class DB; +} +#endif + +@class FSTLocalSerializer; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** Cached Queries backed by LevelDB. */ +@interface FSTLevelDBQueryCache : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** The garbage collector to notify about potential garbage keys. */ +@property(nonatomic, weak, readwrite, nullable) id garbageCollector; + +#ifdef __cplusplus +/** + * Creates a new query cache in the given LevelDB. + * + * @param db The LevelDB in which to create the cache. + */ +- (instancetype)initWithDB:(std::shared_ptr)db + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.mm b/Firestore/Source/Local/FSTLevelDBQueryCache.mm new file mode 100644 index 0000000..c1ba654 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.mm @@ -0,0 +1,340 @@ +/* + * 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 "FSTLevelDBQueryCache.h" + +#include +#include +#include + +#import "Target.pbobjc.h" +#import "FSTQuery.h" +#import "FSTLevelDBKey.h" +#import "FSTLocalSerializer.h" +#import "FSTQueryData.h" +#import "FSTWriteGroup.h" +#import "FSTDocumentKey.h" +#import "FSTAssert.h" + +#include "ordered_code.h" +#include "string_util.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using Firestore::StringView; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::ReadOptions; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteOptions; + +/** + * Returns a standard set of read options. + * + * For now this is paranoid, but perhaps disable that in production builds. + */ +static ReadOptions GetStandardReadOptions() { + ReadOptions options; + options.verify_checksums = true; + return options; +} + +@interface FSTLevelDBQueryCache () + +/** A write-through cached copy of the metadata for the query cache. */ +@property(nonatomic, strong, nullable) FSTPBTargetGlobal *metadata; + +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +@implementation FSTLevelDBQueryCache { + // The DB pointer is shared with all cooperating LevelDB-related objects. + std::shared_ptr _db; + + /** + * The last received snapshot version. This is part of `metadata` but we store it separately to + * avoid extra conversion to/from GPBTimestamp. + */ + FSTSnapshotVersion *_lastRemoteSnapshotVersion; +} + +- (instancetype)initWithDB:(std::shared_ptr)db serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + FSTAssert(db, @"db must not be NULL"); + _db = db; + _serializer = serializer; + } + return self; +} + +- (void)start { + std::string key = [FSTLevelDBTargetGlobalKey key]; + FSTPBTargetGlobal *metadata = [self metadataForKey:key]; + if (!metadata) { + metadata = [FSTPBTargetGlobal message]; + } + _lastRemoteSnapshotVersion = [self.serializer decodedVersion:metadata.lastRemoteSnapshotVersion]; + + self.metadata = metadata; +} + +#pragma mark - FSTQueryCache implementation + +- (FSTTargetID)highestTargetID { + return self.metadata.highestTargetId; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return _lastRemoteSnapshotVersion; +} + +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group { + _lastRemoteSnapshotVersion = snapshotVersion; + self.metadata.lastRemoteSnapshotVersion = [self.serializer encodedVersion:snapshotVersion]; + [group setMessage:self.metadata forKey:[FSTLevelDBTargetGlobalKey key]]; +} + +- (void)shutdown { + _db.reset(); +} + +- (void)addQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group { + // TODO(mcg): actually populate listen sequence number + FSTTargetID targetID = queryData.targetID; + std::string key = [FSTLevelDBTargetKey keyWithTargetID:targetID]; + [group setMessage:[self.serializer encodedQueryData:queryData] forKey:key]; + + NSString *canonicalID = queryData.query.canonicalID; + std::string indexKey = + [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:targetID]; + std::string emptyBuffer; + [group setData:emptyBuffer forKey:indexKey]; + + FSTPBTargetGlobal *metadata = self.metadata; + if (targetID > metadata.highestTargetId) { + metadata.highestTargetId = targetID; + [group setMessage:metadata forKey:[FSTLevelDBTargetGlobalKey key]]; + } +} + +- (void)removeQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group { + FSTTargetID targetID = queryData.targetID; + + [self removeMatchingKeysForTargetID:targetID group:group]; + + std::string key = [FSTLevelDBTargetKey keyWithTargetID:targetID]; + [group removeMessageForKey:key]; + + std::string indexKey = + [FSTLevelDBQueryTargetKey keyWithCanonicalID:queryData.query.canonicalID targetID:targetID]; + [group removeMessageForKey:indexKey]; +} + +/** + * Looks up the query global metadata associated with the given key. + * + * @return the parsed protocol buffer message or nil if the row referenced by the given key does + * not exist. + */ +- (nullable FSTPBTargetGlobal *)metadataForKey:(const std::string &)key { + std::string value; + Status status = _db->Get(GetStandardReadOptions(), key, &value); + if (status.IsNotFound()) { + return nil; + } else if (!status.ok()) { + FSTFail(@"metadataForKey: failed loading key %s with status: %s", key.c_str(), + status.ToString().c_str()); + } + + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)value.data() length:value.size() freeWhenDone:NO]; + + NSError *error; + FSTPBTargetGlobal *proto = [FSTPBTargetGlobal parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBTargetGlobal failed to parse: %@", error); + } + + return proto; +} + +/** + * Parses the given bytes as an FSTPBTarget protocol buffer and then converts to the equivalent + * query data. + */ +- (FSTQueryData *)decodedTargetWithSlice:(Slice)slice { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBTarget *proto = [FSTPBTarget parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBTarget failed to parse: %@", error); + } + + return [self.serializer decodedQueryData:proto]; +} + +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + // Scan the query-target index starting with a prefix starting with the given query's canonicalID. + // Note that this is a scan rather than a get because canonicalIDs are not required to be unique + // per target. + Slice canonicalID = StringView(query.canonicalID); + std::unique_ptr indexItererator(_db->NewIterator(GetStandardReadOptions())); + std::string indexPrefix = [FSTLevelDBQueryTargetKey keyPrefixWithCanonicalID:canonicalID]; + indexItererator->Seek(indexPrefix); + + // Simultaneously scan the targets table. This works because each (canonicalID, targetID) pair is + // unique and ordered, so when scanning a table prefixed by exactly one canonicalID, all the + // targetIDs will be unique and in order. + std::string targetPrefix = [FSTLevelDBTargetKey keyPrefix]; + std::unique_ptr targetIterator(_db->NewIterator(GetStandardReadOptions())); + + FSTLevelDBQueryTargetKey *rowKey = [[FSTLevelDBQueryTargetKey alloc] init]; + for (; indexItererator->Valid(); indexItererator->Next()) { + Slice indexKey = indexItererator->key(); + + // Only consider rows matching exactly the specific canonicalID of interest. + if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey] || + canonicalID != rowKey.canonicalID) { + // End of this canonicalID's possible targets. + break; + } + + // Each row is a unique combination of canonicalID and targetID, so this foreign key reference + // can only occur once. + std::string targetKey = [FSTLevelDBTargetKey keyWithTargetID:rowKey.targetID]; + targetIterator->Seek(targetKey); + if (!targetIterator->Valid() || targetIterator->key() != targetKey) { + NSString *foundKeyDescription = @"the end of the table"; + if (targetIterator->Valid()) { + foundKeyDescription = [FSTLevelDBKey descriptionForKey:targetIterator->key()]; + } + FSTFail(@"Dangling query-target reference found: " + @"%@ points to %@; seeking there found %@", + [FSTLevelDBKey descriptionForKey:indexKey], + [FSTLevelDBKey descriptionForKey:targetKey], foundKeyDescription); + } + + // Finally after finding a potential match, check that the query is actually equal to the + // requested query. + FSTQueryData *target = [self decodedTargetWithSlice:targetIterator->value()]; + if ([target.query isEqual:query]) { + return target; + } + } + + return nil; +} + +#pragma mark Matching Key tracking + +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group { + // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the + // future if we wanted to store some other kind of value here, we can parse these empty values as + // with some other protocol buffer (and the parser will see all default values). + std::string emptyBuffer; + + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *documentKey, BOOL *stop) { + [group setData:emptyBuffer + forKey:[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:documentKey]]; + [group setData:emptyBuffer + forKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey targetID:targetID]]; + }]; +} + +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [group + removeMessageForKey:[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key]]; + [group + removeMessageForKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID]]; + [self.garbageCollector addPotentialGarbageKey:key]; + }]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(FSTWriteGroup *)group { + std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID]; + std::unique_ptr indexIterator(_db->NewIterator(GetStandardReadOptions())); + indexIterator->Seek(indexPrefix); + + FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init]; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching this specific targetID. + if (![rowKey decodeKey:indexKey] || rowKey.targetID != targetID) { + break; + } + FSTDocumentKey *documentKey = rowKey.documentKey; + + // Delete both index rows + [group removeMessageForKey:indexKey]; + [group removeMessageForKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey + targetID:targetID]]; + [self.garbageCollector addPotentialGarbageKey:documentKey]; + } +} + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { + std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID]; + std::unique_ptr indexIterator(_db->NewIterator(GetStandardReadOptions())); + indexIterator->Seek(indexPrefix); + + FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init]; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching this specific targetID. + if (![rowKey decodeKey:indexKey] || rowKey.targetID != targetID) { + break; + } + + result = [result setByAddingObject:rowKey.documentKey]; + } + + return result; +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)key { + std::string indexPrefix = [FSTLevelDBDocumentTargetKey keyPrefixWithResourcePath:key.path]; + std::unique_ptr indexIterator(_db->NewIterator(GetStandardReadOptions())); + indexIterator->Seek(indexPrefix); + + if (indexIterator->Valid()) { + FSTLevelDBDocumentTargetKey *rowKey = [[FSTLevelDBDocumentTargetKey alloc] init]; + if ([rowKey decodeKey:indexIterator->key()] && [rowKey.documentKey isEqualToKey:key]) { + return YES; + } + } + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h new file mode 100644 index 0000000..f327813 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h @@ -0,0 +1,50 @@ +/* + * 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 + +#import "FSTRemoteDocumentCache.h" + +#ifdef __cplusplus +#include + +namespace leveldb { +class DB; +} +#endif + +@class FSTLocalSerializer; + +NS_ASSUME_NONNULL_BEGIN + +/** Cached Remote Documents backed by leveldb. */ +@interface FSTLevelDBRemoteDocumentCache : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +#ifdef __cplusplus +/** + * Creates a new remote documents cache in the given leveldb. + * + * @param db The leveldb in which to create the cache. + */ +- (instancetype)initWithDB:(std::shared_ptr)db + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm new file mode 100644 index 0000000..e2424b9 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm @@ -0,0 +1,153 @@ +/* + * 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 "FSTLevelDBRemoteDocumentCache.h" + +#include +#include +#include + +#import "MaybeDocument.pbobjc.h" +#import "FSTLevelDBKey.h" +#import "FSTLocalSerializer.h" +#import "FSTWriteGroup.h" +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTAssert.h" + +#include "ordered_code.h" +#include "string_util.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::ReadOptions; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteOptions; + +@interface FSTLevelDBRemoteDocumentCache () + +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +/** + * Returns a standard set of read options. + * + * For now this is paranoid, but perhaps disable that in production builds. + */ +static ReadOptions StandardReadOptions() { + ReadOptions options; + options.verify_checksums = true; + return options; +} + +@implementation FSTLevelDBRemoteDocumentCache { + // The DB pointer is shared with all cooperating LevelDB-related objects. + std::shared_ptr _db; +} + +- (instancetype)initWithDB:(std::shared_ptr)db serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + _db = db; + _serializer = serializer; + } + return self; +} + +- (void)shutdown { + _db.reset(); +} + +- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group { + std::string key = [self remoteDocumentKey:document.key]; + [group setMessage:[self.serializer encodedMaybeDocument:document] forKey:key]; +} + +- (void)removeEntryForKey:(FSTDocumentKey *)documentKey group:(FSTWriteGroup *)group { + std::string key = [self remoteDocumentKey:documentKey]; + [group removeMessageForKey:key]; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey { + std::string key = [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:documentKey]; + std::string value; + Status status = _db->Get(StandardReadOptions(), key, &value); + if (status.IsNotFound()) { + return nil; + } else if (status.ok()) { + return [self decodedMaybeDocument:value withKey:documentKey]; + } else { + FSTFail(@"Fetch document for key (%@) failed with status: %s", documentKey, + status.ToString().c_str()); + } +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + // TODO(mikelehen): PERF: At least filter to the documents that match the path of the query. + FSTDocumentDictionary *results = [FSTDocumentDictionary documentDictionary]; + + std::string startKey = [FSTLevelDBRemoteDocumentKey keyPrefix]; + std::unique_ptr it(_db->NewIterator(StandardReadOptions())); + it->Seek(startKey); + + FSTLevelDBRemoteDocumentKey *currentKey = [[FSTLevelDBRemoteDocumentKey alloc] init]; + for (; it->Valid() && [currentKey decodeKey:it->key()]; it->Next()) { + FSTMaybeDocument *maybeDoc = + [self decodedMaybeDocument:it->value() withKey:currentKey.documentKey]; + if ([maybeDoc isKindOfClass:[FSTDocument class]]) { + results = [results dictionaryBySettingObject:(FSTDocument *)maybeDoc forKey:maybeDoc.key]; + } + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Find documents matching query (%@) failed with status: %s", query, + status.ToString().c_str()); + } + + return results; +} + +- (std::string)remoteDocumentKey:(FSTDocumentKey *)key { + return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:key]; +} + +- (FSTMaybeDocument *)decodedMaybeDocument:(Slice)slice withKey:(FSTDocumentKey *)documentKey { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBMaybeDocument *proto = [FSTPBMaybeDocument parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBMaybeDocument failed to parse: %@", error); + } + + FSTMaybeDocument *maybeDocument = [self.serializer decodedMaybeDocument:proto]; + FSTAssert([maybeDocument.key isEqualToKey:documentKey], + @"Read document has key (%@) instead of expected key (%@).", maybeDocument.key, + documentKey); + return maybeDocument; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h new file mode 100644 index 0000000..60571c2 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalDocumentsView.h @@ -0,0 +1,62 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" + +@class FSTDocumentKey; +@class FSTMaybeDocument; +@class FSTQuery; +@protocol FSTMutationQueue; +@protocol FSTRemoteDocumentCache; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A readonly view of the local state of all documents we're tracking (i.e. we have a cached + * version in remoteDocumentCache or local mutations for the document). The view is computed by + * applying the mutations in the FSTMutationQueue to the FSTRemoteDocumentCache. + */ +@interface FSTLocalDocumentsView : NSObject + ++ (instancetype)viewWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue; + +- (instancetype)init __attribute__((unavailable("Use a static constructor"))); + +/** + * Get the local view of the document identified by `key`. + * + * @return Local view of the document or nil if we don't have any cached state for it. + */ +- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key; + +/** + * Gets the local view of the documents identified by `keys`. + * + * If we don't have cached state for a document in `keys`, a FSTDeletedDocument will be stored + * for that key in the resulting set. + */ +- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys; + +/** Performs a query against the local view of all documents. */ +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.m b/Firestore/Source/Local/FSTLocalDocumentsView.m new file mode 100644 index 0000000..0cad593 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalDocumentsView.m @@ -0,0 +1,182 @@ +/* + * 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 "FSTLocalDocumentsView.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTMutationQueue.h" +#import "FSTQuery.h" +#import "FSTRemoteDocumentCache.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalDocumentsView () +- (instancetype)initWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue + NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) id remoteDocumentCache; +@property(nonatomic, strong, readonly) id mutationQueue; +@end + +@implementation FSTLocalDocumentsView + ++ (instancetype)viewWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue { + return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache + mutationQueue:mutationQueue]; +} + +- (instancetype)initWithRemoteDocumentCache:(id)remoteDocumentCache + mutationQueue:(id)mutationQueue { + if (self = [super init]) { + _remoteDocumentCache = remoteDocumentCache; + _mutationQueue = mutationQueue; + } + return self; +} + +- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key { + FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:key]; + return [self localDocument:remoteDoc key:key]; +} + +- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys { + FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + for (FSTDocumentKey *key in keys.objectEnumerator) { + // TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one. + FSTMaybeDocument *maybeDoc = [self documentForKey:key]; + // TODO(http://b/32275378): Don't conflate missing / deleted. + if (!maybeDoc) { + maybeDoc = [FSTDeletedDocument documentWithKey:key version:[FSTSnapshotVersion noVersion]]; + } + results = [results dictionaryBySettingObject:maybeDoc forKey:key]; + } + return results; +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + if ([FSTDocumentKey isDocumentKey:query.path]) { + return [self documentsMatchingDocumentQuery:query.path]; + } else { + return [self documentsMatchingCollectionQuery:query]; + } +} + +- (FSTDocumentDictionary *)documentsMatchingDocumentQuery:(FSTResourcePath *)docPath { + FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; + // Just do a simple document lookup. + FSTMaybeDocument *doc = [self documentForKey:[FSTDocumentKey keyWithPath:docPath]]; + if ([doc isKindOfClass:[FSTDocument class]]) { + result = [result dictionaryBySettingObject:(FSTDocument *)doc forKey:doc.key]; + } + return result; +} + +- (FSTDocumentDictionary *)documentsMatchingCollectionQuery:(FSTQuery *)query { + // Query the remote documents and overlay mutations. + // TODO(mikelehen): There may be significant overlap between the mutations affecting these + // remote documents and the allMutationBatchesAffectingQuery mutations. Consider optimizing. + __block FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; + results = [self localDocuments:results]; + + // Now use the mutation queue to discover any other documents that may match the query after + // applying mutations. + FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet]; + NSArray *matchingMutationBatches = + [self.mutationQueue allMutationBatchesAffectingQuery:query]; + for (FSTMutationBatch *batch in matchingMutationBatches) { + for (FSTMutation *mutation in batch.mutations) { + // TODO(mikelehen): PERF: Check if this mutation actually affects the query to reduce work. + + // If the key is already in the results, we can skip it. + if (![results containsKey:mutation.key]) { + matchingKeys = [matchingKeys setByAddingObject:mutation.key]; + } + } + } + + // Now add in results for the matchingKeys. + for (FSTDocumentKey *key in matchingKeys.objectEnumerator) { + FSTMaybeDocument *doc = [self documentForKey:key]; + if ([doc isKindOfClass:[FSTDocument class]]) { + results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key]; + } + } + + // Finally, filter out any documents that don't actually match the query. Note that the extra + // reference here prevents ARC from deallocating the initial unfiltered results while we're + // enumerating them. + FSTDocumentDictionary *unfiltered = results; + [unfiltered + enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *doc, BOOL *stop) { + if (![query matchesDocument:doc]) { + results = [results dictionaryByRemovingObjectForKey:key]; + } + }]; + + return results; +} + +/** + * Takes a remote document and applies local mutations to generate the local view of the + * document. + * + * @param document The base remote document to apply mutations to. + * @param documentKey The key of the document (necessary when remoteDocument is nil). + */ +- (nullable FSTMaybeDocument *)localDocument:(nullable FSTMaybeDocument *)document + key:(FSTDocumentKey *)documentKey { + NSArray *batches = + [self.mutationQueue allMutationBatchesAffectingDocumentKey:documentKey]; + for (FSTMutationBatch *batch in batches) { + document = [batch applyTo:document documentKey:documentKey]; + } + + return document; +} + +/** + * Takes a set of remote documents and applies local mutations to generate the local view of + * the documents. + * + * @param documents The base remote documents to apply mutations to. + * @return The local view of the documents. + */ +- (FSTDocumentDictionary *)localDocuments:(FSTDocumentDictionary *)documents { + __block FSTDocumentDictionary *result = documents; + [documents enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *remoteDocument, + BOOL *stop) { + FSTMaybeDocument *mutatedDoc = [self localDocument:remoteDocument key:key]; + if ([mutatedDoc isKindOfClass:[FSTDeletedDocument class]]) { + result = [documents dictionaryByRemovingObjectForKey:key]; + } else if ([mutatedDoc isKindOfClass:[FSTDocument class]]) { + result = [documents dictionaryBySettingObject:(FSTDocument *)mutatedDoc forKey:key]; + } else { + FSTFail(@"Unknown document: %@", mutatedDoc); + } + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.h b/Firestore/Source/Local/FSTLocalSerializer.h new file mode 100644 index 0000000..6ca7f01 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalSerializer.h @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@class FSTMaybeDocument; +@class FSTMutationBatch; +@class FSTQueryData; +@class FSTSerializerBeta; +@class FSTSnapshotVersion; + +@class FSTPBMaybeDocument; +@class FSTPBTarget; +@class FSTPBWriteBatch; + +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Serializer for values stored in the LocalStore. + * + * Note that FSTLocalSerializer currently delegates to the serializer for the Firestore v1beta1 RPC + * protocol to save implementation time and code duplication. We'll need to revisit this when the + * RPC protocol we use diverges from local storage. + */ +@interface FSTLocalSerializer : NSObject + +- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer; + +- (instancetype)init NS_UNAVAILABLE; + +/** Encodes an FSTMaybeDocument model to the equivalent protocol buffer for local storage. */ +- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document; + +/** Decodes an FSTPBMaybeDocument proto to the equivalent model. */ +- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto; + +/** Encodes an FSTMutationBatch model for local storage in the mutation queue. */ +- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch; + +/** Decodes an FSTPBWriteBatch proto into a MutationBatch model. */ +- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch; + +/** Encodes an FSTQueryData model for local storage in the query cache. */ +- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData; + +/** Decodes an FSTPBTarget proto from local storage into an FSTQueryData model. */ +- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target; + +/** Encodes an FSTSnapshotVersion model into a GPBTimestamp proto. */ +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; + +/** Decodes a GPBTimestamp proto into a FSTSnapshotVersion model. */ +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.m b/Firestore/Source/Local/FSTLocalSerializer.m new file mode 100644 index 0000000..58b09af --- /dev/null +++ b/Firestore/Source/Local/FSTLocalSerializer.m @@ -0,0 +1,208 @@ +/* + * 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 "FSTLocalSerializer.h" + +#import "Document.pbobjc.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTFieldValue.h" +#import "FSTMutationBatch.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTSerializerBeta.h" +#import "MaybeDocument.pbobjc.h" +#import "Mutation.pbobjc.h" +#import "Target.pbobjc.h" + +@interface FSTLocalSerializer () + +@property(nonatomic, strong, readonly) FSTSerializerBeta *remoteSerializer; + +@end + +/** Serializer for values stored in the LocalStore. */ +@implementation FSTLocalSerializer + +- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer { + self = [super init]; + if (self) { + _remoteSerializer = remoteSerializer; + } + return self; +} + +- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document { + FSTPBMaybeDocument *proto = [FSTPBMaybeDocument message]; + + if ([document isKindOfClass:[FSTDeletedDocument class]]) { + proto.noDocument = [self encodedDeletedDocument:(FSTDeletedDocument *)document]; + } else if ([document isKindOfClass:[FSTDocument class]]) { + proto.document = [self encodedDocument:(FSTDocument *)document]; + } else { + FSTFail(@"Unknown document type %@", NSStringFromClass([document class])); + } + + return proto; +} + +- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto { + switch (proto.documentTypeOneOfCase) { + case FSTPBMaybeDocument_DocumentType_OneOfCase_Document: + return [self decodedDocument:proto.document]; + + case FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument: + return [self decodedDeletedDocument:proto.noDocument]; + + default: + FSTFail(@"Unknown MaybeDocument %@", proto); + } +} + +/** + * Encodes a Document for local storage. This differs from the v1beta1 RPC serializer for + * Documents in that it preserves the updateTime, which is considered an output only value by the + * server. + */ +- (GCFSDocument *)encodedDocument:(FSTDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + GCFSDocument *proto = [GCFSDocument message]; + proto.name = [remoteSerializer encodedDocumentKey:document.key]; + proto.fields = [remoteSerializer encodedFields:document.data]; + proto.updateTime = [remoteSerializer encodedVersion:document.version]; + + return proto; +} + +/** Decodes a Document proto to the equivalent model. */ +- (FSTDocument *)decodedDocument:(GCFSDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTObjectValue *data = [remoteSerializer decodedFields:document.fields]; + FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:document.name]; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime]; + return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; +} + +/** Encodes a NoDocument value to the equivalent proto. */ +- (FSTPBNoDocument *)encodedDeletedDocument:(FSTDeletedDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTPBNoDocument *proto = [FSTPBNoDocument message]; + proto.name = [remoteSerializer encodedDocumentKey:document.key]; + proto.readTime = [remoteSerializer encodedVersion:document.version]; + return proto; +} + +/** Decodes a NoDocument proto to the equivalent model. */ +- (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:proto.name]; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime]; + return [FSTDeletedDocument documentWithKey:key version:version]; +} + +- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; + proto.batchId = batch.batchID; + proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; + + NSMutableArray *writes = proto.writesArray; + for (FSTMutation *mutation in batch.mutations) { + [writes addObject:[remoteSerializer encodedMutation:mutation]]; + } + return proto; +} + +- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + int batchID = batch.batchId; + NSMutableArray *mutations = [NSMutableArray array]; + for (GCFSWrite *write in batch.writesArray) { + [mutations addObject:[remoteSerializer decodedMutation:write]]; + } + + FSTTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; + + return [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; +} + +- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTAssert(queryData.purpose == FSTQueryPurposeListen, + @"only queries with purpose %lu may be stored, got %lu", + (unsigned long)FSTQueryPurposeListen, (unsigned long)queryData.purpose); + + FSTPBTarget *proto = [FSTPBTarget message]; + proto.targetId = queryData.targetID; + proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshotVersion]; + proto.resumeToken = queryData.resumeToken; + + FSTQuery *query = queryData.query; + if ([query isDocumentQuery]) { + proto.documents = [remoteSerializer encodedDocumentsTarget:query]; + } else { + proto.query = [remoteSerializer encodedQueryTarget:query]; + } + + return proto; +} + +- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTTargetID targetID = target.targetId; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion]; + NSData *resumeToken = target.resumeToken; + + FSTQuery *query; + switch (target.targetTypeOneOfCase) { + case FSTPBTarget_TargetType_OneOfCase_Documents: + query = [remoteSerializer decodedQueryFromDocumentsTarget:target.documents]; + break; + + case FSTPBTarget_TargetType_OneOfCase_Query: + query = [remoteSerializer decodedQueryFromQueryTarget:target.query]; + break; + + default: + FSTFail(@"Unknown Target.targetType %" PRId32, target.targetTypeOneOfCase); + } + + return [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:version + resumeToken:resumeToken]; +} + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { + return [self.remoteSerializer encodedVersion:version]; +} + +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { + return [self.remoteSerializer decodedVersion:version]; +} + +@end diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h new file mode 100644 index 0000000..0fdc08e --- /dev/null +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -0,0 +1,194 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" +#import "FSTDocumentVersionDictionary.h" +#import "FSTTypes.h" + +@class FSTLocalViewChanges; +@class FSTLocalWriteResult; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTMutationBatchResult; +@class FSTQuery; +@class FSTQueryData; +@class FSTRemoteEvent; +@class FSTUser; +@protocol FSTPersistence; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Local storage in the Firestore client. Coordinates persistence components like the mutation + * queue and remote document cache to present a latency compensated view of stored data. + * + * The LocalStore is responsible for accepting mutations from the Sync Engine. Writes from the + * client are put into a queue as provisional Mutations until they are processed by the RemoteStore + * and confirmed as having been written to the server. + * + * The local store provides the local version of documents that have been modified locally. It + * maintains the constraint: + * + * LocalDocument = RemoteDocument + Active(LocalMutations) + * + * (Active mutations are those that are enqueued and have not been previously acknowledged or + * rejected). + * + * The RemoteDocument ("ground truth") state is provided via the applyChangeBatch method. It will + * be some version of a server-provided document OR will be a server-provided document PLUS + * acknowledged mutations: + * + * RemoteDocument' = RemoteDocument + Acknowledged(LocalMutations) + * + * Note that this "dirty" version of a RemoteDocument will not be identical to a server base + * version, since it has LocalMutations added to it pending getting an authoritative copy from the + * server. + * + * Since LocalMutations can be rejected by the server, we have to be able to revert a LocalMutation + * that has already been applied to the LocalDocument (typically done by replaying all remaining + * LocalMutations to the RemoteDocument to re-apply). + * + * The LocalStore is responsible for the garbage collection of the documents it contains. For now, + * it every doc referenced by a view, the mutation queue, or the RemoteStore. + * + * It also maintains the persistence of mapping queries to resume tokens and target ids. It needs + * to know this data about queries to properly know what docs it would be allowed to garbage + * collect. + * + * The LocalStore must be able to efficiently execute queries against its local cache of the + * documents, to provide the initial set of results before any remote changes have been received. + */ +@interface FSTLocalStore : NSObject + +/** Creates a new instance of the FSTLocalStore with its required dependencies as parameters. */ +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector + initialUser:(FSTUser *)initialUser NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** Performs any initial startup actions required by the local store. */ +- (void)start; + +/** Releases any open resources. */ +- (void)shutdown; + +/** + * Tells the FSTLocalStore that the currently authenticated user has changed. + * + * In response the local store switches the mutation queue to the new user and returns any + * resulting document changes. + */ +- (FSTMaybeDocumentDictionary *)userDidChange:(FSTUser *)user; + +/** Accepts locally generated Mutations and commits them to storage. */ +- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations; + +/** Returns the current value of a document with a given key, or nil if not found. */ +- (nullable FSTMaybeDocument *)readDocument:(FSTDocumentKey *)key; + +/** + * Acknowledges the given batch. + * + * On the happy path when a batch is acknowledged, the local store will + * + * + remove the batch from the mutation queue; + * + apply the changes to the remote document cache; + * + recalculate the latency compensated view implied by those changes (there may be mutations in + * the queue that affect the documents but haven't been acknowledged yet); and + * + give the changed documents back the sync engine + * + * @return The resulting (modified) documents. + */ +- (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult; + +/** + * Removes mutations from the MutationQueue for the specified batch. LocalDocuments will be + * recalculated. + * + * @return The resulting (modified) documents. + */ +- (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID; + +/** Returns the last recorded stream token for the current user. */ +- (nullable NSData *)lastStreamToken; + +/** + * Sets the stream token for the current user without acknowledging any mutation batch. This is + * usually only useful after a stream handshake or in response to an error that requires clearing + * the stream token. + */ +- (void)setLastStreamToken:(nullable NSData *)streamToken; + +/** + * Returns the last consistent snapshot processed (used by the RemoteStore to determine whether to + * buffer incoming snapshots from the backend). + */ +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; + +/** + * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any + * write batches that have been acknowledged or rejected (i.e. we do not re-apply local mutations + * to updates from this event). + * + * LocalDocuments are re-calculated if there are remaining mutations in the queue. + */ +- (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; + +/** + * Returns the keys of the documents that are associated with the given targetID in the remote + * table. + */ +- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID; + +/** + * Collects garbage if necessary. + * + * Should be called periodically by Sync Engine to recover resources. The implementation must + * guarantee that GC won't happen in other places than this method call. + */ +- (void)collectGarbage; + +/** + * Assigns @a query an internal ID so that its results can be pinned so they don't get GC'd. + * A query must be allocated in the local store before the store can be used to manage its view. + */ +- (FSTQueryData *)allocateQuery:(FSTQuery *)query; + +/** Unpin all the documents associated with @a query. */ +- (void)releaseQuery:(FSTQuery *)query; + +/** Runs @a query against all the documents in the local store and returns the results. */ +- (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query; + +/** Notify the local store of the changed views to locally pin / unpin documents. */ +- (void)notifyLocalViewChanges:(NSArray *)viewChanges; + +/** + * Gets the mutation batch after the passed in batchId in the mutation queue or nil if empty. + * + * @param batchID The batch to search after, or -1 for the first mutation in the queue. + * @return the next mutation or nil if there wasn't one. + */ +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalStore.m b/Firestore/Source/Local/FSTLocalStore.m new file mode 100644 index 0000000..d31712a --- /dev/null +++ b/Firestore/Source/Local/FSTLocalStore.m @@ -0,0 +1,546 @@ +/* + * 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 "FSTLocalStore.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTGarbageCollector.h" +#import "FSTLocalDocumentsView.h" +#import "FSTLocalViewChanges.h" +#import "FSTLocalWriteResult.h" +#import "FSTLogger.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTMutationQueue.h" +#import "FSTPersistence.h" +#import "FSTQuery.h" +#import "FSTQueryCache.h" +#import "FSTQueryData.h" +#import "FSTReferenceSet.h" +#import "FSTRemoteDocumentCache.h" +#import "FSTRemoteDocumentChangeBuffer.h" +#import "FSTRemoteEvent.h" +#import "FSTSnapshotVersion.h" +#import "FSTTargetIDGenerator.h" +#import "FSTTimestamp.h" +#import "FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalStore () + +/** Manages our in-memory or durable persistence. */ +@property(nonatomic, strong, readonly) id persistence; + +/** The set of all mutations that have been sent but not yet been applied to the backend. */ +@property(nonatomic, strong) id mutationQueue; + +/** The set of all cached remote documents. */ +@property(nonatomic, strong) id remoteDocumentCache; + +/** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */ +@property(nonatomic, strong) FSTLocalDocumentsView *localDocuments; + +/** The set of document references maintained by any local views. */ +@property(nonatomic, strong) FSTReferenceSet *localViewReferences; + +/** + * The garbage collector collects documents that should no longer be cached (e.g. if they are no + * longer retained by the above reference sets and the garbage collector is performing eager + * collection). + */ +@property(nonatomic, strong) id garbageCollector; + +/** Maps a query to the data about that query. */ +@property(nonatomic, strong) id queryCache; + +/** Maps a targetID to data about its query. */ +@property(nonatomic, strong) NSMutableDictionary *targetIDs; + +/** Used to generate targetIDs for queries tracked locally. */ +@property(nonatomic, strong) FSTTargetIDGenerator *targetIDGenerator; + +/** + * A heldBatchResult is a mutation batch result (from a write acknowledgement) that arrived before + * the watch stream got notified of a snapshot that includes the write.  So we "hold" it until + * the watch stream catches up. It ensures that the local write remains visible (latency + * compensation) and doesn't temporarily appear reverted because the watch stream is slower than + * the write stream and so wasn't reflecting it. + * + * NOTE: Eventually we want to move this functionality into the remote store. + */ +@property(nonatomic, strong) NSMutableArray *heldBatchResults; + +@end + +@implementation FSTLocalStore + +- (instancetype)initWithPersistence:(id)persistence + garbageCollector:(id)garbageCollector + initialUser:(FSTUser *)initialUser { + if (self = [super init]) { + _persistence = persistence; + _mutationQueue = [persistence mutationQueueForUser:initialUser]; + _remoteDocumentCache = [persistence remoteDocumentCache]; + _queryCache = [persistence queryCache]; + _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache + mutationQueue:_mutationQueue]; + _localViewReferences = [[FSTReferenceSet alloc] init]; + + _garbageCollector = garbageCollector; + [_garbageCollector addGarbageSource:_queryCache]; + [_garbageCollector addGarbageSource:_localViewReferences]; + [_garbageCollector addGarbageSource:_mutationQueue]; + + _targetIDs = [NSMutableDictionary dictionary]; + _heldBatchResults = [NSMutableArray array]; + } + return self; +} + +- (void)start { + [self startMutationQueue]; + [self startQueryCache]; +} + +- (void)startMutationQueue { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + + // If we have any leftover mutation batch results from a prior run, just drop them. + // TODO(http://b/33446471): We probably need to repopulate heldBatchResults or similar instead, + // but that is not straightforward since we're not persisting the write ack versions. + [self.heldBatchResults removeAllObjects]; + + // TODO(mikelehen): This is the only usage of getAllMutationBatchesThroughBatchId:. Consider + // removing it in favor of a getAcknowledgedBatches method. + FSTBatchID highestAck = [self.mutationQueue highestAcknowledgedBatchID]; + if (highestAck != kFSTBatchIDUnknown) { + NSArray *batches = + [self.mutationQueue allMutationBatchesThroughBatchID:highestAck]; + if (batches.count > 0) { + // NOTE: This could be more efficient if we had a removeBatchesThroughBatchID, but this set + // should be very small and this code should go away eventually. + [self.mutationQueue removeMutationBatches:batches group:group]; + } + } + [self.persistence commitGroup:group]; +} + +- (void)startQueryCache { + [self.queryCache start]; + + FSTTargetID targetID = [self.queryCache highestTargetID]; + self.targetIDGenerator = [FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:targetID]; +} + +- (void)shutdown { + [self.mutationQueue shutdown]; + [self.remoteDocumentCache shutdown]; + [self.queryCache shutdown]; +} + +- (FSTMaybeDocumentDictionary *)userDidChange:(FSTUser *)user { + // Swap out the mutation queue, grabbing the pending mutation batches before and after. + NSArray *oldBatches = [self.mutationQueue allMutationBatches]; + + [self.mutationQueue shutdown]; + [self.garbageCollector removeGarbageSource:self.mutationQueue]; + + self.mutationQueue = [self.persistence mutationQueueForUser:user]; + [self.garbageCollector addGarbageSource:self.mutationQueue]; + + [self startMutationQueue]; + + NSArray *newBatches = [self.mutationQueue allMutationBatches]; + + // Recreate our LocalDocumentsView using the new MutationQueue. + self.localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:self.remoteDocumentCache + mutationQueue:self.mutationQueue]; + + // Union the old/new changed keys. + FSTDocumentKeySet *changedKeys = [FSTDocumentKeySet keySet]; + for (NSArray *batches in @[ oldBatches, newBatches ]) { + for (FSTMutationBatch *batch in batches) { + for (FSTMutation *mutation in batch.mutations) { + changedKeys = [changedKeys setByAddingObject:mutation.key]; + } + } + } + + // Return the set of all (potentially) changed documents as the result of the user change. + return [self.localDocuments documentsForKeys:changedKeys]; +} + +- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Locally write mutations"]; + FSTTimestamp *localWriteTime = [FSTTimestamp timestamp]; + FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime + mutations:mutations + group:group]; + [self.persistence commitGroup:group]; + + FSTDocumentKeySet *keys = [batch keys]; + FSTMaybeDocumentDictionary *changedDocuments = [self.localDocuments documentsForKeys:keys]; + return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:changedDocuments]; +} + +- (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Acknowledge batch"]; + id mutationQueue = self.mutationQueue; + + [mutationQueue acknowledgeBatch:batchResult.batch + streamToken:batchResult.streamToken + group:group]; + + FSTDocumentKeySet *affected; + if ([self shouldHoldBatchResultWithVersion:batchResult.commitVersion]) { + [self.heldBatchResults addObject:batchResult]; + affected = [FSTDocumentKeySet keySet]; + } else { + FSTRemoteDocumentChangeBuffer *remoteDocuments = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache]; + + affected = + [self releaseBatchResults:@[ batchResult ] group:group remoteDocuments:remoteDocuments]; + + [remoteDocuments applyToWriteGroup:group]; + } + + [self.persistence commitGroup:group]; + [self.mutationQueue performConsistencyCheck]; + + return [self.localDocuments documentsForKeys:affected]; +} + +- (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Reject batch"]; + + FSTMutationBatch *toReject = [self.mutationQueue lookupMutationBatch:batchID]; + FSTAssert(toReject, @"Attempt to reject nonexistent batch!"); + + FSTBatchID lastAcked = [self.mutationQueue highestAcknowledgedBatchID]; + FSTAssert(batchID > lastAcked, @"Acknowledged batches can't be rejected."); + + FSTDocumentKeySet *affected = [self removeMutationBatch:toReject group:group]; + + [self.persistence commitGroup:group]; + [self.mutationQueue performConsistencyCheck]; + + return [self.localDocuments documentsForKeys:affected]; +} + +- (nullable NSData *)lastStreamToken { + return [self.mutationQueue lastStreamToken]; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Set stream token"]; + + [self.mutationQueue setLastStreamToken:streamToken group:group]; + [self.persistence commitGroup:group]; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return [self.queryCache lastRemoteSnapshotVersion]; +} + +- (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + id queryCache = self.queryCache; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Apply remote event"]; + FSTRemoteDocumentChangeBuffer *remoteDocuments = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache]; + + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + NSNumber *targetIDNumber, FSTTargetChange *change, BOOL *stop) { + FSTTargetID targetID = targetIDNumber.intValue; + + // Do not ref/unref unassigned targetIDs - it may lead to leaks. + FSTQueryData *queryData = self.targetIDs[targetIDNumber]; + if (!queryData) { + return; + } + + FSTTargetMapping *mapping = change.mapping; + if (mapping) { + // First make sure that all references are deleted. + if ([mapping isKindOfClass:[FSTResetMapping class]]) { + FSTResetMapping *reset = (FSTResetMapping *)mapping; + [queryCache removeMatchingKeysForTargetID:targetID group:group]; + [queryCache addMatchingKeys:reset.documents forTargetID:targetID group:group]; + + } else if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { + FSTUpdateMapping *update = (FSTUpdateMapping *)mapping; + [queryCache removeMatchingKeys:update.removedDocuments forTargetID:targetID group:group]; + [queryCache addMatchingKeys:update.addedDocuments forTargetID:targetID group:group]; + + } else { + FSTFail(@"Unknown mapping type: %@", mapping); + } + } + + // Update the resume token if the change includes one. Don't clear any preexisting value. + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken]; + self.targetIDs[targetIDNumber] = queryData; + [self.queryCache addQueryData:queryData group:group]; + } + }]; + + // TODO(klimt): This could probably be an NSMutableDictionary. + __block FSTDocumentKeySet *changedDocKeys = [FSTDocumentKeySet keySet]; + [remoteEvent.documentUpdates + enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *doc, BOOL *stop) { + changedDocKeys = [changedDocKeys setByAddingObject:key]; + FSTMaybeDocument *existingDoc = [remoteDocuments entryForKey:key]; + // Make sure we don't apply an old document version to the remote cache, though we + // make an exception for [SnapshotVersion noVersion] which can happen for manufactured + // events (e.g. in the case of a limbo document resolution failing). + if (!existingDoc || [doc.version isEqual:[FSTSnapshotVersion noVersion]] || + [doc.version compare:existingDoc.version] != NSOrderedAscending) { + [remoteDocuments addEntry:doc]; + } else { + FSTLog( + @"FSTLocalStore Ignoring outdated watch update for %@. " + "Current version: %@ Watch version: %@", + key, existingDoc.version, doc.version); + } + + // The document might be garbage because it was unreferenced by everything. + // Make sure to mark it as garbage if it is... + [self.garbageCollector addPotentialGarbageKey:key]; + }]; + + // HACK: The only reason we allow omitting snapshot version is so we can synthesize remote events + // when we get permission denied errors while trying to resolve the state of a locally cached + // document that is in limbo. + FSTSnapshotVersion *lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion]; + FSTSnapshotVersion *remoteVersion = remoteEvent.snapshotVersion; + if (![remoteVersion isEqual:[FSTSnapshotVersion noVersion]]) { + FSTAssert([remoteVersion compare:lastRemoteVersion] != NSOrderedAscending, + @"Watch stream reverted to previous snapshot?? (%@ < %@)", remoteVersion, + lastRemoteVersion); + [self.queryCache setLastRemoteSnapshotVersion:remoteVersion group:group]; + } + + FSTDocumentKeySet *releasedWriteKeys = + [self releaseHeldBatchResultsWithGroup:group remoteDocuments:remoteDocuments]; + + [remoteDocuments applyToWriteGroup:group]; + + [self.persistence commitGroup:group]; + + // Union the two key sets. + __block FSTDocumentKeySet *keysToRecalc = changedDocKeys; + [releasedWriteKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + keysToRecalc = [keysToRecalc setByAddingObject:key]; + }]; + + return [self.localDocuments documentsForKeys:keysToRecalc]; +} + +- (void)notifyLocalViewChanges:(NSArray *)viewChanges { + FSTReferenceSet *localViewReferences = self.localViewReferences; + for (FSTLocalViewChanges *view in viewChanges) { + FSTQueryData *queryData = [self.queryCache queryDataForQuery:view.query]; + FSTAssert(queryData, @"Local view changes contain unallocated query."); + FSTTargetID targetID = queryData.targetID; + [localViewReferences addReferencesToKeys:view.addedKeys forID:targetID]; + [localViewReferences removeReferencesToKeys:view.removedKeys forID:targetID]; + } +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + return [self.mutationQueue nextMutationBatchAfterBatchID:batchID]; +} + +- (nullable FSTMaybeDocument *)readDocument:(FSTDocumentKey *)key { + return [self.localDocuments documentForKey:key]; +} + +- (FSTQueryData *)allocateQuery:(FSTQuery *)query { + FSTQueryData *cached = [self.queryCache queryDataForQuery:query]; + FSTTargetID targetID; + if (cached) { + // This query has been listened to previously, so reuse the previous targetID. + // TODO(mcg): freshen last accessed date? + targetID = cached.targetID; + } else { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Allocate query"]; + + targetID = [self.targetIDGenerator nextID]; + cached = + [[FSTQueryData alloc] initWithQuery:query targetID:targetID purpose:FSTQueryPurposeListen]; + [self.queryCache addQueryData:cached group:group]; + + [self.persistence commitGroup:group]; + } + + // Sanity check to ensure that even when resuming a query it's not currently active. + FSTBoxedTargetID *boxedTargetID = @(targetID); + FSTAssert(!self.targetIDs[boxedTargetID], @"Tried to allocate an already allocated query: %@", + query); + self.targetIDs[boxedTargetID] = cached; + return cached; +} + +- (void)releaseQuery:(FSTQuery *)query { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Release query"]; + + FSTQueryData *queryData = [self.queryCache queryDataForQuery:query]; + FSTAssert(queryData, @"Tried to release nonexistent query: %@", query); + + [self.localViewReferences removeReferencesForID:queryData.targetID]; + if (self.garbageCollector.isEager) { + [self.queryCache removeQueryData:queryData group:group]; + } + [self.targetIDs removeObjectForKey:@(queryData.targetID)]; + + // If this was the last watch target, then we won't get any more watch snapshots, so we should + // release any held batch results. + if ([self.targetIDs count] == 0) { + FSTRemoteDocumentChangeBuffer *remoteDocuments = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache]; + + [self releaseHeldBatchResultsWithGroup:group remoteDocuments:remoteDocuments]; + + [remoteDocuments applyToWriteGroup:group]; + } + + [self.persistence commitGroup:group]; +} + +- (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query { + return [self.localDocuments documentsMatchingQuery:query]; +} + +- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID { + return [self.queryCache matchingKeysForTargetID:targetID]; +} + +- (void)collectGarbage { + // Call collectGarbage regardless of whether isGCEnabled so the referenceSet doesn't continue to + // accumulate the garbage keys. + NSSet *garbage = [self.garbageCollector collectGarbage]; + if (garbage.count > 0) { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Garbage Collection"]; + for (FSTDocumentKey *key in garbage) { + [self.remoteDocumentCache removeEntryForKey:key group:group]; + } + [self.persistence commitGroup:group]; + } +} + +/** + * Releases all the held mutation batches up to the current remote version received, and + * applies their mutations to the docs in the remote documents cache. + * + * @return the set of keys of docs that were modified by those writes. + */ +- (FSTDocumentKeySet *)releaseHeldBatchResultsWithGroup:(FSTWriteGroup *)group + remoteDocuments: + (FSTRemoteDocumentChangeBuffer *)remoteDocuments { + NSMutableArray *toRelease = [NSMutableArray array]; + for (FSTMutationBatchResult *batchResult in self.heldBatchResults) { + if (![self isRemoteUpToVersion:batchResult.commitVersion]) { + break; + } + [toRelease addObject:batchResult]; + } + + if (toRelease.count == 0) { + return [FSTDocumentKeySet keySet]; + } else { + [self.heldBatchResults removeObjectsInRange:NSMakeRange(0, toRelease.count)]; + return [self releaseBatchResults:toRelease group:group remoteDocuments:remoteDocuments]; + } +} + +- (BOOL)isRemoteUpToVersion:(FSTSnapshotVersion *)version { + // If there are no watch targets, then we won't get remote snapshots, and are always "up-to-date." + return [version compare:self.queryCache.lastRemoteSnapshotVersion] != NSOrderedDescending || + self.targetIDs.count == 0; +} + +- (BOOL)shouldHoldBatchResultWithVersion:(FSTSnapshotVersion *)version { + // Check if watcher isn't up to date or prior results are already held. + return ![self isRemoteUpToVersion:version] || self.heldBatchResults.count > 0; +} + +- (FSTDocumentKeySet *)releaseBatchResults:(NSArray *)batchResults + group:(FSTWriteGroup *)group + remoteDocuments:(FSTRemoteDocumentChangeBuffer *)remoteDocuments { + NSMutableArray *batches = [NSMutableArray array]; + for (FSTMutationBatchResult *batchResult in batchResults) { + [self applyBatchResult:batchResult toRemoteDocuments:remoteDocuments]; + [batches addObject:batchResult.batch]; + } + + return [self removeMutationBatches:batches group:group]; +} + +- (FSTDocumentKeySet *)removeMutationBatch:(FSTMutationBatch *)batch group:(FSTWriteGroup *)group { + return [self removeMutationBatches:@[ batch ] group:group]; +} + +/** Removes all the mutation batches named in the given array. */ +- (FSTDocumentKeySet *)removeMutationBatches:(NSArray *)batches + group:(FSTWriteGroup *)group { + // TODO(klimt): Could this be an NSMutableDictionary? + __block FSTDocumentKeySet *affectedDocs = [FSTDocumentKeySet keySet]; + + for (FSTMutationBatch *batch in batches) { + for (FSTMutation *mutation in batch.mutations) { + FSTDocumentKey *key = mutation.key; + affectedDocs = [affectedDocs setByAddingObject:key]; + } + } + + [self.mutationQueue removeMutationBatches:batches group:group]; + + return affectedDocs; +} + +- (void)applyBatchResult:(FSTMutationBatchResult *)batchResult + toRemoteDocuments:(FSTRemoteDocumentChangeBuffer *)remoteDocuments { + FSTMutationBatch *batch = batchResult.batch; + FSTDocumentKeySet *docKeys = batch.keys; + [docKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) { + FSTMaybeDocument *_Nullable remoteDoc = [remoteDocuments entryForKey:docKey]; + FSTMaybeDocument *_Nullable doc = remoteDoc; + FSTSnapshotVersion *ackVersion = batchResult.docVersions[docKey]; + FSTAssert(ackVersion, @"docVersions should contain every doc in the write."); + if (!doc || [doc.version compare:ackVersion] == NSOrderedAscending) { + doc = [batch applyTo:doc documentKey:docKey mutationBatchResult:batchResult]; + if (!doc) { + FSTAssert(!remoteDoc, @"Mutation batch %@ applied to document %@ resulted in nil.", batch, + remoteDoc); + } else { + [remoteDocuments addEntry:doc]; + } + } + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h new file mode 100644 index 0000000..d44959e --- /dev/null +++ b/Firestore/Source/Local/FSTLocalViewChanges.h @@ -0,0 +1,51 @@ +/* + * 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 + +#import "FSTDocumentKeySet.h" + +@class FSTDocumentKey; +@class FSTDocumentSet; +@class FSTMutation; +@class FSTQuery; +@class FSTRemoteEvent; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A set of changes to what documents are currently in view and out of view for a given query. + * These changes are sent to the LocalStore by the View (via the SyncEngine) and are used to pin / + * unpin documents as appropriate. + */ +@interface FSTLocalViewChanges : NSObject + ++ (instancetype)changesForQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys; + ++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot; + +- (id)init NS_UNAVAILABLE; + +@property(nonatomic, strong, readonly) FSTQuery *query; +@property(nonatomic, strong) FSTDocumentKeySet *addedKeys; +@property(nonatomic, strong) FSTDocumentKeySet *removedKeys; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalViewChanges.m b/Firestore/Source/Local/FSTLocalViewChanges.m new file mode 100644 index 0000000..05407b2 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalViewChanges.m @@ -0,0 +1,76 @@ +/* + * 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 "FSTLocalViewChanges.h" + +#import "FSTDocument.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalViewChanges () +- (instancetype)initWithQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTLocalViewChanges + ++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot { + FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet]; + FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet]; + + for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { + switch (docChange.type) { + case FSTDocumentViewChangeTypeAdded: + addedKeys = [addedKeys setByAddingObject:docChange.document.key]; + break; + + case FSTDocumentViewChangeTypeRemoved: + removedKeys = [removedKeys setByAddingObject:docChange.document.key]; + break; + + default: + // Do nothing. + break; + } + } + + return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys]; +} + ++ (instancetype)changesForQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys { + return + [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys]; +} + +- (instancetype)initWithQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys { + self = [super init]; + if (self) { + _query = query; + _addedKeys = addedKeys; + _removedKeys = removedKeys; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalWriteResult.h b/Firestore/Source/Local/FSTLocalWriteResult.h new file mode 100644 index 0000000..4cd7d34 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalWriteResult.h @@ -0,0 +1,36 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" +#import "FSTTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The result of a write to the local store. */ +@interface FSTLocalWriteResult : NSObject + ++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes; + +- (id)init __attribute__((unavailable("Use resultForBatchID:changes:"))); + +@property(nonatomic, assign, readonly) FSTBatchID batchID; +@property(nonatomic, strong, readonly) FSTMaybeDocumentDictionary *changes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalWriteResult.m b/Firestore/Source/Local/FSTLocalWriteResult.m new file mode 100644 index 0000000..7586686 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalWriteResult.m @@ -0,0 +1,43 @@ +/* + * 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 "FSTLocalWriteResult.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalWriteResult () +- (instancetype)initWithBatchID:(FSTBatchID)batchID + changes:(FSTMaybeDocumentDictionary *)changes NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTLocalWriteResult + ++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { + return [[FSTLocalWriteResult alloc] initWithBatchID:batchID changes:changes]; +} + +- (instancetype)initWithBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { + self = [super init]; + if (self) { + _batchID = batchID; + _changes = changes; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firestore/Source/Local/FSTMemoryMutationQueue.h new file mode 100644 index 0000000..6d917b7 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.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 + +#import "FSTMutationQueue.h" + +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryMutationQueue : NSObject + ++ (instancetype)mutationQueue; + +/** The garbage collector to notify about potential garbage keys. */ +@property(nonatomic, weak, readwrite, nullable) id garbageCollector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.m b/Firestore/Source/Local/FSTMemoryMutationQueue.m new file mode 100644 index 0000000..6118ad6 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.m @@ -0,0 +1,441 @@ +/* + * 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 "FSTMemoryMutationQueue.h" + +#import "FSTAssert.h" +#import "FSTComparison.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentReference.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTPath.h" +#import "FSTQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryMutationQueue () + +/** + * A FIFO queue of all mutations to apply to the backend. Mutations are added to the end of the + * queue as they're written, and removed from the front of the queue as the mutations become + * visible or are rejected. + * + * When successfully applied, mutations must be acknowledged by the write stream and made visible + * on the watch stream. It's possible for the watch stream to fall behind in which case the batches + * at the head of the queue will be acknowledged but held until the watch stream sees the changes. + * + * If a batch is rejected while there are held write acknowledgements at the head of the queue + * the rejected batch is converted to a tombstone: its mutations are removed but the batch remains + * in the queue. This maintains a simple consecutive ordering of batches in the queue. + * + * Once the held write acknowledgements become visible they are removed from the head of the queue + * along with any tombstones that follow. + */ +@property(nonatomic, strong, readonly) NSMutableArray *queue; + +/** An ordered mapping between documents and the mutation batch IDs. */ +@property(nonatomic, strong) FSTImmutableSortedSet *batchesByDocumentKey; + +/** The next value to use when assigning sequential IDs to each mutation batch. */ +@property(nonatomic, assign) FSTBatchID nextBatchID; + +/** The highest acknowledged mutation in the queue. */ +@property(nonatomic, assign) FSTBatchID highestAcknowledgedBatchID; + +/** + * The last received stream token from the server, used to acknowledge which responses the client + * has processed. Stream tokens are opaque checkpoint markers whose only real value is their + * inclusion in the next request. + */ +@property(nonatomic, strong, nullable) NSData *lastStreamToken; + +@end + +@implementation FSTMemoryMutationQueue + ++ (instancetype)mutationQueue { + return [[FSTMemoryMutationQueue alloc] init]; +} + +- (instancetype)init { + if (self = [super init]) { + _queue = [NSMutableArray array]; + _batchesByDocumentKey = + [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; + + _nextBatchID = 1; + _highestAcknowledgedBatchID = kFSTBatchIDUnknown; + } + return self; +} + +#pragma mark - FSTMutationQueue implementation + +- (void)startWithGroup:(FSTWriteGroup *)group { + // Note: The queue may be shutdown / started multiple times, since we maintain the queue for the + // duration of the app session in case a user logs out / back in. To behave like the + // LevelDB-backed MutationQueue (and accommodate tests that expect as much), we reset nextBatchID + // and highestAcknowledgedBatchID if the queue is empty. + if (self.isEmpty) { + self.nextBatchID = 1; + self.highestAcknowledgedBatchID = kFSTBatchIDUnknown; + } + FSTAssert(self.highestAcknowledgedBatchID < self.nextBatchID, + @"highestAcknowledgedBatchID must be less than the nextBatchID"); +} + +- (void)shutdown { +} + +- (BOOL)isEmpty { + // If the queue has any entries at all, the first entry must not be a tombstone (otherwise it + // would have been removed already). + return self.queue.count == 0; +} + +- (FSTBatchID)highestAcknowledgedBatchID { + return _highestAcknowledgedBatchID; +} + +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(__unused FSTWriteGroup *)group { + NSMutableArray *queue = self.queue; + + FSTBatchID batchID = batch.batchID; + FSTAssert(batchID > self.highestAcknowledgedBatchID, + @"Mutation batchIDs must be acknowledged in order"); + + NSInteger batchIndex = [self indexOfExistingBatchID:batchID action:@"acknowledged"]; + + // Verify that the batch in the queue is the one to be acknowledged. + FSTMutationBatch *check = queue[(NSUInteger)batchIndex]; + FSTAssert(batchID == check.batchID, @"Queue ordering failure: expected batch %d, got batch %d", + batchID, check.batchID); + FSTAssert(![check isTombstone], @"Can't acknowledge a previously removed batch"); + + self.highestAcknowledgedBatchID = batchID; + self.lastStreamToken = streamToken; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(__unused FSTWriteGroup *)group { + self.lastStreamToken = streamToken; +} + +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations + group:(FSTWriteGroup *)group { + FSTAssert(mutations.count > 0, @"Mutation batches should not be empty"); + + FSTBatchID batchID = self.nextBatchID; + self.nextBatchID += 1; + + NSMutableArray *queue = self.queue; + if (queue.count > 0) { + FSTMutationBatch *prior = queue[queue.count - 1]; + FSTAssert(prior.batchID < batchID, @"Mutation batchIDs must be monotonically increasing order"); + } + + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; + [queue addObject:batch]; + + // Track references by document key. + FSTImmutableSortedSet *references = self.batchesByDocumentKey; + for (FSTMutation *mutation in batch.mutations) { + references = [references + setByAddingObject:[[FSTDocumentReference alloc] initWithKey:mutation.key ID:batchID]]; + } + self.batchesByDocumentKey = references; + + return batch; +} + +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + + NSInteger index = [self indexOfBatchID:batchID]; + if (index < 0 || index >= queue.count) { + return nil; + } + + FSTMutationBatch *batch = queue[(NSUInteger)index]; + FSTAssert(batch.batchID == batchID, @"If found batch must match"); + return [batch isTombstone] ? nil : batch; +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + NSUInteger count = queue.count; + + // All batches with batchID <= self.highestAcknowledgedBatchID have been acknowledged so the + // first unacknowledged batch after batchID will have a batchID larger than both of these values. + batchID = MAX(batchID + 1, self.highestAcknowledgedBatchID); + + // The requested batchID may still be out of range so normalize it to the start of the queue. + NSInteger rawIndex = [self indexOfBatchID:batchID]; + NSUInteger index = rawIndex < 0 ? 0 : (NSUInteger)rawIndex; + + // Finally return the first non-tombstone batch. + for (; index < count; index++) { + FSTMutationBatch *batch = queue[index]; + if (![batch isTombstone]) { + return batch; + } + } + + return nil; +} + +- (NSArray *)allMutationBatches { + return [self allLiveMutationBatchesBeforeIndex:self.queue.count]; +} + +- (NSArray *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + NSUInteger count = queue.count; + + NSInteger endIndex = [self indexOfBatchID:batchID]; + if (endIndex < 0) { + endIndex = 0; + } else if (endIndex >= count) { + endIndex = count; + } else { + // The endIndex is in the queue so increment to pull everything in the queue including it. + endIndex += 1; + } + + return [self allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex]; +} + +- (NSArray *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey { + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:documentKey ID:0]; + + NSMutableArray *result = [NSMutableArray array]; + FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { + if (![documentKey isEqualToKey:reference.key]) { + *stop = YES; + return; + } + + FSTMutationBatch *batch = [self lookupMutationBatch:reference.ID]; + FSTAssert(batch, @"Batches in the index must exist in the main table"); + [result addObject:batch]; + }; + + [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; + return result; +} + +- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { + // Use the query path as a prefix for testing if a document matches the query. + FSTResourcePath *prefix = query.path; + int immediateChildrenPathLength = prefix.length + 1; + + // Construct a document reference for actually scanning the index. Unlike the prefix, the document + // key in this reference must have an even number of segments. The empty segment can be used as + // a suffix of the query path because it precedes all other segments in an ordered traversal. + FSTResourcePath *startPath = query.path; + if (![FSTDocumentKey isDocumentKey:startPath]) { + startPath = [startPath pathByAppendingSegment:@""]; + } + FSTDocumentReference *start = + [[FSTDocumentReference alloc] initWithKey:[FSTDocumentKey keyWithPath:startPath] ID:0]; + + // Find unique batchIDs referenced by all documents potentially matching the query. + __block FSTImmutableSortedSet *uniqueBatchIDs = + [FSTImmutableSortedSet setWithComparator:FSTNumberComparator]; + FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { + FSTResourcePath *rowKeyPath = reference.key.path; + if (![prefix isPrefixOfPath:rowKeyPath]) { + *stop = YES; + return; + } + + // Rows with document keys more than one segment longer than the query path can't be matches. + // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. + // TODO(mcg): we'll need a different scanner when we implement ancestor queries. + if (rowKeyPath.length != immediateChildrenPathLength) { + return; + } + + uniqueBatchIDs = [uniqueBatchIDs setByAddingObject:@(reference.ID)]; + }; + [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; + + // Construct an array of matching batches, sorted by batchID to ensure that multiple mutations + // affecting the same document key are applied in order. + NSMutableArray *result = [NSMutableArray array]; + [uniqueBatchIDs enumerateObjectsUsingBlock:^(NSNumber *batchID, BOOL *stop) { + FSTMutationBatch *batch = [self lookupMutationBatch:[batchID intValue]]; + if (batch) { + [result addObject:batch]; + } + }]; + + return result; +} + +- (void)removeMutationBatches:(NSArray *)batches group:(FSTWriteGroup *)group { + NSUInteger batchCount = batches.count; + FSTAssert(batchCount > 0, @"Should not remove mutations when none exist."); + + FSTBatchID firstBatchID = batches[0].batchID; + + NSMutableArray *queue = self.queue; + NSUInteger queueCount = queue.count; + + // Find the position of the first batch for removal. This need not be the first entry in the + // queue. + NSUInteger startIndex = [self indexOfExistingBatchID:firstBatchID action:@"removed"]; + FSTAssert(queue[startIndex].batchID == firstBatchID, @"Removed batches must exist in the queue"); + + // Check that removed batches are contiguous (while excluding tombstones). + NSUInteger batchIndex = 1; + NSUInteger queueIndex = startIndex + 1; + while (batchIndex < batchCount && queueIndex < queueCount) { + FSTMutationBatch *batch = queue[queueIndex]; + if ([batch isTombstone]) { + queueIndex++; + continue; + } + + FSTAssert(batch.batchID == batches[batchIndex].batchID, + @"Removed batches must be contiguous in the queue"); + batchIndex++; + queueIndex++; + } + + // Only actually remove batches if removing at the front of the queue. Previously rejected batches + // may have left tombstones in the queue, so expand the removal range to include any tombstones. + if (startIndex == 0) { + for (; queueIndex < queueCount; queueIndex++) { + FSTMutationBatch *batch = queue[queueIndex]; + if (![batch isTombstone]) { + break; + } + } + + NSUInteger length = queueIndex - startIndex; + [queue removeObjectsInRange:NSMakeRange(startIndex, length)]; + + } else { + // Mark tombstones + for (NSUInteger i = startIndex; i < queueIndex; i++) { + queue[i] = [queue[i] toTombstone]; + } + } + + // Remove entries from the index too. + id garbageCollector = self.garbageCollector; + FSTImmutableSortedSet *references = self.batchesByDocumentKey; + for (FSTMutationBatch *batch in batches) { + FSTBatchID batchID = batch.batchID; + for (FSTMutation *mutation in batch.mutations) { + FSTDocumentKey *key = mutation.key; + [garbageCollector addPotentialGarbageKey:key]; + + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID]; + references = [references setByRemovingObject:reference]; + } + } + self.batchesByDocumentKey = references; +} + +- (void)performConsistencyCheck { + if (self.queue.count == 0) { + FSTAssert([self.batchesByDocumentKey isEmpty], + @"Document leak -- detected dangling mutation references when queue is empty."); + } +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)key { + // Create a reference with a zero ID as the start position to find any document reference with + // this key. + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; + + NSEnumerator *enumerator = + [self.batchesByDocumentKey objectEnumeratorFrom:reference]; + FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; + return [firstKey isEqual:key]; +} + +#pragma mark - Helpers + +/** + * A private helper that collects all the mutation batches in the queue up to but not including + * the given endIndex. All tombstones in the queue are excluded. + */ +- (NSArray *)allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:endIndex]; + + NSUInteger index = 0; + for (FSTMutationBatch *batch in self.queue) { + if (index++ >= endIndex) break; + + if (![batch isTombstone]) { + [result addObject:batch]; + } + } + + return result; +} + +/** + * Finds the index of the given batchID in the mutation queue. This operation is O(1). + * + * @return The computed index of the batch with the given batchID, based on the state of the + * queue. Note this index can negative if the requested batchID has already been removed from + * the queue or past the end of the queue if the batchID is larger than the last added batch. + */ +- (NSInteger)indexOfBatchID:(FSTBatchID)batchID { + NSMutableArray *queue = self.queue; + NSUInteger count = queue.count; + if (count == 0) { + // As an index this is past the end of the queue + return 0; + } + + // Examine the front of the queue to figure out the difference between the batchID and indexes + // in the array. Note that since the queue is ordered by batchID, if the first batch has a larger + // batchID then the requested batchID doesn't exist in the queue. + FSTMutationBatch *firstBatch = queue[0]; + FSTBatchID firstBatchID = firstBatch.batchID; + return batchID - firstBatchID; +} + +/** + * Finds the index of the given batchID in the mutation queue and asserts that the resulting + * index is within the bounds of the queue. + * + * @param batchID The batchID to search for + * @param action A description of what the caller is doing, phrased in passive form (e.g. + * "acknowledged" in a routine that acknowledges batches). + */ +- (NSUInteger)indexOfExistingBatchID:(FSTBatchID)batchID action:(NSString *)action { + NSInteger index = [self indexOfBatchID:batchID]; + FSTAssert(index >= 0 && index < self.queue.count, @"Batches must exist to be %@", action); + return (NSUInteger)index; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.h b/Firestore/Source/Local/FSTMemoryPersistence.h new file mode 100644 index 0000000..c52962a --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryPersistence.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 + +#import "FSTPersistence.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An in-memory implementation of the FSTPersistence protocol. Values are stored only in RAM and + * are never persisted to any durable storage. + */ +@interface FSTMemoryPersistence : NSObject + ++ (instancetype)persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.m b/Firestore/Source/Local/FSTMemoryPersistence.m new file mode 100644 index 0000000..9caf3e7 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryPersistence.m @@ -0,0 +1,107 @@ +/* + * 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 "FSTMemoryPersistence.h" + +#import "FSTAssert.h" +#import "FSTMemoryMutationQueue.h" +#import "FSTMemoryQueryCache.h" +#import "FSTMemoryRemoteDocumentCache.h" +#import "FSTUser.h" +#import "FSTWriteGroup.h" +#import "FSTWriteGroupTracker.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryPersistence () +@property(nonatomic, strong, nonnull) FSTWriteGroupTracker *writeGroupTracker; +@property(nonatomic, strong, nonnull) + NSMutableDictionary> *mutationQueues; +@property(nonatomic, assign, getter=isStarted) BOOL started; +@end + +@implementation FSTMemoryPersistence { + /** + * The FSTQueryCache representing the persisted cache of queries. + * + * Note that this is retained here to make it easier to write tests affecting both the in-memory + * and LevelDB-backed persistence layers. Tests can create a new FSTLocalStore wrapping this + * FSTPersistence instance and this will make the in-memory persistence layer behave as if it + * were actually persisting values. + */ + FSTMemoryQueryCache *_queryCache; + + /** The FSTRemoteDocumentCache representing the persisted cache of remote documents. */ + FSTMemoryRemoteDocumentCache *_remoteDocumentCache; +} + ++ (instancetype)persistence { + return [[FSTMemoryPersistence alloc] init]; +} + +- (instancetype)init { + if (self = [super init]) { + _writeGroupTracker = [FSTWriteGroupTracker tracker]; + _queryCache = [[FSTMemoryQueryCache alloc] init]; + _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init]; + _mutationQueues = [NSMutableDictionary dictionary]; + } + return self; +} + +- (BOOL)start:(NSError **)error { + // No durable state to read on startup. + FSTAssert(!self.isStarted, @"FSTMemoryPersistence double-started!"); + self.started = YES; + return YES; +} + +- (void)shutdown { + // No durable state to ensure is closed on shutdown. + FSTAssert(self.isStarted, @"FSTMemoryPersistence shutdown without start!"); + self.started = NO; +} + +- (id)mutationQueueForUser:(FSTUser *)user { + id queue = self.mutationQueues[user]; + if (!queue) { + queue = [FSTMemoryMutationQueue mutationQueue]; + self.mutationQueues[user] = queue; + } + return queue; +} + +- (id)queryCache { + return _queryCache; +} + +- (id)remoteDocumentCache { + return _remoteDocumentCache; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + return [self.writeGroupTracker startGroupWithAction:action]; +} + +- (void)commitGroup:(FSTWriteGroup *)group { + [self.writeGroupTracker endGroup:group]; + + FSTAssert(group.isEmpty, @"Memory persistence shouldn't use write groups: %@", group.action); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.h b/Firestore/Source/Local/FSTMemoryQueryCache.h new file mode 100644 index 0000000..58e0133 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryQueryCache.h @@ -0,0 +1,30 @@ +/* + * 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 + +#import "FSTQueryCache.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of the FSTQueryCache protocol that merely keeps queries in memory, suitable + * for online only clients with persistence disabled. + */ +@interface FSTMemoryQueryCache : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.m b/Firestore/Source/Local/FSTMemoryQueryCache.m new file mode 100644 index 0000000..1466caa --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryQueryCache.m @@ -0,0 +1,131 @@ +/* + * 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 "FSTMemoryQueryCache.h" + +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTReferenceSet.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryQueryCache () + +/** Maps a query to the data about that query. */ +@property(nonatomic, strong, readonly) NSMutableDictionary *queries; + +/** A ordered bidirectional mapping between documents and the remote target IDs. */ +@property(nonatomic, strong, readonly) FSTReferenceSet *references; + +/** The highest numbered target ID encountered. */ +@property(nonatomic, assign) FSTTargetID highestTargetID; + +@end + +@implementation FSTMemoryQueryCache { + /** The last received snapshot version. */ + FSTSnapshotVersion *_lastRemoteSnapshotVersion; +} + +- (instancetype)init { + if (self = [super init]) { + _queries = [NSMutableDictionary dictionary]; + _references = [[FSTReferenceSet alloc] init]; + _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion]; + } + return self; +} + +#pragma mark - FSTQueryCache implementation +#pragma mark Query tracking + +- (void)start { + // Nothing to do. +} + +- (void)shutdown { + // No resources to release. +} + +- (FSTTargetID)highestTargetID { + return _highestTargetID; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return _lastRemoteSnapshotVersion; +} + +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group { + _lastRemoteSnapshotVersion = snapshotVersion; +} + +- (void)addQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { + self.queries[queryData.query] = queryData; + if (queryData.targetID > self.highestTargetID) { + self.highestTargetID = queryData.targetID; + } +} + +- (void)removeQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { + [self.queries removeObjectForKey:queryData.query]; + [self.references removeReferencesForID:queryData.targetID]; +} + +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return self.queries[query]; +} + +#pragma mark Reference tracking + +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(__unused FSTWriteGroup *)group { + [self.references addReferencesToKeys:keys forID:targetID]; +} + +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(__unused FSTWriteGroup *)group { + [self.references removeReferencesToKeys:keys forID:targetID]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(__unused FSTWriteGroup *)group { + [self.references removeReferencesForID:targetID]; +} + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { + return [self.references referencedKeysForID:targetID]; +} + +#pragma mark - FSTGarbageSource implementation + +- (nullable id)garbageCollector { + return self.references.garbageCollector; +} + +- (void)setGarbageCollector:(nullable id)garbageCollector { + self.references.garbageCollector = garbageCollector; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + return [self.references containsKey:key]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h new file mode 100644 index 0000000..aca0ca1 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.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 + +#import "FSTRemoteDocumentCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryRemoteDocumentCache : NSObject + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m new file mode 100644 index 0000000..175be43 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m @@ -0,0 +1,84 @@ +/* + * 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 "FSTMemoryRemoteDocumentCache.h" + +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" +#import "FSTQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryRemoteDocumentCache () + +/** Underlying cache of documents. */ +@property(nonatomic, strong) FSTMaybeDocumentDictionary *docs; + +@end + +@implementation FSTMemoryRemoteDocumentCache + +- (instancetype)init { + if (self = [super init]) { + _docs = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + } + return self; +} + +- (void)shutdown { +} + +- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group { + self.docs = [self.docs dictionaryBySettingObject:document forKey:document.key]; +} + +- (void)removeEntryForKey:(FSTDocumentKey *)key group:(FSTWriteGroup *)group { + self.docs = [self.docs dictionaryByRemovingObjectForKey:key]; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)key { + return self.docs[key]; +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; + + // Documents are ordered by key, so we can use a prefix scan to narrow down the documents + // we need to match the query against. + FSTDocumentKey *prefix = [FSTDocumentKey keyWithPath:[query.path pathByAppendingSegment:@""]]; + NSEnumerator *enumerator = [self.docs keyEnumeratorFrom:prefix]; + for (FSTDocumentKey *key in enumerator) { + if (![query.path isPrefixOfPath:key.path]) { + break; + } + FSTMaybeDocument *maybeDoc = self.docs[key]; + if (![maybeDoc isKindOfClass:[FSTDocument class]]) { + continue; + } + FSTDocument *doc = (FSTDocument *)maybeDoc; + if ([query matchesDocument:doc]) { + result = [result dictionaryBySettingObject:doc forKey:doc.key]; + } + } + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMutationQueue.h b/Firestore/Source/Local/FSTMutationQueue.h new file mode 100644 index 0000000..c822b96 --- /dev/null +++ b/Firestore/Source/Local/FSTMutationQueue.h @@ -0,0 +1,159 @@ +/* + * 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 + +#import "FSTGarbageCollector.h" +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTQuery; +@class FSTTimestamp; +@class FSTWriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMutationQueue + +/** A queue of mutations to apply to the remote store. */ +@protocol FSTMutationQueue + +/** + * Starts the mutation queue, performing any initial reads that might be required to establish + * invariants, etc. + * + * After starting, the mutation queue must guarantee that the highestAcknowledgedBatchID is less + * than nextBatchID. This prevents the local store from creating new batches that the mutation + * queue would consider erroneously acknowledged. + */ +- (void)startWithGroup:(FSTWriteGroup *)group; + +/** Shuts this mutation queue down, closing open files, etc. */ +- (void)shutdown; + +/** Returns YES if this queue contains no mutation batches. */ +- (BOOL)isEmpty; + +/** + * Returns the next FSTBatchID that will be assigned to a new mutation batch. + * + * Callers generally don't care about this value except to test that the mutation queue is + * properly maintaining the invariant that highestAcknowledgedBatchID is less than nextBatchID. + */ +- (FSTBatchID)nextBatchID; + +/** + * Returns the highest batchID that has been acknowledged. If no batches have been acknowledged + * or if there are no batches in the queue this can return kFSTBatchIDUnknown. + */ +- (FSTBatchID)highestAcknowledgedBatchID; + +/** Acknowledges the given batch. */ +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(FSTWriteGroup *)group; + +/** Returns the current stream token for this mutation queue. */ +- (nullable NSData *)lastStreamToken; + +/** Sets the stream token for this mutation queue. */ +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(FSTWriteGroup *)group; + +/** Creates a new mutation batch and adds it to this mutation queue. */ +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations + group:(FSTWriteGroup *)group; + +/** Loads the mutation batch with the given batchID. */ +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID; + +/** + * Gets the first unacknowledged mutation batch after the passed in batchId in the mutation queue + * or nil if empty. + * + * @param batchID The batch to search after, or kFSTBatchIDUnknown for the first mutation in the + * queue. + * + * @return the next mutation or nil if there wasn't one. + */ +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID; + +/** Gets all mutation batches in the mutation queue. */ +// TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can provide that +// cheaply, we should replace this. +- (NSArray *)allMutationBatches; + +/** + * Finds all mutations with a batchID less than or equal to the given batchID. + * + * Generally the caller should be asking for the next unacknowledged batchID and the number of + * acknowledged batches should be very small when things are functioning well. + * + * @param batchID The batch to search through. + * + * @return an NSArray containing all batches with matching batchIDs. + */ +// TODO(mcg): This should really return NSEnumerator and the caller should be adjusted to only +// loop through these once. +- (NSArray *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID; + +/** + * Finds all mutation batches that could @em possibly affect the given document key. Not all + * mutations in a batch will necessarily affect the document key, so when looping through the + * batch you'll need to check that the mutation itself matches the key. + * + * Note that because of this requirement implementations are free to return mutation batches that + * don't contain the document key at all if it's convenient. + */ +// TODO(mcg): This should really return an NSEnumerator +// also for b/32992024, all backing stores should really index by document key +- (NSArray *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey; + +/** + * Finds all mutation batches that could affect the results for the given query. Not all + * mutations in a batch will necessarily affect the query, so when looping through the batch + * you'll need to check that the mutation itself matches the query. + * + * Note that because of this requirement implementations are free to return mutation batches that + * don't match the query at all if it's convenient. + * + * NOTE: A FSTPatchMutation does not need to include all fields in the query filter criteria in + * order to be a match (but any fields it does contain do need to match). + */ +// TODO(mikelehen): This should perhaps return an NSEnumerator, though I'm not sure we can avoid +// loading them all in memory. +- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query; + +/** + * Removes the given mutation batches from the queue. This is useful in two circumstances: + * + * + Removing applied mutations from the head of the queue + * + Removing rejected mutations from anywhere in the queue + * + * In both cases, the array of mutations to remove must be a contiguous range of batchIds. This is + * most easily accomplished by loading mutations with @a -allMutationBatchesThroughBatchID:. + */ +- (void)removeMutationBatches:(NSArray *)batches group:(FSTWriteGroup *)group; + +/** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ +- (void)performConsistencyCheck; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.h b/Firestore/Source/Local/FSTNoOpGarbageCollector.h new file mode 100644 index 0000000..8873a1b --- /dev/null +++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.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 + +#import "FSTGarbageCollector.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A garbage collector implementation that does absolutely nothing. It ignores all + * addGarbageSource: and addPotentialGarbageKey: messages and never produces any garbage. + */ +@interface FSTNoOpGarbageCollector : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.m b/Firestore/Source/Local/FSTNoOpGarbageCollector.m new file mode 100644 index 0000000..6e035ab --- /dev/null +++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.m @@ -0,0 +1,45 @@ +/* + * 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 "FSTNoOpGarbageCollector.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTNoOpGarbageCollector + +- (BOOL)isEager { + return NO; +} + +- (void)addGarbageSource:(id)garbageSource { + // Not tracking garbage so don't track sources. +} + +- (void)removeGarbageSource:(id)garbageSource { + // Not tracking garbage so don't track sources. +} + +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { + // Not tracking garbage so ignore. +} + +- (NSSet *)collectGarbage { + return [NSSet set]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h new file mode 100644 index 0000000..cf07a9e --- /dev/null +++ b/Firestore/Source/Local/FSTPersistence.h @@ -0,0 +1,103 @@ +/* + * 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 + +@class FSTUser; +@class FSTWriteGroup; +@protocol FSTMutationQueue; +@protocol FSTQueryCache; +@protocol FSTRemoteDocumentCache; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTPersistence is the lowest-level shared interface to persistent storage in Firestore. + * + * FSTPersistence is used to create FSTMutationQueue and FSTRemoteDocumentCache instances backed + * by persistence (which might be in-memory or LevelDB). + * + * FSTPersistence also exposes an API to create and commit FSTWriteGroup instances. + * Implementations of FSTWriteGroup/FSTPersistence only need to guarantee that writes made + * against the FSTWriteGroup are not made to durable storage until commitGroup:action: is called + * here. Since memory-only storage components do not alter durable storage, they are free to ignore + * the group. + * + * This contract is enough to allow the FSTLocalStore be be written independently of whether or not + * the stored state actually is durably persisted. If persistent storage is enabled, writes are + * grouped together to avoid inconsistent state that could cause crashes. + * + * Concretely, when persistent storage is enabled, the persistent versions of FSTMutationQueue, + * FSTRemoteDocumentCache, and others (the mutators) will defer their writes into an FSTWriteGroup. + * Once the local store has completed one logical operation, it commits the write group using + * [FSTPersistence commitGroup:action:]. + * + * When persistent storage is disabled, the non-persistent versions of the mutators ignore the + * FSTWriteGroup and [FSTPersistence commitGroup:action:] is a no-op. This short-cut is allowed + * because memory-only storage leaves no state so it cannot be inconsistent. + * + * This simplifies the implementations of the mutators and allows memory-only implementations to + * supplement the persistent ones without requiring any special dual-store implementation of + * FSTPersistence. The cost is that the FSTLocalStore needs to be slightly careful about the order + * of its reads and writes in order to avoid relying on being able to read back uncommitted writes. + */ +@protocol FSTPersistence + +/** + * Starts persistent storage, opening the database or similar. + * + * @param error An error object that will be populated if startup fails. + * @return YES if persistent storage started successfully, NO otherwise. + */ +- (BOOL)start:(NSError **)error; + +/** Releases any resources held during eager shutdown. */ +- (void)shutdown; + +/** + * Returns an FSTMutationQueue representing the persisted mutations for the given user. + * + *

Note: The implementation is free to return the same instance every time this is called for a + * given user. In particular, the memory-backed implementation does this to emulate the persisted + * implementation to the extent possible (e.g. in the case of uid switching from + * sally=>jack=>sally, sally's mutation queue will be preserved). + */ +- (id)mutationQueueForUser:(FSTUser *)user; + +/** Creates an FSTQueryCache representing the persisted cache of queries. */ +- (id)queryCache; + +/** Creates an FSTRemoteDocumentCache representing the persisted cache of remote documents. */ +- (id)remoteDocumentCache; + +/** + * Creates an FSTWriteGroup with the specified action description. + * + * @param action A description of the action performed by this group, used for logging. + * @return The created group. + */ +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action; + +/** + * Commits all accumulated changes in the given group. If there are no changes this is a no-op. + * + * @param group The group of changes to write as a unit. + */ +- (void)commitGroup:(FSTWriteGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryCache.h b/Firestore/Source/Local/FSTQueryCache.h new file mode 100644 index 0000000..87ee342 --- /dev/null +++ b/Firestore/Source/Local/FSTQueryCache.h @@ -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 + +#import "FSTDocumentKeySet.h" +#import "FSTGarbageCollector.h" +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTDocumentSet; +@class FSTMaybeDocument; +@class FSTQuery; +@class FSTQueryData; +@class FSTWriteGroup; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents cached queries received from the remote backend. This contains both a mapping between + * queries and the documents that matched them according to the server, but also metadata about the + * queries. + * + * The cache is keyed by FSTQuery and entries in the cache are FSTQueryData instances. + */ +@protocol FSTQueryCache + +/** Starts the query cache up. */ +- (void)start; + +/** Shuts this cache down, closing open files, etc. */ +- (void)shutdown; + +/** + * Returns the highest target ID of any query in the cache. Typically called during startup to + * seed a target ID generator and avoid collisions with existing queries. If there are no queries + * in the cache, returns zero. + */ +- (FSTTargetID)highestTargetID; + +/** + * A global snapshot version representing the last consistent snapshot we received from the + * backend. This is monotonically increasing and any snapshots received from the backend prior to + * this version (e.g. for targets resumed with a resume_token) should be suppressed (buffered) + * until the backend has caught up to this snapshot version again. This prevents our cache from + * ever going backwards in time. + * + * This is updated whenever our we get a TargetChange with a read_time and empty target_ids. + */ +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; + +/** + * Set the snapshot version representing the last consistent snapshot received from the backend. + * (see -lastRemoteSnapshotVersion for more details). + * + * @param snapshotVersion The new snapshot version. + */ +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group; + +/** + * Adds or replaces an entry in the cache. + * + * The cache key is extracted from `queryData.query`. If there is already a cache entry for the + * key, it will be replaced. + * + * @param queryData An FSTQueryData instance to put in the cache. + */ +- (void)addQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group; + +/** Removes the cached entry for the given query data (no-op if no entry exists). */ +- (void)removeQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group; + +/** + * Looks up an FSTQueryData entry in the cache. + * + * @param query The query corresponding to the entry to look up. + * @return The cached FSTQueryData entry, or nil if the cache has no entry for the query. + */ +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query; + +/** Adds the given document keys to cached query results of the given target ID. */ +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group; + +/** Removes the given document keys from the cached query results of the given target ID. */ +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group; + +/** Removes all the keys in the query results of the given target ID. */ +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(FSTWriteGroup *)group; + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.h b/Firestore/Source/Local/FSTQueryData.h new file mode 100644 index 0000000..060fd78 --- /dev/null +++ b/Firestore/Source/Local/FSTQueryData.h @@ -0,0 +1,82 @@ +/* + * 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 + +#import "FSTTypes.h" + +@class FSTQuery; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** An enumeration of the different purposes we have for queries. */ +typedef NS_ENUM(NSInteger, FSTQueryPurpose) { + /** A regular, normal query. */ + FSTQueryPurposeListen, + + /** The query was used to refill a query after an existence filter mismatch. */ + FSTQueryPurposeExistenceFilterMismatch, + + /** The query was used to resolve a limbo document. */ + FSTQueryPurposeLimboResolution, +}; + +/** An immutable set of metadata that the store will need to keep track of for each query. */ +@interface FSTQueryData : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken NS_DESIGNATED_INITIALIZER; + +/** Convenience initializer for use when creating an FSTQueryData for the first time. */ +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose; + +- (instancetype)init NS_UNAVAILABLE; + +/** Creates a new query data instance with an updated snapshot version and resume token. */ +- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken; + +/** The query being listened to. */ +@property(nonatomic, strong, readonly) FSTQuery *query; + +/** + * The targetID to which the query corresponds, assigned by the FSTLocalStore for user queries or + * the FSTSyncEngine for limbo queries. + */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** The purpose of the query. */ +@property(nonatomic, assign, readonly) FSTQueryPurpose purpose; + +/** The latest snapshot version seen for this target. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** + * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting + * without retransmitting all the data that matches the query. The resume token essentially + * identifies a point in time from which the server should resume sending results. + */ +@property(nonatomic, copy, readonly) NSData *resumeToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.m b/Firestore/Source/Local/FSTQueryData.m new file mode 100644 index 0000000..438f229 --- /dev/null +++ b/Firestore/Source/Local/FSTQueryData.m @@ -0,0 +1,93 @@ +/* + * 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 "FSTQueryData.h" + +#import "FSTQuery.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryData + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + self = [super init]; + if (self) { + _query = query; + _targetID = targetID; + _purpose = purpose; + _snapshotVersion = snapshotVersion; + _resumeToken = [resumeToken copy]; + } + return self; +} + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose { + return [self initWithQuery:query + targetID:targetID + purpose:purpose + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTQueryData class]]) { + return NO; + } + + FSTQueryData *other = (FSTQueryData *)object; + return [self.query isEqual:other.query] && self.targetID == other.targetID && + self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] && + [self.resumeToken isEqual:other.resumeToken]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.query hash]; + result = result * 31 + self.targetID; + result = result * 31 + self.purpose; + result = result * 31 + [self.snapshotVersion hash]; + result = result * 31 + [self.resumeToken hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", + self.query, self.targetID, (unsigned long)self.purpose, self.snapshotVersion, + self.resumeToken]; +} + +- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + return [[FSTQueryData alloc] initWithQuery:self.query + targetID:self.targetID + purpose:self.purpose + snapshotVersion:snapshotVersion + resumeToken:resumeToken]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTReferenceSet.h b/Firestore/Source/Local/FSTReferenceSet.h new file mode 100644 index 0000000..e4f50a7 --- /dev/null +++ b/Firestore/Source/Local/FSTReferenceSet.h @@ -0,0 +1,71 @@ +/* + * 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 + +#import "FSTDocumentKeySet.h" +#import "FSTGarbageCollector.h" +#import "FSTTypes.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A collection of references to a document from some kind of numbered entity (either a targetID or + * batchID). As references are added to or removed from the set corresponding events are emitted to + * a registered garbage collector. + * + * Each reference is represented by a FSTDocumentReference object. Each of them contains enough + * information to uniquely identify the reference. They are all stored primarily in a set sorted + * by key. A document is considered garbage if there's no references in that set (this can be + * efficiently checked thanks to sorting by key). + * + * FSTReferenceSet also keeps a secondary set that contains references sorted by IDs. This one is + * used to efficiently implement removal of all references by some target ID. + */ +@interface FSTReferenceSet : NSObject + +/** Keeps track of keys that have references. */ +@property(nonatomic, weak, readwrite, nullable) id garbageCollector; + +/** Returns YES if the reference set contains no references. */ +- (BOOL)isEmpty; + +/** Adds a reference to the given document key for the given ID. */ +- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID; + +/** Add references to the given document keys for the given ID. */ +- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; + +/** Removes a reference to the given document key for the given ID. */ +- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID; + +/** Removes references to the given document keys for the given ID. */ +- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; + +/** Clears all references with a given ID. Calls -removeReferenceToKey: for each key removed. */ +- (void)removeReferencesForID:(int)ID; + +/** Clears all references for all IDs. */ +- (void)removeAllReferences; + +/** Returns all of the document keys that have had references added for the given ID. */ +- (FSTDocumentKeySet *)referencedKeysForID:(int)ID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTReferenceSet.m b/Firestore/Source/Local/FSTReferenceSet.m new file mode 100644 index 0000000..2326ded --- /dev/null +++ b/Firestore/Source/Local/FSTReferenceSet.m @@ -0,0 +1,135 @@ +/* + * 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 "FSTReferenceSet.h" + +#import "FSTDocumentKey.h" +#import "FSTDocumentReference.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTReferenceSet + +@interface FSTReferenceSet () + +/** A set of outstanding references to a document sorted by key. */ +@property(nonatomic, strong) FSTImmutableSortedSet *referencesByKey; + +/** A set of outstanding references to a document sorted by target ID (or batch ID). */ +@property(nonatomic, strong) FSTImmutableSortedSet *referencesByID; + +@end + +@implementation FSTReferenceSet + +#pragma mark - Initializer + +- (instancetype)init { + self = [super init]; + if (self) { + _referencesByKey = + [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; + _referencesByID = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByID]; + } + return self; +} + +#pragma mark - Testing helper methods + +- (BOOL)isEmpty { + return [self.referencesByKey isEmpty]; +} + +- (NSUInteger)count { + return self.referencesByKey.count; +} + +#pragma mark - Public methods + +- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:ID]; + self.referencesByKey = [self.referencesByKey setByAddingObject:reference]; + self.referencesByID = [self.referencesByID setByAddingObject:reference]; +} + +- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [self addReferenceToKey:key forID:ID]; + }]; +} + +- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { + [self removeReference:[[FSTDocumentReference alloc] initWithKey:key ID:ID]]; +} + +- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [self removeReferenceToKey:key forID:ID]; + }]; +} + +- (void)removeReferencesForID:(int)ID { + FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; + FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; + + [self.referencesByID enumerateObjectsFrom:start + to:end + usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { + [self removeReference:reference]; + }]; +} + +- (void)removeAllReferences { + for (FSTDocumentReference *reference in self.referencesByKey.objectEnumerator) { + [self removeReference:reference]; + } +} + +- (void)removeReference:(FSTDocumentReference *)reference { + self.referencesByKey = [self.referencesByKey setByRemovingObject:reference]; + self.referencesByID = [self.referencesByID setByRemovingObject:reference]; + [self.garbageCollector addPotentialGarbageKey:reference.key]; +} + +- (FSTDocumentKeySet *)referencedKeysForID:(int)ID { + FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; + FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; + + __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + [self.referencesByID enumerateObjectsFrom:start + to:end + usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { + keys = [keys setByAddingObject:reference.key]; + }]; + return keys; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + // Create a reference with a zero ID as the start position to find any document reference with + // this key. + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; + + NSEnumerator *enumerator = + [self.referencesByKey objectEnumeratorFrom:reference]; + FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; + return [firstKey isEqual:reference.key]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentCache.h b/Firestore/Source/Local/FSTRemoteDocumentCache.h new file mode 100644 index 0000000..8979455 --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentCache.h @@ -0,0 +1,76 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" + +@class FSTDocumentKey; +@class FSTMaybeDocument; +@class FSTQuery; +@class FSTWriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents cached documents received from the remote backend. + * + * The cache is keyed by FSTDocumentKey and entries in the cache are FSTMaybeDocument instances, + * meaning we can cache both FSTDocument instances (an actual document with data) as well as + * FSTDeletedDocument instances (indicating that the document is known to not exist). + */ +@protocol FSTRemoteDocumentCache + +/** Shuts this cache down, closing open files, etc. */ +- (void)shutdown; + +/** + * Adds or replaces an entry in the cache. + * + * The cache key is extracted from `maybeDocument.key`. If there is already a cache entry for + * the key, it will be replaced. + * + * @param maybeDocument A FSTDocument or FSTDeletedDocument to put in the cache. + */ +- (void)addEntry:(FSTMaybeDocument *)maybeDocument group:(FSTWriteGroup *)group; + +/** Removes the cached entry for the given key (no-op if no entry exists). */ +- (void)removeEntryForKey:(FSTDocumentKey *)documentKey group:(FSTWriteGroup *)group; + +/** + * Looks up an entry in the cache. + * + * @param documentKey The key of the entry to look up. + * @return The cached FSTDocument or FSTDeletedDocument entry, or nil if we have nothing cached. + */ +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey; + +/** + * Executes a query against the cached FSTDocument entries + * + * Implementations may return extra documents if convenient. The results should be re-filtered + * by the consumer before presenting them to the user. + * + * Cached FSTDeletedDocument entries have no bearing on query results. + * + * @param query The query to match documents against. + * @return The set of matching documents. + */ +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h new file mode 100644 index 0000000..be0d609 --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h @@ -0,0 +1,66 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@protocol FSTRemoteDocumentCache; +@class FSTMaybeDocument; +@class FSTDocumentKey; +@class FSTWriteGroup; + +/** + * An in-memory buffer of entries to be written to an FSTRemoteDocumentCache. It can be used to + * batch up a set of changes to be written to the cache, but additionally supports reading entries + * back with the `entryForKey:` method, falling back to the underlying FSTRemoteDocumentCache if + * no entry is buffered. In the absence of LevelDB transactions (that would allow reading back + * uncommitted writes), this greatly simplifies the implementation of complex operations that + * may want to freely read/write entries to the FSTRemoteDocumentCache while still ensuring that + * the final writing of the buffered entries is atomic. + * + * For doing blind writes that don't depend on the current state of the FSTRemoteDocumentCache + * or for plain reads, you can/should still just use the FSTRemoteDocumentCache directly. + */ +@interface FSTRemoteDocumentChangeBuffer : NSObject + ++ (instancetype)changeBufferWithCache:(id)cache; + +- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); + +/** Buffers an `FSTRemoteDocumentCache addEntry:group:` call. */ +- (void)addEntry:(FSTMaybeDocument *)maybeDocument; + +// NOTE: removeEntryForKey: is not presently necessary and so is omitted. + +/** + * Looks up an entry in the cache. The buffered changes will first be checked, and if no + * buffered change applies, this will forward to `FSTRemoteDocumentCache entryForKey:`. + * + * @param documentKey The key of the entry to look up. + * @return The cached FSTDocument or FSTDeletedDocument entry, or nil if we have nothing cached. + */ +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey; + +/** + * Applies buffered changes to the underlying FSTRemoteDocumentCache, using the provided + * FSTWriteGroup. + */ +- (void)applyToWriteGroup:(FSTWriteGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m new file mode 100644 index 0000000..12a68ff --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m @@ -0,0 +1,88 @@ +/* + * 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 "FSTRemoteDocumentChangeBuffer.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTRemoteDocumentCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteDocumentChangeBuffer () + +- (instancetype)initWithCache:(id)cache; + +/** The underlying cache we're buffering changes for. */ +@property(nonatomic, strong, nonnull) id remoteDocumentCache; + +/** The buffered changes, stored as a dictionary for easy lookups. */ +@property(nonatomic, strong, nullable) + NSMutableDictionary *changes; + +@end + +@implementation FSTRemoteDocumentChangeBuffer + ++ (instancetype)changeBufferWithCache:(id)cache { + return [[FSTRemoteDocumentChangeBuffer alloc] initWithCache:cache]; +} + +- (instancetype)initWithCache:(id)cache { + if (self = [super init]) { + _remoteDocumentCache = cache; + _changes = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)addEntry:(FSTMaybeDocument *)maybeDocument { + [self assertValid]; + + self.changes[maybeDocument.key] = maybeDocument; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey { + [self assertValid]; + + FSTMaybeDocument *bufferedEntry = self.changes[documentKey]; + if (bufferedEntry) { + return bufferedEntry; + } else { + return [self.remoteDocumentCache entryForKey:documentKey]; + } +} + +- (void)applyToWriteGroup:(FSTWriteGroup *)group { + [self assertValid]; + + [self.changes enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *value, + BOOL *stop) { + [self.remoteDocumentCache addEntry:value group:group]; + }]; + + // We should not be used to buffer any more changes. + self.changes = nil; +} + +- (void)assertValid { + FSTAssert(self.changes, @"Changes have already been applied."); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroup.h b/Firestore/Source/Local/FSTWriteGroup.h new file mode 100644 index 0000000..21482af --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroup.h @@ -0,0 +1,97 @@ +/* + * 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 + +#ifdef __cplusplus +#include + +#include "StringView.h" + +namespace leveldb { +class DB; +class Status; +} + +#endif + +NS_ASSUME_NONNULL_BEGIN + +@class GPBMessage; + +/** + * A group of writes that will be applied together atomically to persistent storage. + * + * This class is usable by both Objective-C and Objective-C++ clients. Objective-C clients are able + * to create a new group and commit it. Objective-C++ clients can additionally add to the group + * using deleteKey: and putKey:value:. + * + * Note that this is a write "group" even though the underlying LevelDB concept is a write "batch" + * because Firestore already has a concept of mutation batches, which are user-specified groups of + * changes. This means that an FSTWriteGroup may contain the application of multiple user-specified + * mutation batches. + */ +@interface FSTWriteGroup : NSObject + +/** + * Creates a new, empty write group. + * + * @param action A description of the action performed by this group, used for logging. + */ ++ (instancetype)groupWithAction:(NSString *)action; + +- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); + +/** The action description assigned to this write group. */ +@property(nonatomic, copy, readonly) NSString *action; + +/** Returns YES if the write group has no messages in it. */ +- (BOOL)isEmpty; + +#ifdef __cplusplus + +/** + * Marks the given key for deletion. + * + * @param key The LevelDB key of the row to delete + */ +- (void)removeMessageForKey:(Firestore::StringView)key; + +/** + * Sets the row identified by the given key to the value of the given protocol buffer message. + * + * @param key The LevelDB Key of the row to set. + * @param message The protocol buffer message whose serialized contents should be used for the + * value associated with the key. + */ +- (void)setMessage:(GPBMessage *)message forKey:(Firestore::StringView)key; + +/** + * Sets the row identified by the given key to the value of the given data bytes. + * + * @param key The LevelDB Key of the row to set. + * @param data The exact value to be associated with the key. + */ +- (void)setData:(Firestore::StringView)data forKey:(Firestore::StringView)key; + +/** Writes the contents to the given LevelDB. */ +- (leveldb::Status)writeToDB:(std::shared_ptr)db; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroup.mm b/Firestore/Source/Local/FSTWriteGroup.mm new file mode 100644 index 0000000..e6da131 --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroup.mm @@ -0,0 +1,145 @@ +/* + * 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 "FSTWriteGroup.h" + +#import +#include +#include + +#import "FSTLevelDBKey.h" +#import "FSTAssert.h" + +#include "ordered_code.h" + +using Firestore::OrderedCode; +using Firestore::StringView; +using leveldb::DB; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteBatch; +using leveldb::WriteOptions; + +NS_ASSUME_NONNULL_BEGIN + +namespace Firestore { + +/** + * A WriteBatch::Handler implementation that extracts batch details from a leveldb::WriteBatch. + * This is used for describing a write batch primarily in log messages after a failure. + */ +class BatchDescription : public WriteBatch::Handler { + public: + BatchDescription() : ops_(0), size_(0), message_([NSMutableString string]) {} + virtual ~BatchDescription(); + virtual void Put(const Slice &key, const Slice &value); + virtual void Delete(const Slice &key); + + // Converts the batch to a printable string description of it + NSString *ToString() const { + return [NSString + stringWithFormat:@"%d changes (%lu bytes):%@", ops_, (unsigned long)size_, message_]; + } + + // Disallow copies and moves + BatchDescription(const BatchDescription &) = delete; + BatchDescription &operator=(const BatchDescription &) = delete; + BatchDescription(BatchDescription &&) = delete; + BatchDescription &operator=(BatchDescription &&) = delete; + + private: + int ops_; + size_t size_; + NSMutableString *message_; +}; + +BatchDescription::~BatchDescription() {} + +void BatchDescription::Put(const Slice &key, const Slice &value) { + ops_ += 1; + size_ += value.size(); + + [message_ appendFormat:@"\n - Put %@ (%lu bytes)", [FSTLevelDBKey descriptionForKey:key], + (unsigned long)value.size()]; +} + +void BatchDescription::Delete(const Slice &key) { + ops_ += 1; + + [message_ appendFormat:@"\n - Delete %@", [FSTLevelDBKey descriptionForKey:key]]; +} + +} // namespace Firestore + +@interface FSTWriteGroup () +- (instancetype)initWithAction:(NSString *)action NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTWriteGroup { + int _changes; + WriteBatch _contents; +} + ++ (instancetype)groupWithAction:(NSString *)action { + return [[FSTWriteGroup alloc] initWithAction:action]; +} + +- (instancetype)initWithAction:(NSString *)action { + if (self = [super init]) { + _action = action; + } + return self; +} + +- (NSString *)description { + Firestore::BatchDescription description; + Status status = _contents.Iterate(&description); + if (!status.ok()) { + FSTFail(@"Iterate over write batch should not fail"); + } + return [NSString + stringWithFormat:@"", self.action, description.ToString()]; +} + +- (void)removeMessageForKey:(StringView)key { + _contents.Delete(key); + _changes += 1; +} + +- (void)setMessage:(GPBMessage *)message forKey:(StringView)key { + NSData *data = [message data]; + Slice value((const char *)data.bytes, data.length); + + _contents.Put(key, value); + _changes += 1; +} + +- (void)setData:(StringView)data forKey:(StringView)key { + _contents.Put(key, data); + _changes += 1; +} + +- (leveldb::Status)writeToDB:(std::shared_ptr)db { + return db->Write(leveldb::WriteOptions(), &_contents); +} + +- (BOOL)isEmpty { + return _changes == 0; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.h b/Firestore/Source/Local/FSTWriteGroupTracker.h new file mode 100644 index 0000000..bd26a46 --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroupTracker.h @@ -0,0 +1,45 @@ +/* + * 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 + +@class FSTWriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Helper class for FSTPersistence implementations to create WriteGroups and verify internal + * contracts are maintained: + * 1. Can't create a group when an uncommitted group exists (no nesting). + * 2. Can't commit a group that differs from the last created one. + */ +@interface FSTWriteGroupTracker : NSObject + +/** Creates and returns an FSTWriteGroupTracker instance. */ ++ (instancetype)tracker; + +/** + * Verifies there's no active group already and then creates a new group and stores it for later + * validation with `endGroup`. + */ +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action; + +/** Ends a group previously started with `startGroupWithAction`. */ +- (void)endGroup:(FSTWriteGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.m b/Firestore/Source/Local/FSTWriteGroupTracker.m new file mode 100644 index 0000000..1c6c84d --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroupTracker.m @@ -0,0 +1,52 @@ +/* + * 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 "FSTWriteGroupTracker.h" + +#import "FSTAssert.h" +#import "FSTWriteGroup.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWriteGroupTracker () +@property(nonatomic, strong, nullable) FSTWriteGroup *activeGroup; +@end + +@implementation FSTWriteGroupTracker + ++ (instancetype)tracker { + return [[FSTWriteGroupTracker alloc] init]; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + // NOTE: We can relax this to allow nesting if/when we find we need it. + FSTAssert(!self.activeGroup, + @"Attempt to create write group (%@) while existing write group (%@) still active.", + action, self.activeGroup.action); + self.activeGroup = [FSTWriteGroup groupWithAction:action]; + return self.activeGroup; +} + +- (void)endGroup:(FSTWriteGroup *)group { + FSTAssert(self.activeGroup == group, + @"Attempted to end write group (%@) which is different from active group (%@)", + group.action, self.activeGroup.action); + self.activeGroup = nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/StringView.h b/Firestore/Source/Local/StringView.h new file mode 100644 index 0000000..799baf8 --- /dev/null +++ b/Firestore/Source/Local/StringView.h @@ -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. + */ + +#ifndef IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ +#define IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ + +#ifndef __cplusplus +#error "StringView is Objective-C++ and can only be included from .mm files" +#endif + +#import + +#include +#include + +namespace Firestore { + +// A simple wrapper for the character data of any string-like type to which +// we'd like to temporarily refer as an argument. +// +// This is superficially similar to StringPiece and leveldb::Slice except +// that it also supports implicit conversion from NSString *, which is useful +// when writing Objective-C++ methods that accept any string-like type. +// +// Note that much like any other view-type class in C++, the caller is +// responsible for ensuring that the lifetime of the string-like data is longer +// than the lifetime of the StringView. +// +// Functions that take a StringView argument promise that they won't keep the +// pointer beyond the immediate scope of their own stack frame. +class StringView { + public: + // Creates a StringView from an NSString. When StringView is an argument type + // into which an NSString* is passed, the caller should ensure that the + // NSString is retained. + StringView(NSString *str) : data_([str UTF8String]), size_(str.length) { + } + + // Creates a StringView from the given char* pointer with an explicit size. + // The character data can contain NUL bytes as a result. + StringView(const char *data, size_t size) : data_(data), size_(size) { + } + + // Creates a StringView from the given char* pointer but computes the size + // with strlen. This is really only suitable for passing C string literals. + StringView(const char *data) : data_(data), size_(strlen(data)) { + } + + // Creates a StringView from the given slice. + StringView(leveldb::Slice slice) : data_(slice.data()), size_(slice.size()) { + } + + // Creates a StringView from the given std::string. The string must be an + // lvalue for the lifetime requirements to be satisfied. + StringView(const std::string &str) : data_(str.data()), size_(str.size()) { + } + + // Converts this StringView to a Slice, which is an equivalent (and more + // functional) type. The returned slice has the same lifetime as this + // StringView. + operator leveldb::Slice() { + return leveldb::Slice(data_, size_); + } + + private: + const char *data_; + const size_t size_; +}; + +} // namespace Firestore + +#endif // IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ diff --git a/Firestore/Source/Model/FSTDatabaseID.h b/Firestore/Source/Model/FSTDatabaseID.h new file mode 100644 index 0000000..442e764 --- /dev/null +++ b/Firestore/Source/Model/FSTDatabaseID.h @@ -0,0 +1,48 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** FSTDatabaseID represents a particular database in the datastore. */ +@interface FSTDatabaseID : NSObject + +/** + * Creates and returns a new FSTDatabaseID. + * @param projectID The project for the database. + * @param databaseID The database in the project to use. + * @return A new instance of FSTDatabaseID. + */ ++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID; + +/** The project. */ +@property(nonatomic, copy, readonly) NSString *projectID; + +/** The database. */ +@property(nonatomic, copy, readonly) NSString *databaseID; + +/** Whether this is the default database of the project. */ +- (BOOL)isDefaultDatabase; + +- (NSComparisonResult)compare:(FSTDatabaseID *)other; +- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID; + +@end + +extern NSString *const kDefaultDatabaseID; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDatabaseID.m b/Firestore/Source/Model/FSTDatabaseID.m new file mode 100644 index 0000000..bf4b417 --- /dev/null +++ b/Firestore/Source/Model/FSTDatabaseID.m @@ -0,0 +1,90 @@ +/* + * 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 "FSTDatabaseID.h" + +#import "FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The default name for "unset" database ID in resource names. */ +NSString *const kDefaultDatabaseID = @"(default)"; + +#pragma mark - FSTDatabaseID + +@implementation FSTDatabaseID + ++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID { + return [[FSTDatabaseID alloc] initWithProject:projectID database:databaseID]; +} + +/** + * Designated initializer. + * + * @param projectID The project for the database. + * @param databaseID The database in the datastore. + */ +- (instancetype)initWithProject:(NSString *)projectID database:(NSString *)databaseID { + if (self = [super init]) { + FSTAssert(databaseID, @"databaseID cannot be nil"); + _projectID = [projectID copy]; + _databaseID = [databaseID copy]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToDatabaseId:other]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.projectID hash]; + hash = hash * 31u + [self.databaseID hash]; + return hash; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.projectID, self.databaseID]; +} + +- (NSComparisonResult)compare:(FSTDatabaseID *)other { + NSComparisonResult cmp = [self.projectID compare:other.projectID]; + return cmp == NSOrderedSame ? [self.databaseID compare:other.databaseID] : cmp; +} + +- (BOOL)isDefaultDatabase { + return [self.databaseID isEqualToString:kDefaultDatabaseID]; +} + +- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID { + if (self == databaseID) return YES; + if (databaseID == nil) return NO; + if (self.projectID != databaseID.projectID && + ![self.projectID isEqualToString:databaseID.projectID]) + return NO; + if (self.databaseID != databaseID.databaseID && + ![self.databaseID isEqualToString:databaseID.databaseID]) + return NO; + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocument.h b/Firestore/Source/Model/FSTDocument.h new file mode 100644 index 0000000..100f553 --- /dev/null +++ b/Firestore/Source/Model/FSTDocument.h @@ -0,0 +1,58 @@ +/* + * 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 + +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTObjectValue; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * The result of a lookup for a given path may be an existing document or a tombstone that marks + * the path deleted. + */ +@interface FSTMaybeDocument : NSObject +- (id)init __attribute__((unavailable("Abstract base class"))); + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@property(nonatomic, readonly) FSTSnapshotVersion *version; +@end + +@interface FSTDocument : FSTMaybeDocument ++ (instancetype)documentWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations; + +- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, readonly, getter=hasLocalMutations) BOOL localMutations; + +@end + +@interface FSTDeletedDocument : FSTMaybeDocument ++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version; +@end + +/** An NSComparator suitable for comparing docs using only their keys. */ +extern const NSComparator FSTDocumentComparatorByKey; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocument.m b/Firestore/Source/Model/FSTDocument.m new file mode 100644 index 0000000..5146d46 --- /dev/null +++ b/Firestore/Source/Model/FSTDocument.m @@ -0,0 +1,139 @@ +/* + * 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 "FSTDocument.h" + +#import "FSTAssert.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMaybeDocument () + +- (instancetype)initWithKey:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTMaybeDocument + +- (instancetype)initWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { + FSTAssert(!!version, @"Version must not be nil."); + self = [super init]; + if (self) { + _key = key; + _version = version; + } + return self; +} + +- (id)copyWithZone:(NSZone *_Nullable)zone { + // All document types are immutable + return self; +} + +@end + +@implementation FSTDocument + ++ (instancetype)documentWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations { + return + [[FSTDocument alloc] initWithData:data key:key version:version hasLocalMutations:mutations]; +} + +- (instancetype)initWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations { + self = [super initWithKey:key version:version]; + if (self) { + _data = data; + _localMutations = mutations; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDocument class]]) { + return NO; + } + + FSTDocument *otherDoc = other; + return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version] && + [self.data isEqual:otherDoc.data] && self.hasLocalMutations == otherDoc.hasLocalMutations; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31 + [self.version hash]; + result = result * 31 + [self.data hash]; + result = result * 31 + (self.hasLocalMutations ? 1 : 0); + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.key.path, self.version, + self.localMutations ? @"YES" : @"NO", self.data]; +} + +- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path { + return [_data valueForPath:path]; +} + +@end + +@implementation FSTDeletedDocument + ++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { + return [[FSTDeletedDocument alloc] initWithKey:key version:version]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeletedDocument class]]) { + return NO; + } + + FSTDocument *otherDoc = other; + return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31 + [self.version hash]; + return result; +} + +@end + +const NSComparator FSTDocumentComparatorByKey = + ^NSComparisonResult(FSTMaybeDocument *doc1, FSTMaybeDocument *doc2) { + return [doc1.key compare:doc2.key]; + }; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentDictionary.h b/Firestore/Source/Model/FSTDocumentDictionary.h new file mode 100644 index 0000000..8ae8e01 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentDictionary.h @@ -0,0 +1,44 @@ +/* + * 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 + +#import "FSTImmutableSortedDictionary.h" + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTMaybeDocument; + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience type for a map of keys to MaybeDocuments, since they are so common. */ +typedef FSTImmutableSortedDictionary + FSTMaybeDocumentDictionary; + +/** Convenience type for a map of keys to Documents, since they are so common. */ +typedef FSTImmutableSortedDictionary FSTDocumentDictionary; + +@interface FSTImmutableSortedDictionary (FSTDocumentDictionary) + +/** Returns a new set using the DocumentKeyComparator. */ ++ (FSTMaybeDocumentDictionary *)maybeDocumentDictionary; + +/** Returns a new set using the DocumentKeyComparator. */ ++ (FSTDocumentDictionary *)documentDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentDictionary.m b/Firestore/Source/Model/FSTDocumentDictionary.m new file mode 100644 index 0000000..67e3ae7 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentDictionary.m @@ -0,0 +1,42 @@ +/* + * 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 "FSTDocumentDictionary.h" + +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (FSTMaybeDocumentDictionary) + ++ (instancetype)maybeDocumentDictionary { + // Immutable dictionaries are contravariant in their value type, so just return a + // FSTDocumentDictionary here. + return [FSTDocumentDictionary documentDictionary]; +} + ++ (instancetype)documentDictionary { + static FSTDocumentDictionary *singleton; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + singleton = [FSTDocumentDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + }); + return singleton; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKey.h b/Firestore/Source/Model/FSTDocumentKey.h new file mode 100644 index 0000000..2af1c9a --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKey.h @@ -0,0 +1,66 @@ +/* + * 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 + +@class FSTResourcePath; + +NS_ASSUME_NONNULL_BEGIN + +/** FSTDocumentKey represents the location of a document in the Firestore database. */ +@interface FSTDocumentKey : NSObject + +/** + * Creates and returns a new document key with the given path. + * + * @param path The path to the document. + * @return A new instance of FSTDocumentKey. + */ ++ (instancetype)keyWithPath:(FSTResourcePath *)path; + +/** + * Creates and returns a new document key with a path with the given segments. + * + * @param segments The segments of the path to the document. + * @return A new instance of FSTDocumentKey. + */ ++ (instancetype)keyWithSegments:(NSArray *)segments; + +/** + * Creates and returns a new document key from the given resource path string. + * + * @param resourcePath The slash-separated segments of the resource's path. + * @return A new instance of FSTDocumentKey. + */ ++ (instancetype)keyWithPathString:(NSString *)resourcePath; + +/** Returns true iff the given path is a path to a document. */ ++ (BOOL)isDocumentKey:(FSTResourcePath *)path; + +- (BOOL)isEqualToKey:(FSTDocumentKey *)other; +- (NSComparisonResult)compare:(FSTDocumentKey *)other; + +/** The path to the document. */ +@property(strong, nonatomic, readonly) FSTResourcePath *path; + +@end + +extern const NSComparator FSTDocumentKeyComparator; + +/** The field path string that represents the document's key. */ +extern NSString *const kDocumentKeyPath; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKey.m b/Firestore/Source/Model/FSTDocumentKey.m new file mode 100644 index 0000000..a412b13 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKey.m @@ -0,0 +1,105 @@ +/* + * 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 "FSTDocumentKey.h" + +#import "FSTAssert.h" +#import "FSTFirestoreClient.h" +#import "FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentKey () +/** The path to the document. */ +@property(strong, nonatomic, readwrite) FSTResourcePath *path; +@end + +@implementation FSTDocumentKey + ++ (instancetype)keyWithPath:(FSTResourcePath *)path { + return [[FSTDocumentKey alloc] initWithPath:path]; +} + ++ (instancetype)keyWithSegments:(NSArray *)segments { + return [FSTDocumentKey keyWithPath:[FSTResourcePath pathWithSegments:segments]]; +} + ++ (instancetype)keyWithPathString:(NSString *)resourcePath { + NSArray *segments = [resourcePath componentsSeparatedByString:@"/"]; + return [FSTDocumentKey keyWithSegments:segments]; +} + +/** Designated initializer. */ +- (instancetype)initWithPath:(FSTResourcePath *)path { + FSTAssert([FSTDocumentKey isDocumentKey:path], @"invalid document key path: %@", path); + + if (self = [super init]) { + _path = path; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTDocumentKey class]]) { + return NO; + } + return [self isEqualToKey:(FSTDocumentKey *)object]; +} + +- (NSUInteger)hash { + return self.path.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.path]; +} + +/** Implements NSCopying without actually copying because FSTDocumentKeys are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +- (BOOL)isEqualToKey:(FSTDocumentKey *)other { + return FSTDocumentKeyComparator(self, other) == NSOrderedSame; +} + +- (NSComparisonResult)compare:(FSTDocumentKey *)other { + return FSTDocumentKeyComparator(self, other); +} + ++ (NSComparator)comparator { + return ^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare:obj2]; + }; +} + ++ (BOOL)isDocumentKey:(FSTResourcePath *)path { + return path.length % 2 == 0; +} + +@end + +const NSComparator FSTDocumentKeyComparator = + ^NSComparisonResult(FSTDocumentKey *key1, FSTDocumentKey *key2) { + return [key1.path compare:key2.path]; + }; + +NSString *const kDocumentKeyPath = @"__name__"; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKeySet.h b/Firestore/Source/Model/FSTDocumentKeySet.h new file mode 100644 index 0000000..7352985 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKeySet.h @@ -0,0 +1,35 @@ +/* + * 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 + +#import "FSTImmutableSortedSet.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience type for a set of keys, since they are so common. */ +typedef FSTImmutableSortedSet FSTDocumentKeySet; + +@interface FSTImmutableSortedSet (FSTDocumentKey) + +/** Returns a new set using the DocumentKeyComparator. */ ++ (FSTDocumentKeySet *)keySet; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKeySet.m b/Firestore/Source/Model/FSTDocumentKeySet.m new file mode 100644 index 0000000..54f1b2c --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKeySet.m @@ -0,0 +1,31 @@ +/* + * 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 "FSTDocumentKeySet.h" + +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedSet (FSTDocumentKey) + ++ (instancetype)keySet { + return [FSTDocumentKeySet setWithComparator:FSTDocumentKeyComparator]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.h b/Firestore/Source/Model/FSTDocumentSet.h new file mode 100644 index 0000000..7457ea3 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentSet.h @@ -0,0 +1,95 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" + +@class FSTDocument; +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * DocumentSet is an immutable (copy-on-write) collection that holds documents in order specified + * by the provided comparator. We always add a document key comparator on top of what is provided + * to guarantee document equality based on the key. + */ +@interface FSTDocumentSet : NSObject + +/** Creates a new, empty FSTDocumentSet sorted by the given comparator, then by keys. */ ++ (instancetype)documentSetWithComparator:(NSComparator)comparator; + +- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); + +- (NSUInteger)count; + +/** Returns true if the dictionary contains no elements. */ +- (BOOL)isEmpty; + +/** Returns YES if this set contains a document with the given key. */ +- (BOOL)containsKey:(FSTDocumentKey *)key; + +/** Returns the document from this set with the given key if it exists or nil if it doesn't. */ +- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key; + +/** + * Returns the first document in the set according to its built in ordering, or nil if the set + * is empty. + */ +- (FSTDocument *_Nullable)firstDocument; + +/** + * Returns the last document in the set according to its built in ordering, or nil if the set + * is empty. + */ +- (FSTDocument *_Nullable)lastDocument; + +/** + * Returns the document previous to the document associated with the given key in the set according + * to its built in ordering. Returns nil if the document associated with the given key is the + * first document. + * + * @param key A key that must be present in the DocumentSet. + * @throws NSInvalidArgumentException if key is not present. + */ +- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key; + +/** + * Returns the index of the document with the provided key in the document set. Returns NSNotFound + * if the key is not present. + */ +- (NSUInteger)indexOfKey:(FSTDocumentKey *)key; + +- (NSEnumerator *)documentEnumerator; + +/** Returns a copy of the documents in this set as an array. This is O(n) on the size of the set. */ +- (NSArray *)arrayValue; + +/** + * Returns the documents as a FSTMaybeDocumentDictionary. This is O(1) as this leverages our + * internal representation. + */ +- (FSTMaybeDocumentDictionary *)dictionaryValue; + +/** Returns a new FSTDocumentSet that contains the given document. */ +- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document; + +/** Returns a new FSTDocumentSet that excludes any document associated with the given key. */ +- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.m b/Firestore/Source/Model/FSTDocumentSet.m new file mode 100644 index 0000000..94b7b58 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentSet.m @@ -0,0 +1,197 @@ +/* + * 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 "FSTDocumentSet.h" + +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTImmutableSortedSet.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The type of the index of the documents in an FSTDocumentSet. + * @see FSTDocumentSet#index + */ +typedef FSTImmutableSortedDictionary IndexType; + +/** + * The type of the main collection of documents in an FSTDocumentSet. + * @see FSTDocumentSet#sortedSet + */ +typedef FSTImmutableSortedSet SetType; + +@interface FSTDocumentSet () + +- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet NS_DESIGNATED_INITIALIZER; + +/** + * An index of the documents in the FSTDocumentSet, indexed by document key. The index + * exists to guarantee the uniqueness of document keys in the set and to allow lookup and removal + * of documents by key. + */ +@property(nonatomic, strong, readonly) IndexType *index; + +/** + * The main collection of documents in the FSTDocumentSet. The documents are ordered by a + * comparator supplied from a query. The SetType collection exists in addition to the index to + * allow ordered traversal of the FSTDocumentSet. + */ +@property(nonatomic, strong, readonly) SetType *sortedSet; +@end + +@implementation FSTDocumentSet + ++ (instancetype)documentSetWithComparator:(NSComparator)comparator { + IndexType *index = + [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + SetType *set = [FSTImmutableSortedSet setWithComparator:comparator]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet { + self = [super init]; + if (self) { + _index = index; + _sortedSet = sortedSet; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTDocumentSet class]]) { + return NO; + } + + FSTDocumentSet *otherSet = (FSTDocumentSet *)other; + if ([self count] != [otherSet count]) { + return NO; + } + + NSEnumerator *selfIter = [self.sortedSet objectEnumerator]; + NSEnumerator *otherIter = [otherSet.sortedSet objectEnumerator]; + + FSTDocument *selfDoc = [selfIter nextObject]; + FSTDocument *otherDoc = [otherIter nextObject]; + while (selfDoc) { + if (![selfDoc isEqual:otherDoc]) { + return NO; + } + selfDoc = [selfIter nextObject]; + otherDoc = [otherIter nextObject]; + } + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = 0; + for (FSTDocument *doc in self.sortedSet.objectEnumerator) { + hash = 31 * hash + [doc hash]; + } + return hash; +} + +- (NSString *)description { + return [self.sortedSet description]; +} + +- (NSUInteger)count { + return [self.index count]; +} + +- (BOOL)isEmpty { + return [self.index isEmpty]; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + return [self.index objectForKey:key] != nil; +} + +- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key { + return [self.index objectForKey:key]; +} + +- (FSTDocument *_Nullable)firstDocument { + return [self.sortedSet firstObject]; +} + +- (FSTDocument *_Nullable)lastDocument { + return [self.sortedSet lastObject]; +} + +- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + if (!doc) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat:@"Key %@ does not exist", key] + userInfo:nil]; + } + return [self.sortedSet predecessorObject:doc]; +} + +- (NSUInteger)indexOfKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + return doc ? [self.sortedSet indexOfObject:doc] : NSNotFound; +} + +- (NSEnumerator *)documentEnumerator { + return [self.sortedSet objectEnumerator]; +} + +- (NSArray *)arrayValue { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + for (FSTDocument *doc in self.documentEnumerator) { + [result addObject:doc]; + } + return result; +} + +- (FSTMaybeDocumentDictionary *)dictionaryValue { + return self.index; +} + +- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document { + // TODO(mcg): look into making document nonnull. + if (!document) { + return self; + } + + // Remove any prior mapping of the document's key before adding, preventing sortedSet from + // accumulating values that aren't in the index. + FSTDocumentSet *removed = [self documentSetByRemovingKey:document.key]; + + IndexType *index = [removed.index dictionaryBySettingObject:document forKey:document.key]; + SetType *set = [removed.sortedSet setByAddingObject:document]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + if (!doc) { + return self; + } + + IndexType *index = [self.index dictionaryByRemovingObjectForKey:key]; + SetType *set = [self.sortedSet setByRemovingObject:doc]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.h b/Firestore/Source/Model/FSTDocumentVersionDictionary.h new file mode 100644 index 0000000..f94545f --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.h @@ -0,0 +1,40 @@ +/* + * 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 + +#import "FSTImmutableSortedDictionary.h" + +@class FSTDocumentKey; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** A map of key to version number. */ +typedef FSTImmutableSortedDictionary + FSTDocumentVersionDictionary; + +/** + * Extension to FSTImmutableSortedDictionary that allows natural construction of + * FSTDocumentVersionDictionary. + */ +@interface FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) + ++ (instancetype)documentVersionDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.m b/Firestore/Source/Model/FSTDocumentVersionDictionary.m new file mode 100644 index 0000000..0eaf9f8 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.m @@ -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 "FSTDocumentVersionDictionary.h" + +#import "FSTDocumentKey.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) + ++ (instancetype)documentVersionDictionary { + static FSTDocumentVersionDictionary *singleton; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + singleton = [FSTDocumentVersionDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + }); + return singleton; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h new file mode 100644 index 0000000..cbf7e3e --- /dev/null +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -0,0 +1,242 @@ +/* + * 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 + +#import "FSTImmutableSortedDictionary.h" + +@class FSTDatabaseID; +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTTimestamp; +@class FIRGeoPoint; + +NS_ASSUME_NONNULL_BEGIN + +/** The order of types in Firestore; this order is defined by the backend. */ +typedef NS_ENUM(NSInteger, FSTTypeOrder) { + FSTTypeOrderNull, + FSTTypeOrderBoolean, + FSTTypeOrderNumber, + FSTTypeOrderTimestamp, + FSTTypeOrderString, + FSTTypeOrderBlob, + FSTTypeOrderReference, + FSTTypeOrderGeoPoint, + FSTTypeOrderArray, + FSTTypeOrderObject, +}; + +/** + * Abstract base class representing an immutable data value as stored in Firestore. FSTFieldValue + * represents all the different kinds of values that can be stored in fields in a document. + * + * Supported types are: + * - Null + * - Boolean + * - Long + * - Double + * - Timestamp + * - ServerTimestamp (a sentinel used in uncommitted writes) + * - String + * - Binary + * - (Document) References + * - GeoPoint + * - Array + * - Object + */ +@interface FSTFieldValue : NSObject + +/** Returns the FSTTypeOrder for this value. */ +- (FSTTypeOrder)typeOrder; + +/** + * Converts an FSTFieldValue into the value that users will see in document snapshots. + * + * TODO(mikelehen): This conversion should probably happen at the API level and right now `value` is + * used inappropriately in the serializer implementation, etc. We need to do some reworking. + */ +- (id)value; + +/** Compares against another FSTFieldValue. */ +- (NSComparisonResult)compare:(FSTFieldValue *)other; + +@end + +/** + * A null value stored in Firestore. The |value| of a FSTNullValue is [NSNull null]. + */ +@interface FSTNullValue : FSTFieldValue ++ (instancetype)nullValue; +- (id)value; +@end + +/** + * A boolean value stored in Firestore. + */ +@interface FSTBooleanValue : FSTFieldValue ++ (instancetype)trueValue; ++ (instancetype)falseValue; ++ (instancetype)booleanValue:(BOOL)value; +- (NSNumber *)value; +@end + +/** + * Base class inherited from by FSTIntegerValue and FSTDoubleValue. It implements proper number + * comparisons between the two types. + */ +@interface FSTNumberValue : FSTFieldValue +@end + +/** + * An integer value stored in Firestore. + */ +@interface FSTIntegerValue : FSTNumberValue ++ (instancetype)integerValue:(int64_t)value; +- (NSNumber *)value; +- (int64_t)internalValue; +@end + +/** + * A double-precision floating point number stored in Firestore. + */ +@interface FSTDoubleValue : FSTNumberValue ++ (instancetype)doubleValue:(double)value; ++ (instancetype)nanValue; +- (NSNumber *)value; +- (double)internalValue; +@end + +/** + * A string stored in Firestore. + */ +@interface FSTStringValue : FSTFieldValue ++ (instancetype)stringValue:(NSString *)value; +- (NSString *)value; +@end + +/** + * A timestamp value stored in Firestore. + */ +@interface FSTTimestampValue : FSTFieldValue ++ (instancetype)timestampValue:(FSTTimestamp *)value; +- (NSDate *)value; +- (FSTTimestamp *)internalValue; +@end + +/** + * Represents a locally-applied Server Timestamp. + * + * Notes: + * - FSTServerTimestampValue instances are created as the result of applying an FSTTransformMutation + * (see [FSTTransformMutation applyTo]). They can only exist in the local view of a document. + * Therefore they do not need to be parsed or serialized. + * - When evaluated locally (e.g. via FSTDocumentSnapshot data), they evaluate to NSNull (at least + * for now, see b/62064202). + * - They sort after all FSTTimestampValues. With respect to other FSTServerTimestampValues, they + * sort by their localWriteTime. + */ +@interface FSTServerTimestampValue : FSTFieldValue ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime; +- (NSNull *)value; +@property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@end + +/** + * A geo point value stored in Firestore. + */ +@interface FSTGeoPointValue : FSTFieldValue ++ (instancetype)geoPointValue:(FIRGeoPoint *)value; +- (FIRGeoPoint *)value; +@end + +/** + * A blob value stored in Firestore. + */ +@interface FSTBlobValue : FSTFieldValue ++ (instancetype)blobValue:(NSData *)value; +- (NSData *)value; +@end + +/** + * A reference value stored in Firestore. + */ +@interface FSTReferenceValue : FSTFieldValue ++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID; +- (FSTDocumentKey *)value; +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@end + +/** + * A structured object value stored in Firestore. + */ +@interface FSTObjectValue : FSTFieldValue +/** Returns an empty FSTObjectValue. */ ++ (instancetype)objectValue; + +/** + * Initializes this FSTObjectValue with the given dictionary. + */ +- (instancetype)initWithDictionary:(NSDictionary *)value; + +/** + * Initializes this FSTObjectValue with the given immutable dictionary. + */ +- (instancetype)initWithImmutableDictionary: + (FSTImmutableSortedDictionary *)value NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (NSDictionary *)value; +- (FSTImmutableSortedDictionary *)internalValue; + +/** Returns the value at the given path if it exists. Returns nil otherwise. */ +- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath; + +/** + * Returns a new object where the field at the named path has its value set to the given value. + * This object remains unmodified. + */ +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath; + +/** + * Returns a new object where the field at the named path has been removed. If any segment of the + * path does not exist within this object's structure, no change is performed. + */ +- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath; +@end + +/** + * An array value stored in Firestore. + */ +@interface FSTArrayValue : FSTFieldValue + +/** + * Initializes this instance with the given array of wrapped values. + * + * @param value An immutable array of FSTFieldValue objects. Caller is responsible for copying the + * value or releasing all references. + */ +- (instancetype)initWithValueNoCopy:(NSArray *)value NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (NSArray *)value; +- (NSArray *)internalValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m new file mode 100644 index 0000000..7f96a3c --- /dev/null +++ b/Firestore/Source/Model/FSTFieldValue.m @@ -0,0 +1,837 @@ +/* + * 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 "FSTFieldValue.h" + +#import "FIRGeoPoint+Internal.h" +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTComparison.h" +#import "FSTDatabaseID.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldValue + +@interface FSTFieldValue () +- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other; +@end + +@implementation FSTFieldValue + +- (FSTTypeOrder)typeOrder { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (id)value { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (BOOL)isEqual:(id)other { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSUInteger)hash { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSString *)description { + return [[self value] description]; +} + +- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other { + if (self.typeOrder > other.typeOrder) { + return NSOrderedDescending; + } else { + FSTAssert(self.typeOrder < other.typeOrder, + @"defaultCompare should not be used for values of same type."); + return NSOrderedAscending; + } +} + +@end + +#pragma mark - FSTNullValue + +@implementation FSTNullValue + ++ (instancetype)nullValue { + static FSTNullValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTNullValue alloc] init]; + }); + return sharedInstance; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderNull; +} + +- (id)value { + return [NSNull null]; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[self class]]; +} + +- (NSUInteger)hash { + return 47; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[self class]]) { + return NSOrderedSame; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTBooleanValue + +@interface FSTBooleanValue () +@property(nonatomic, assign, readonly) BOOL internalValue; +@end + +@implementation FSTBooleanValue + ++ (instancetype)trueValue { + static FSTBooleanValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTBooleanValue alloc] initWithValue:YES]; + }); + return sharedInstance; +} + ++ (instancetype)falseValue { + static FSTBooleanValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTBooleanValue alloc] initWithValue:NO]; + }); + return sharedInstance; +} + ++ (instancetype)booleanValue:(BOOL)value { + return value ? [FSTBooleanValue trueValue] : [FSTBooleanValue falseValue]; +} + +- (id)initWithValue:(BOOL)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderBoolean; +} + +- (id)value { + return self.internalValue ? @YES : @NO; +} + +- (BOOL)isEqual:(id)other { + // Since we create shared instances for true / false, we can use reference equality. + return self == other; +} + +- (NSUInteger)hash { + return self.internalValue ? 1231 : 1237; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTBooleanValue class]]) { + return FSTCompareBools(self.internalValue, ((FSTBooleanValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTNumberValue + +@implementation FSTNumberValue + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderNumber; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if (![other isKindOfClass:[FSTNumberValue class]]) { + return [self defaultCompare:other]; + } else { + if ([self isKindOfClass:[FSTDoubleValue class]]) { + double thisDouble = ((FSTDoubleValue *)self).internalValue; + if ([other isKindOfClass:[FSTDoubleValue class]]) { + return FSTCompareDoubles(thisDouble, ((FSTDoubleValue *)other).internalValue); + } else { + FSTAssert([other isKindOfClass:[FSTIntegerValue class]], @"Unknown number value: %@", + other); + return FSTCompareMixed(thisDouble, ((FSTIntegerValue *)other).internalValue); + } + } else { + int64_t thisInt = ((FSTIntegerValue *)self).internalValue; + if ([other isKindOfClass:[FSTIntegerValue class]]) { + return FSTCompareInt64s(thisInt, ((FSTIntegerValue *)other).internalValue); + } else { + FSTAssert([other isKindOfClass:[FSTDoubleValue class]], @"Unknown number value: %@", other); + return -1 * FSTCompareMixed(((FSTDoubleValue *)other).internalValue, thisInt); + } + } + } +} + +@end + +#pragma mark - FSTIntegerValue + +@interface FSTIntegerValue () +@property(nonatomic, assign, readonly) int64_t internalValue; +@end + +@implementation FSTIntegerValue + ++ (instancetype)integerValue:(int64_t)value { + return [[FSTIntegerValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(int64_t)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (id)value { + return @(self.internalValue); +} + +- (BOOL)isEqual:(id)other { + // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them + // equal via isEqual: + return [other isKindOfClass:[FSTIntegerValue class]] && + self.internalValue == ((FSTIntegerValue *)other).internalValue; +} + +- (NSUInteger)hash { + return (((NSUInteger)self.internalValue) ^ (NSUInteger)(self.internalValue >> 32)); +} + +// NOTE: compare: is implemented in NumberValue. + +@end + +#pragma mark - FSTDoubleValue + +@interface FSTDoubleValue () +@property(nonatomic, assign, readonly) double internalValue; +@end + +@implementation FSTDoubleValue + ++ (instancetype)doubleValue:(double)value { + // Normalize NaNs to match the behavior on the backend (which uses Double.doubletoLongBits()). + if (isnan(value)) { + return [FSTDoubleValue nanValue]; + } + return [[FSTDoubleValue alloc] initWithValue:value]; +} + ++ (instancetype)nanValue { + static FSTDoubleValue *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDoubleValue alloc] initWithValue:NAN]; + }); + return sharedInstance; +} + +- (id)initWithValue:(double)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (id)value { + return @(self.internalValue); +} + +- (BOOL)isEqual:(id)other { + // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them + // equal via isEqual: + + // NOTE: isEqual: should compare NaN equal to itself and -0.0 not equal to 0.0. + + return [other isKindOfClass:[FSTDoubleValue class]] && + FSTDoubleBitwiseEquals(self.internalValue, ((FSTDoubleValue *)other).internalValue); +} + +- (NSUInteger)hash { + return FSTDoubleBitwiseHash(self.internalValue); +} + +// NOTE: compare: is implemented in NumberValue. + +@end + +#pragma mark - FSTStringValue + +@interface FSTStringValue () +@property(nonatomic, copy, readonly) NSString *internalValue; +@end + +// TODO(b/37267885): Add truncation support +@implementation FSTStringValue + ++ (instancetype)stringValue:(NSString *)value { + return [[FSTStringValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(NSString *)value { + self = [super init]; + if (self) { + _internalValue = [value copy]; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderString; +} + +- (id)value { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTStringValue class]] && + [self.internalValue isEqualToString:((FSTStringValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return self.internalValue ? 1 : 0; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTStringValue class]]) { + return FSTCompareStrings(self.internalValue, ((FSTStringValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTTimestampValue + +@interface FSTTimestampValue () +@property(nonatomic, strong, readonly) FSTTimestamp *internalValue; +@end + +@implementation FSTTimestampValue + ++ (instancetype)timestampValue:(FSTTimestamp *)value { + return [[FSTTimestampValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(FSTTimestamp *)value { + self = [super init]; + if (self) { + _internalValue = value; // FSTTimestamp is immutable. + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderTimestamp; +} + +- (id)value { + // For developers, we expose Timestamps as Dates. + return self.internalValue.approximateDateValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTTimestampValue class]] && + [self.internalValue isEqual:((FSTTimestampValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTTimestampValue class]]) { + return [self.internalValue compare:((FSTTimestampValue *)other).internalValue]; + } else if ([other isKindOfClass:[FSTServerTimestampValue class]]) { + // Concrete timestamps come before server timestamps. + return NSOrderedAscending; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTServerTimestampValue + +@implementation FSTServerTimestampValue + ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime { + return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime]; +} + +- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime { + self = [super init]; + if (self) { + _localWriteTime = localWriteTime; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderTimestamp; +} + +- (NSNull *)value { + // For developers, server timestamps always evaluate to NSNull (for now, at least; b/62064202). + return [NSNull null]; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTServerTimestampValue class]] && + [self.localWriteTime isEqual:((FSTServerTimestampValue *)other).localWriteTime]; +} + +- (NSUInteger)hash { + return [self.localWriteTime hash]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.localWriteTime]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTServerTimestampValue class]]) { + return [self.localWriteTime compare:((FSTServerTimestampValue *)other).localWriteTime]; + } else if ([other isKindOfClass:[FSTTimestampValue class]]) { + // Server timestamps come after all concrete timestamps. + return NSOrderedDescending; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTGeoPointValue + +@interface FSTGeoPointValue () +@property(nonatomic, strong, readonly) FIRGeoPoint *internalValue; +@end + +@implementation FSTGeoPointValue + ++ (instancetype)geoPointValue:(FIRGeoPoint *)value { + return [[FSTGeoPointValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(FIRGeoPoint *)value { + self = [super init]; + if (self) { + _internalValue = value; // FIRGeoPoint is immutable. + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderGeoPoint; +} + +- (id)value { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTGeoPointValue class]] && + [self.internalValue isEqual:((FSTGeoPointValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTGeoPointValue class]]) { + return [self.internalValue compare:((FSTGeoPointValue *)other).internalValue]; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTBlobValue + +@interface FSTBlobValue () +@property(nonatomic, copy, readonly) NSData *internalValue; +@end + +// TODO(b/37267885): Add truncation support +@implementation FSTBlobValue + ++ (instancetype)blobValue:(NSData *)value { + return [[FSTBlobValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(NSData *)value { + self = [super init]; + if (self) { + _internalValue = [value copy]; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderBlob; +} + +- (id)value { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTBlobValue class]] && + [self.internalValue isEqual:((FSTBlobValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTBlobValue class]]) { + return FSTCompareBytes(self.internalValue, ((FSTBlobValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTReferenceValue + +@interface FSTReferenceValue () +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +@implementation FSTReferenceValue + ++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { + return [[FSTReferenceValue alloc] initWithValue:value databaseID:databaseID]; +} + +- (id)initWithValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = value; + _databaseID = databaseID; + } + return self; +} + +- (id)value { + return self.key; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderReference; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTReferenceValue class]]) { + return NO; + } + + FSTReferenceValue *otherRef = (FSTReferenceValue *)other; + return [self.key isEqualToKey:otherRef.key] && + [self.databaseID isEqualToDatabaseId:otherRef.databaseID]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.databaseID hash]; + result = 31 * result + [self.key hash]; + return result; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)other; + NSComparisonResult cmp = [self.databaseID compare:ref.databaseID]; + return cmp != NSOrderedSame ? cmp : [self.key compare:ref.key]; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTObjectValue + +@interface FSTObjectValue () +@property(nonatomic, strong, readonly) + FSTImmutableSortedDictionary *internalValue; +@end + +@implementation FSTObjectValue + ++ (instancetype)objectValue { + static FSTObjectValue *sharedEmptyInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + FSTImmutableSortedDictionary *empty = + [FSTImmutableSortedDictionary dictionaryWithComparator:FSTStringComparator]; + sharedEmptyInstance = [[FSTObjectValue alloc] initWithImmutableDictionary:empty]; + }); + return sharedEmptyInstance; +} + +- (instancetype)initWithImmutableDictionary: + (FSTImmutableSortedDictionary *)value { + self = [super init]; + if (self) { + _internalValue = value; // FSTImmutableSortedDictionary is immutable. + } + return self; +} + +- (id)initWithDictionary:(NSDictionary *)value { + FSTImmutableSortedDictionary *dictionary = + [FSTImmutableSortedDictionary dictionaryWithDictionary:value comparator:FSTStringComparator]; + return [self initWithImmutableDictionary:dictionary]; +} + +- (id)value { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [self.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { + result[key] = [obj value]; + }]; + return result; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderObject; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTObjectValue class]]) { + return NO; + } + + FSTObjectValue *otherObj = other; + return [self.internalValue isEqual:otherObj.internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTObjectValue class]]) { + FSTImmutableSortedDictionary *selfDict = self.internalValue; + FSTImmutableSortedDictionary *otherDict = ((FSTObjectValue *)other).internalValue; + NSEnumerator *enumerator1 = [selfDict keyEnumerator]; + NSEnumerator *enumerator2 = [otherDict keyEnumerator]; + NSString *key1 = [enumerator1 nextObject]; + NSString *key2 = [enumerator2 nextObject]; + while (key1 && key2) { + NSComparisonResult keyCompare = [key1 compare:key2]; + if (keyCompare != NSOrderedSame) { + return keyCompare; + } + NSComparisonResult valueCompare = [selfDict[key1] compare:otherDict[key2]]; + if (valueCompare != NSOrderedSame) { + return valueCompare; + } + key1 = [enumerator1 nextObject]; + key2 = [enumerator2 nextObject]; + } + // Only equal if both enumerators are exhausted. + return FSTCompareBools(key1 != nil, key2 != nil); + } else { + return [self defaultCompare:other]; + } +} + +- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath { + FSTFieldValue *value = self; + for (int i = 0, max = fieldPath.length; value && i < max; i++) { + if (![value isMemberOfClass:[FSTObjectValue class]]) { + return nil; + } + + NSString *fieldName = fieldPath[i]; + value = ((FSTObjectValue *)value).internalValue[fieldName]; + } + + return value; +} + +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath { + FSTAssert([fieldPath length] > 0, @"Cannot set value with an empty path"); + + NSString *childName = [fieldPath firstSegment]; + if ([fieldPath length] == 1) { + // Recursive base case: + return [self objectBySettingValue:value forField:childName]; + } else { + // Nested path. Recursively generate a new sub-object and then wrap a new FSTObjectValue around + // the result. + FSTFieldValue *child = [_internalValue objectForKey:childName]; + FSTObjectValue *childObject; + if ([child isKindOfClass:[FSTObjectValue class]]) { + childObject = (FSTObjectValue *)child; + } else { + // If the child is not found or is a primitive type, pretend as if an empty object lived + // there. + childObject = [FSTObjectValue objectValue]; + } + FSTFieldValue *newChild = + [childObject objectBySettingValue:value forPath:[fieldPath pathByRemovingFirstSegment]]; + return [self objectBySettingValue:newChild forField:childName]; + } +} + +- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath { + FSTAssert([fieldPath length] > 0, @"Cannot delete an empty path"); + NSString *childName = [fieldPath firstSegment]; + if ([fieldPath length] == 1) { + return [[FSTObjectValue alloc] + initWithImmutableDictionary:[_internalValue dictionaryByRemovingObjectForKey:childName]]; + } else { + FSTFieldValue *child = _internalValue[childName]; + if ([child isKindOfClass:[FSTObjectValue class]]) { + FSTObjectValue *newChild = + [((FSTObjectValue *)child) objectByDeletingPath:[fieldPath pathByRemovingFirstSegment]]; + return [self objectBySettingValue:newChild forField:childName]; + } else { + // If the child is not found or is a primitive type, make no modifications + return self; + } + } +} + +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forField:(NSString *)field { + return [[FSTObjectValue alloc] + initWithImmutableDictionary:[_internalValue dictionaryBySettingObject:value forKey:field]]; +} + +@end + +@interface FSTArrayValue () +@property(nonatomic, strong, readonly) NSArray *internalValue; +@end + +#pragma mark - FSTArrayValue + +@implementation FSTArrayValue + +- (id)initWithValueNoCopy:(NSArray *)value { + self = [super init]; + if (self) { + // Does not copy, assumes the caller has already copied. + _internalValue = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[self class]]) { + return NO; + } + + // NSArray's isEqual does the right thing for our purposes. + FSTArrayValue *otherArray = other; + return [self.internalValue isEqual:otherArray.internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (id)value { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count]; + [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) { + [result addObject:[obj value]]; + }]; + return result; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderArray; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTArrayValue class]]) { + NSArray *selfArray = self.internalValue; + NSArray *otherArray = ((FSTArrayValue *)other).internalValue; + NSUInteger minLength = MIN(selfArray.count, otherArray.count); + for (NSUInteger i = 0; i < minLength; i++) { + NSComparisonResult cmp = [selfArray[i] compare:otherArray[i]]; + if (cmp != NSOrderedSame) { + return cmp; + } + } + return FSTCompareUIntegers(selfArray.count, otherArray.count); + } else { + return [self defaultCompare:other]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h new file mode 100644 index 0000000..ef7f1c8 --- /dev/null +++ b/Firestore/Source/Model/FSTMutation.h @@ -0,0 +1,325 @@ +/* + * 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 + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTMaybeDocument; +@class FSTObjectValue; +@class FSTSnapshotVersion; +@class FSTTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldMask + +/** + * Provides a set of fields that can be used to partially patch a document. FieldMask is used in + * conjunction with ObjectValue. + * + * Examples: + * foo - Overwrites foo entirely with the provided value. If foo is not present in the companion + * ObjectValue, the field is deleted. + * foo.bar - Overwrites only the field bar of the object foo. If foo is not an object, foo is + * replaced with an object containing bar. + */ +@interface FSTFieldMask : NSObject +- (id)init __attribute__((unavailable("Use initWithFields:"))); + +/** + * Initializes the field mask with the given field paths. Caller is expected to either copy or + * or release the array of fields. + */ +- (instancetype)initWithFields:(NSArray *)fields NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) NSArray *fields; +@end + +#pragma mark - FSTFieldTransform + +/** Represents a transform within a TransformMutation. */ +@protocol FSTTransformOperation +@end + +/** Transforms a value into a server-generated timestamp. */ +@interface FSTServerTimestampTransform : NSObject ++ (instancetype)serverTimestampTransform; +@end + +/** A field path and the FSTTransformOperation to perform upon it. */ +@interface FSTFieldTransform : NSObject +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPath:(FSTFieldPath *)path + transform:(id)transform NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) FSTFieldPath *path; +@property(nonatomic, strong, readonly) id transform; +@end + +#pragma mark - FSTPrecondition + +typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { + FSTPreconditionExistsNotSet, + FSTPreconditionExistsYes, + FSTPreconditionExistsNo, +}; + +/** + * Encodes a precondition for a mutation. This follows the model that the backend accepts with the + * special case of an explicit "empty" precondition (meaning no precondition). + */ +@interface FSTPrecondition : NSObject + +/** Creates a new FSTPrecondition with an exists flag. */ ++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists; + +/** Creates a new FSTPrecondition based on a time the document exists at. */ ++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime; + +/** Returns a precondition representing no precondition. */ ++ (FSTPrecondition *)none; + +/** + * Returns true if the preconditions is valid for the given document (or null if no document is + * available). + */ +- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc; + +/** Returns whether this Precondition represents no precondition. */ +- (BOOL)isNone; + +/** If set, preconditions a mutation based on the last updateTime. */ +@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *updateTime; + +/** + * If set, preconditions a mutation based on whether the document exists. + * Uses FSTPreconditionExistsNotSet to mark as unset. + */ +@property(nonatomic, assign, readonly) FSTPreconditionExists exists; + +@end + +#pragma mark - FSTMutationResult + +@interface FSTMutationResult : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version + transformResults:(NSArray *_Nullable)transformResults + NS_DESIGNATED_INITIALIZER; + +/** The version at which the mutation was committed or null for a delete. */ +@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *version; + +/** + * The resulting fields returned from the backend after a FSTTransformMutation has been committed. + * Contains one FieldValue for each FSTFieldTransform that was in the mutation. + * + * Will be nil if the mutation was not a FSTTransformMutation. + */ +@property(nonatomic, strong, readonly) NSArray *_Nullable transformResults; + +@end + +#pragma mark - FSTMutation + +/** + * A mutation describes a self-contained change to a document. Mutations can create, replace, + * delete, and update subsets of documents. + * + * ## Subclassing Notes + * + * Subclasses of FSTMutation need to implement -applyTo:hasLocalMutations: to implement the + * actual the behavior of mutation as applied to some source document. + */ +@interface FSTMutation : NSObject + +- (id)init NS_UNAVAILABLE; + +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER; + +/** + * Applies this mutation to the given FSTDocument, FSTDeletedDocument or nil, if we don't have + * information about this document. Both the input and returned documents can be nil. + * + * @param maybeDoc The document to mutate. The input document should nil if it does not currently + * exist. + * @param localWriteTime A timestamp indicating the local write time of the batch this mutation is + * a part of. + * @param mutationResult Optional result info from the backend. If omitted, it's assumed that + * this is merely a local (latency-compensated) application, and the resulting document will + * have its hasLocalMutations flag set. + * + * @return The mutated document. The returned document may be nil, but only if maybeDoc was nil + * and the mutation would not create a new document. + * + * NOTE: We preserve the version of the base document only in case of Set or Patch mutation to + * denote what version of original document we've changed. In case of DeleteMutation we always reset + * the version. + * + * Here's the expected transition table. + * + * MUTATION APPLIED TO RESULTS IN + * + * SetMutation Document(v3) Document(v3) + * SetMutation DeletedDocument(v3) Document(v0) + * SetMutation nil Document(v0) + * PatchMutation Document(v3) Document(v3) + * PatchMutation DeletedDocument(v3) DeletedDocument(v3) + * PatchMutation nil nil + * TransformMutation Document(v3) Document(v3) + * TransformMutation DeletedDocument(v3) DeletedDocument(v3) + * TransformMutation nil nil + * DeleteMutation Document(v3) DeletedDocument(v0) + * DeleteMutation DeletedDocument(v3) DeletedDocument(v0) + * DeleteMutation nil DeletedDocument(v0) + * + * Note that FSTTransformMutations don't create FSTDocuments (in the case of being applied to an + * FSTDeletedDocument), even though they would on the backend. This is because the client always + * combines the FSTTransformMutation with a FSTSetMutation or FSTPatchMutation and we only want to + * apply the transform if the prior mutation resulted in an FSTDocument (always true for an + * FSTSetMutation, but not necessarily for an FSTPatchMutation). + */ +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult; + +/** + * A helper version of applyTo for applying mutations locally (without a mutation result from the + * backend). + */ +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime; + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; + +/** The precondition for this mutation. */ +@property(nonatomic, strong, readonly) FSTPrecondition *precondition; + +@end + +#pragma mark - FSTSetMutation + +/** + * A mutation that creates or replaces the document at the given key with the object value + * contents. + */ +@interface FSTSetMutation : FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE; + +/** + * Initializes the set mutation. + * + * @param key Identifies the location of the document to mutate. + * @param value An object value that describes the contents to store at the location named by the + * key. + * @param precondition The precondition for this mutation. + */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER; + +/** The object value to use when setting the document. */ +@property(nonatomic, strong, readonly) FSTObjectValue *value; +@end + +#pragma mark - FSTPatchMutation + +/** + * A mutation that modifies fields of the document at the given key with the given values. The + * values are applied through a field mask: + * + * * When a field is in both the mask and the values, the corresponding field is updated. + * * When a field is in neither the mask nor the values, the corresponding field is unmodified. + * * When a field is in the mask but not in the values, the corresponding field is deleted. + * * When a field is not in the mask but is in the values, the values map is ignored. + */ +@interface FSTPatchMutation : FSTMutation + +/** Returns the precondition for the given FSTPrecondition. */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE; + +/** + * Initializes a new patch mutation with an explicit FSTFieldMask and FSTObjectValue representing + * the updates to perform + * + * @param key Identifies the location of the document to mutate. + * @param fieldMask The field mask specifying at what locations the data in value should be + * applied. + * @param value An FSTObjectValue containing the data to be written (using the paths in fieldMask + * to determine the locations at which it should be applied). + * @param precondition The precondition for this mutation. + */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldMask:(FSTFieldMask *)fieldMask + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER; + +/** The fields and associated values to use when patching the document. */ +@property(nonatomic, strong, readonly) FSTObjectValue *value; + +/** + * A mask to apply to |value|, where only fields that are in both the fieldMask and the value + * will be updated. + */ +@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask; + +@end + +#pragma mark - FSTTransformMutation + +/** + * A mutation that modifies specific fields of the document with transform operations. Currently + * the only supported transform is a server timestamp, but IP Address, increment(n), etc. could + * be supported in the future. + * + * It is somewhat similar to an FSTPatchMutation in that it patches specific fields and has no + * effect when applied to nil or an FSTDeletedDocument (see comment on [FSTMutation applyTo] for + * rationale). + */ +@interface FSTTransformMutation : FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE; + +/** + * Initializes a new transform mutation with the specified field transforms. + * + * @param key Identifies the location of the document to mutate. + * @param fieldTransforms A list of FSTFieldTransform objects to perform to the document. + */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldTransforms:(NSArray *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +/** The field transforms to use when transforming the document. */ +@property(nonatomic, strong, readonly) NSArray *fieldTransforms; + +@end + +#pragma mark - FSTDeleteMutation + +@interface FSTDeleteMutation : FSTMutation + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m new file mode 100644 index 0000000..9704fde --- /dev/null +++ b/Firestore/Source/Model/FSTMutation.m @@ -0,0 +1,575 @@ +/* + * 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 "FSTMutation.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" +#import "FSTSnapshotVersion.h" +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldMask + +@implementation FSTFieldMask + +- (instancetype)initWithFields:(NSArray *)fields { + if (self = [super init]) { + _fields = fields; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTFieldMask class]]) { + return NO; + } + + FSTFieldMask *otherMask = (FSTFieldMask *)other; + return [self.fields isEqual:otherMask.fields]; +} + +- (NSUInteger)hash { + return self.fields.hash; +} +@end + +#pragma mark - FSTServerTimestampTransform + +@implementation FSTServerTimestampTransform + ++ (instancetype)serverTimestampTransform { + static FSTServerTimestampTransform *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampTransform alloc] init]; + }); + return sharedInstance; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + return [other isKindOfClass:[FSTServerTimestampTransform class]]; +} + +- (NSUInteger)hash { + // arbitrary number since all instances are equal. + return 37; +} + +@end + +#pragma mark - FSTFieldTransform + +@implementation FSTFieldTransform + +- (instancetype)initWithPath:(FSTFieldPath *)path transform:(id)transform { + self = [super init]; + if (self) { + _path = path; + _transform = transform; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + FSTFieldTransform *otherFieldTransform = other; + return [self.path isEqual:otherFieldTransform.path] && + [self.transform isEqual:otherFieldTransform.transform]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.path hash]; + hash = hash * 31 + [self.transform hash]; + return hash; +} + +@end + +#pragma mark - FSTPrecondition + +@implementation FSTPrecondition + ++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists { + FSTPreconditionExists existsEnum = exists ? FSTPreconditionExistsYes : FSTPreconditionExistsNo; + return [[FSTPrecondition alloc] initWithUpdateTime:nil exists:existsEnum]; +} + ++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime { + return [[FSTPrecondition alloc] initWithUpdateTime:updateTime exists:FSTPreconditionExistsNotSet]; +} + ++ (FSTPrecondition *)none { + static dispatch_once_t onceToken; + static FSTPrecondition *noPrecondition; + dispatch_once(&onceToken, ^{ + noPrecondition = + [[FSTPrecondition alloc] initWithUpdateTime:nil exists:FSTPreconditionExistsNotSet]; + }); + return noPrecondition; +} + +- (instancetype)initWithUpdateTime:(FSTSnapshotVersion *_Nullable)updateTime + exists:(FSTPreconditionExists)exists { + if (self = [super init]) { + _updateTime = updateTime; + _exists = exists; + } + return self; +} + +- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc { + if (self.updateTime) { + return + [maybeDoc isKindOfClass:[FSTDocument class]] && [maybeDoc.version isEqual:self.updateTime]; + } else if (self.exists != FSTPreconditionExistsNotSet) { + if (self.exists == FSTPreconditionExistsYes) { + return [maybeDoc isKindOfClass:[FSTDocument class]]; + } else { + FSTAssert(self.exists == FSTPreconditionExistsNo, @"Invalid precondition"); + return maybeDoc == nil || [maybeDoc isKindOfClass:[FSTDeletedDocument class]]; + } + } else { + FSTAssert(self.isNone, @"Precondition should be empty"); + return YES; + } +} + +- (BOOL)isNone { + return self.updateTime == nil && self.exists == FSTPreconditionExistsNotSet; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + + if (![other isKindOfClass:[FSTPrecondition class]]) { + return NO; + } + + FSTPrecondition *otherPrecondition = (FSTPrecondition *)other; + // Compare references to cover nil equality + return (self.updateTime == otherPrecondition.updateTime || + [self.updateTime isEqual:otherPrecondition.updateTime]) && + self.exists == otherPrecondition.exists; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.updateTime hash]; + hash = hash * 31 + self.exists; + return hash; +} + +- (NSString *)description { + if (self.isNone) { + return @">"; + } else { + NSString *existsString; + switch (self.exists) { + case FSTPreconditionExistsYes: + existsString = @"yes"; + break; + case FSTPreconditionExistsNo: + existsString = @"no"; + break; + default: + existsString = @""; + break; + } + return [NSString stringWithFormat:@"", self.updateTime, + existsString]; + } +} + +@end + +#pragma mark - FSTMutationResult + +@implementation FSTMutationResult + +- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version + transformResults:(NSArray *_Nullable)transformResults { + if (self = [super init]) { + _version = version; + _transformResults = transformResults; + } + return self; +} + +@end + +#pragma mark - FSTMutation + +@implementation FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key precondition:(FSTPrecondition *)precondition { + if (self = [super init]) { + _key = key; + _precondition = precondition; + } + return self; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime { + return [self applyTo:maybeDoc localWriteTime:localWriteTime mutationResult:nil]; +} + +@end + +#pragma mark - FSTSetMutation + +@implementation FSTSetMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + if (self = [super initWithKey:key precondition:precondition]) { + _value = value; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.key, + self.value, self.precondition]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTSetMutation class]]) { + return NO; + } + + FSTSetMutation *otherMutation = (FSTSetMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // If the document didn't exist before, create it. + return [FSTDocument documentWithData:self.value + key:self.key + version:[FSTSnapshotVersion noVersion] + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only set a document with the same key"); + return [FSTDocument documentWithData:self.value + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} +@end + +#pragma mark - FSTPatchMutation + +@implementation FSTPatchMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldMask:(FSTFieldMask *)fieldMask + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + self = [super initWithKey:key precondition:precondition]; + if (self) { + _fieldMask = fieldMask; + _value = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTPatchMutation class]]) { + return NO; + } + + FSTPatchMutation *otherMutation = (FSTPatchMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.fieldMask isEqual:otherMutation.fieldMask] && + [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldMask hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.key, self.fieldMask, self.value, self.precondition]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // Precondition applied, so create the document if necessary + FSTDocumentKey *key = maybeDoc ? maybeDoc.key : self.key; + FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; + maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] + key:key + version:version + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + FSTObjectValue *newData = [self patchObjectValue:doc.data]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +- (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { + FSTObjectValue *result = objectValue; + for (FSTFieldPath *fieldPath in self.fieldMask.fields) { + FSTFieldValue *newValue = [self.value valueForPath:fieldPath]; + if (newValue) { + result = [result objectBySettingValue:newValue forPath:fieldPath]; + } else { + result = [result objectByDeletingPath:fieldPath]; + } + } + return result; +} + +@end + +@implementation FSTTransformMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldTransforms:(NSArray *)fieldTransforms { + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with a FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + if (self = [super initWithKey:key precondition:[FSTPrecondition preconditionWithExists:YES]]) { + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTTransformMutation class]]) { + return NO; + } + + FSTTransformMutation *otherMutation = (FSTTransformMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.fieldTransforms isEqual:otherMutation.fieldTransforms] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldTransforms hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.key, self.fieldTransforms, self.precondition]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(mutationResult.transformResults, + @"Transform results missing for FSTTransformMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + // We only support transforms with precondition exists, so we can only apply it to an existing + // document + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + BOOL hasLocalMutations = (mutationResult == nil); + NSArray *transformResults = + mutationResult ? mutationResult.transformResults + : [self localTransformResultsWithWriteTime:localWriteTime]; + FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +/** + * Creates an array of "transform results" (a transform result is a field value representing the + * result of applying a transform) for use when applying an FSTTransformMutation locally. + * + * @param localWriteTime The local time of the transform mutation (used to generate + * FSTServerTimestampValues). + * @return The transform results array. + */ +- (NSArray *)localTransformResultsWithWriteTime:(FSTTimestamp *)localWriteTime { + NSMutableArray *transformResults = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { + if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { + [transformResults addObject:[FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:localWriteTime]]; + } else { + FSTFail(@"Encountered unknown transform: %@", fieldTransform); + } + } + return transformResults; +} + +- (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue + transformResults:(NSArray *)transformResults { + FSTAssert(transformResults.count == self.fieldTransforms.count, + @"Transform results length mismatch."); + + for (NSUInteger i = 0; i < self.fieldTransforms.count; i++) { + FSTFieldTransform *fieldTransform = self.fieldTransforms[i]; + id transform = fieldTransform.transform; + FSTFieldPath *fieldPath = fieldTransform.path; + if ([transform isKindOfClass:[FSTServerTimestampTransform class]]) { + objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; + } else { + FSTFail(@"Encountered unknown transform: %@", transform); + } + } + return objectValue; +} + +@end + +#pragma mark - FSTDeleteMutation + +@implementation FSTDeleteMutation + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeleteMutation class]]) { + return NO; + } + + FSTDeleteMutation *otherMutation = (FSTDeleteMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"", self.key, self.precondition]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, + @"Transform results received by FSTDeleteMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + if (maybeDoc) { + FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); + } + + return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h new file mode 100644 index 0000000..cbc31b6 --- /dev/null +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -0,0 +1,119 @@ +/* + * 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 + +#import "FSTDocumentKeySet.h" +#import "FSTDocumentVersionDictionary.h" +#import "FSTTypes.h" + +@class FSTMutation; +@class FSTTimestamp; +@class FSTMutationResult; +@class FSTMutationBatchResult; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A BatchID that was searched for and not found or a batch ID value known to be before all known + * batches. + * + * FSTBatchID values from the local store are non-negative so this value is before all batches. + */ +extern const FSTBatchID kFSTBatchIDUnknown; + +/** + * A batch of mutations that will be sent as one unit to the backend. Batches can be marked as a + * tombstone if the mutation queue does not remove them immediately. When a batch is a tombstone + * it has no mutations. + */ +@interface FSTMutationBatch : NSObject + +/** Initializes a mutation batch with the given batchID, localWriteTime, and mutations. */ +- (instancetype)initWithBatchID:(FSTBatchID)batchID + localWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations NS_DESIGNATED_INITIALIZER; + +- (id)init NS_UNAVAILABLE; + +/** + * Applies all the mutations in this FSTMutationBatch to the specified document. + * + * @param maybeDoc The document to apply mutations to. + * @param documentKey The key of the document to apply mutations to. + * @param mutationBatchResult The result of applying the MutationBatch to the backend. If omitted + * it's assumed that this is a local (latency-compensated) application and documents will have + * their hasLocalMutations flag set. + */ +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey + mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult; + +/** + * A helper version of applyTo for applying mutations locally (without a mutation batch result from + * the backend). + */ +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey; + +/** + * Returns YES if this mutation batch has already been removed from the mutation queue. + * + * Note that not all implementations of the FSTMutationQueue necessarily use tombstones as a part + * of their implementation and generally speaking no code outside the mutation queues should really + * care about this. + */ +- (BOOL)isTombstone; + +/** Converts this batch to a tombstone. */ +- (FSTMutationBatch *)toTombstone; + +/** Returns the set of unique keys referenced by all mutations in the batch. */ +- (FSTDocumentKeySet *)keys; + +@property(nonatomic, assign, readonly) FSTBatchID batchID; +@property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@property(nonatomic, strong, readonly) NSArray *mutations; + +@end + +#pragma mark - FSTMutationBatchResult + +/** The result of applying a mutation batch to the backend. */ +@interface FSTMutationBatchResult : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a new FSTMutationBatchResult for the given batch and results. There must be one result + * for each mutation in the batch. This static factory caches a document=>version mapping + * (as docVersions). + */ ++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken; + +@property(nonatomic, strong, readonly) FSTMutationBatch *batch; +@property(nonatomic, strong, readonly) FSTSnapshotVersion *commitVersion; +@property(nonatomic, strong, readonly) NSArray *mutationResults; +@property(nonatomic, strong, readonly, nullable) NSData *streamToken; +@property(nonatomic, strong, readonly) FSTDocumentVersionDictionary *docVersions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m new file mode 100644 index 0000000..ed0e659 --- /dev/null +++ b/Firestore/Source/Model/FSTMutationBatch.m @@ -0,0 +1,176 @@ +/* + * 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 "FSTMutationBatch.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTMutation.h" +#import "FSTSnapshotVersion.h" +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +const FSTBatchID kFSTBatchIDUnknown = -1; + +@implementation FSTMutationBatch + +- (instancetype)initWithBatchID:(FSTBatchID)batchID + localWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray *)mutations { + self = [super init]; + if (self) { + _batchID = batchID; + _localWriteTime = localWriteTime; + _mutations = mutations; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FSTMutationBatch class]]) { + return NO; + } + + FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; + return self.batchID == otherBatch.batchID && + [self.localWriteTime isEqual:otherBatch.localWriteTime] && + [self.mutations isEqual:otherBatch.mutations]; +} + +- (NSUInteger)hash { + NSUInteger result = (NSUInteger)self.batchID; + result = result * 31 + self.localWriteTime.hash; + result = result * 31 + self.mutations.hash; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", + self.batchID, self.localWriteTime, self.mutations]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey + mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult { + FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey], + @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key); + if (mutationBatchResult) { + FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count, + @"Mismatch between mutations length (%lu) and results length (%lu)", + (unsigned long)self.mutations.count, + (unsigned long)mutationBatchResult.mutationResults.count); + } + + for (NSUInteger i = 0; i < self.mutations.count; i++) { + FSTMutation *mutation = self.mutations[i]; + FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i]; + if ([mutation.key isEqualToKey:documentKey]) { + maybeDoc = [mutation applyTo:maybeDoc + localWriteTime:self.localWriteTime + mutationResult:mutationResult]; + } + } + return maybeDoc; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey { + return [self applyTo:maybeDoc documentKey:documentKey mutationBatchResult:nil]; +} + +- (BOOL)isTombstone { + return self.mutations.count == 0; +} + +- (FSTMutationBatch *)toTombstone { + return [[FSTMutationBatch alloc] initWithBatchID:self.batchID + localWriteTime:self.localWriteTime + mutations:@[]]; +} + +// TODO(klimt): This could use NSMutableDictionary instead. +- (FSTDocumentKeySet *)keys { + FSTDocumentKeySet *set = [FSTDocumentKeySet keySet]; + for (FSTMutation *mutation in self.mutations) { + set = [set setByAddingObject:mutation.key]; + } + return set; +} + +@end + +#pragma mark - FSTMutationBatchResult + +@interface FSTMutationBatchResult () +- (instancetype)initWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken + docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTMutationBatchResult + +- (instancetype)initWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken + docVersions:(FSTDocumentVersionDictionary *)docVersions { + if (self = [super init]) { + _batch = batch; + _commitVersion = commitVersion; + _mutationResults = mutationResults; + _streamToken = streamToken; + _docVersions = docVersions; + } + return self; +} + ++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)mutationResults + streamToken:(nullable NSData *)streamToken { + FSTAssert(batch.mutations.count == mutationResults.count, + @"Mutations sent %lu must equal results received %lu", + (unsigned long)batch.mutations.count, (unsigned long)mutationResults.count); + + FSTDocumentVersionDictionary *docVersions = + [FSTDocumentVersionDictionary documentVersionDictionary]; + NSArray *mutations = batch.mutations; + for (NSUInteger i = 0; i < mutations.count; i++) { + FSTSnapshotVersion *_Nullable version = mutationResults[i].version; + if (!version) { + // deletes don't have a version, so we substitute the commitVersion + // of the entire batch. + version = commitVersion; + } + + docVersions = [docVersions dictionaryBySettingObject:version forKey:mutations[i].key]; + } + + return [[FSTMutationBatchResult alloc] initWithBatch:batch + commitVersion:commitVersion + mutationResults:mutationResults + streamToken:streamToken + docVersions:docVersions]; +} + +@end +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTPath.h b/Firestore/Source/Model/FSTPath.h new file mode 100644 index 0000000..1f63f17 --- /dev/null +++ b/Firestore/Source/Model/FSTPath.h @@ -0,0 +1,141 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTPath represents a path sequence in the Firestore database. It is composed of an ordered + * sequence of string segments. + * + * ## Subclassing Notes + * + * FSTPath itself is an abstract class that must be specialized by subclasses. Subclasses should + * implement constructors for common string-based representations of the path and also override + * -canonicalString which converts back to the canonical string-based representation of the path. + */ +@interface FSTPath : NSObject + +/** Returns the path segment of the given index. */ +- (NSString *)segmentAtIndex:(int)index; +- (id)objectAtIndexedSubscript:(int)index; + +- (BOOL)isEqual:(id)path; +- (NSComparisonResult)compare:(SelfType)other; + +/** + * Returns a new path whose segments are the current path's plus one more. + * + * @param segment The new segment to concatenate to the path. + * @return A new path with this path's segment plus the new one. + */ +- (instancetype)pathByAppendingSegment:(NSString *)segment; + +/** + * Returns a new path whose segments are the current path's plus another's. + * + * @param path The new path whose segments should be concatenated to the path. + * @return A new path with this path's segment plus the new ones. + */ +- (instancetype)pathByAppendingPath:(SelfType)path; + +/** Returns a new path whose segments are the same as this one's minus the first one. */ +- (instancetype)pathByRemovingFirstSegment; + +/** Returns a new path whose segments are the same as this one's minus the first `count`. */ +- (instancetype)pathByRemovingFirstSegments:(int)count; + +/** Returns a new path whose segments are the same as this one's minus the last one. */ +- (instancetype)pathByRemovingLastSegment; + +/** Convenience method for getting the first segment of this path. */ +- (NSString *)firstSegment; + +/** Convenience method for getting the last segment of this path. */ +- (NSString *)lastSegment; + +/** Returns true if this path is a prefix of the given path. */ +- (BOOL)isPrefixOfPath:(SelfType)other; + +/** Returns a standardized string representation of this path. */ +- (NSString *)canonicalString; + +/** The number of segments in the path. */ +@property(nonatomic, readonly) int length; + +/** True if the path is empty. */ +@property(nonatomic, readonly, getter=isEmpty) BOOL empty; + +@end + +/** A dot-separated path for navigating sub-objects within a document. */ +@class FSTFieldPath; + +@interface FSTFieldPath : FSTPath + +/** + * Creates and returns a new path with the given segments. The array of segments is not copied, so + * one should not mutate the array once it is passed in here. + * + * @param segments The underlying array of segments for the path. + * @return A new instance of FSTPath. + */ ++ (instancetype)pathWithSegments:(NSArray *)segments; + +/** + * Creates and returns a new path from the server formatted field-path string, where path segments + * are separated by a dot "." and optionally encoded using backticks. + * + * @param fieldPath A dot-separated string representing the path. + */ ++ (instancetype)pathWithServerFormat:(NSString *)fieldPath; + +/** Returns a field path that represents a document key. */ ++ (instancetype)keyFieldPath; + +/** Returns a field path that represents an empty path. */ ++ (instancetype)emptyPath; + +/** Returns YES if this is the `FSTFieldPath.keyFieldPath` field path. */ +- (BOOL)isKeyFieldPath; + +@end + +/** A slash-separated path for navigating resources (documents and collections) within Firestore. */ +@class FSTResourcePath; + +@interface FSTResourcePath : FSTPath + +/** + * Creates and returns a new path with the given segments. The array of segments is not copied, so + * one should not mutate the array once it is passed in here. + * + * @param segments The underlying array of segments for the path. + * @return A new instance of FSTPath. + */ ++ (instancetype)pathWithSegments:(NSArray *)segments; + +/** + * Creates and returns a new path from the given resource-path string, where the path segments are + * separated by a slash "/". + * + * @param resourcePath A slash-separated string representing the path. + */ ++ (instancetype)pathWithString:(NSString *)resourcePath; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTPath.m b/Firestore/Source/Model/FSTPath.m new file mode 100644 index 0000000..0588612 --- /dev/null +++ b/Firestore/Source/Model/FSTPath.m @@ -0,0 +1,356 @@ +/* + * 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 "FSTPath.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDocumentKey.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTPath () +/** An underlying array of which a subset of elements are the segments of the path. */ +@property(strong, nonatomic) NSArray *segments; +/** The index into the segments array of the first segment in this path. */ +@property int offset; +@end + +@implementation FSTPath + +/** + * Designated initializer. + * + * @param segments The underlying array of segments for the path. + * @param offset The starting index in the underlying array for the subarray to use. + * @param length The length of the subarray to use. + */ +- (instancetype)initWithSegments:(NSArray *)segments + offset:(int)offset + length:(int)length { + FSTAssert(offset <= segments.count, @"offset %d out of range %d", offset, (int)segments.count); + FSTAssert(length <= segments.count - offset, @"offset %d out of range %d", offset, + (int)segments.count - offset); + + if (self = [super init]) { + _segments = segments; + _offset = offset; + _length = length; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTPath class]]) { + return NO; + } + FSTPath *path = object; + return [self compare:path] == NSOrderedSame; +} + +- (NSUInteger)hash { + NSUInteger hash = 0; + for (int i = 0; i < self.length; ++i) { + hash += [self segmentAtIndex:i].hash; + } + return hash; +} + +- (NSString *)description { + return [self canonicalString]; +} + +- (id)objectAtIndexedSubscript:(int)index { + return [self segmentAtIndex:index]; +} + +- (NSString *)segmentAtIndex:(int)index { + FSTAssert(index < self.length, @"index %d out of range", index); + return self.segments[self.offset + index]; +} + +- (NSString *)firstSegment { + FSTAssert(!self.isEmpty, @"Cannot call firstSegment on empty path"); + return [self segmentAtIndex:0]; +} + +- (NSString *)lastSegment { + FSTAssert(!self.isEmpty, @"Cannot call lastSegment on empty path"); + return [self segmentAtIndex:self.length - 1]; +} + +- (NSComparisonResult)compare:(FSTPath *)other { + int length = MIN(self.length, other.length); + for (int i = 0; i < length; ++i) { + NSString *left = [self segmentAtIndex:i]; + NSString *right = [other segmentAtIndex:i]; + NSComparisonResult result = [left compare:right]; + if (result != NSOrderedSame) { + return result; + } + } + if (self.length < other.length) { + return NSOrderedAscending; + } + if (self.length > other.length) { + return NSOrderedDescending; + } + return NSOrderedSame; +} + +- (instancetype)pathWithSegments:(NSArray *)segments + offset:(int)offset + length:(int)length { + return [[[self class] alloc] initWithSegments:segments offset:offset length:length]; +} + +- (instancetype)pathByAppendingSegment:(NSString *)segment { + int newLength = self.length + 1; + NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; + for (int i = 0; i < self.length; ++i) { + [segments addObject:self[i]]; + } + [segments addObject:segment]; + return [self pathWithSegments:segments offset:0 length:newLength]; +} + +- (instancetype)pathByAppendingPath:(FSTPath *)path { + int newLength = self.length + path.length; + NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; + for (int i = 0; i < self.length; ++i) { + [segments addObject:self[i]]; + } + for (int i = 0; i < path.length; ++i) { + [segments addObject:path[i]]; + } + return [self pathWithSegments:segments offset:0 length:newLength]; +} + +- (BOOL)isEmpty { + return self.length == 0; +} + +- (instancetype)pathByRemovingFirstSegment { + FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingFirstSegment on empty path"); + return [self pathWithSegments:self.segments offset:self.offset + 1 length:self.length - 1]; +} + +- (instancetype)pathByRemovingFirstSegments:(int)count { + FSTAssert(self.length >= count, @"pathByRemovingFirstSegments:%d on path of length %d", count, + self.length); + return + [self pathWithSegments:self.segments offset:self.offset + count length:self.length - count]; +} + +- (instancetype)pathByRemovingLastSegment { + FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingLastSegment on empty path"); + return [self pathWithSegments:self.segments offset:self.offset length:self.length - 1]; +} + +- (BOOL)isPrefixOfPath:(FSTPath *)other { + if (other.length < self.length) { + return NO; + } + for (int i = 0; i < self.length; ++i) { + if (![self[i] isEqual:other[i]]) { + return NO; + } + } + return YES; +} + +/** Returns a standardized string representation of this path. */ +- (NSString *)canonicalString { + @throw FSTAbstractMethodException(); // NOLINT +} +@end + +@implementation FSTFieldPath ++ (instancetype)pathWithSegments:(NSArray *)segments { + return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; +} + ++ (instancetype)pathWithServerFormat:(NSString *)fieldPath { + NSMutableArray *segments = [NSMutableArray array]; + + // TODO(b/37244157): Once we move to v1beta1, we should make this more strict. Right now, it + // allows non-identifier path components, even if they aren't escaped. Technically, this will + // mangle paths with backticks in them used in v1alpha1, but that's fine. + + const char *source = [fieldPath UTF8String]; + char *segment = (char *)malloc(strlen(source) + 1); + char *segmentEnd = segment; + + // If we're inside '`' backticks, then we should ignore '.' dots. + BOOL inBackticks = NO; + + char c; + do { + // Examine current character. This is legit even on zero-length strings because there's always + // a null terminator. + c = *source++; + switch (c) { + case '\0': // Falls through + case '.': + if (!inBackticks) { + // Segment is complete + *segmentEnd = '\0'; + if (segment == segmentEnd) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with " + @"'.', end with '.', or contain '..'", + fieldPath); + } + + [segments addObject:[NSString stringWithUTF8String:segment]]; + segmentEnd = segment; + } else { + // copy into the current segment + *segmentEnd++ = c; + } + break; + + case '`': + if (inBackticks) { + inBackticks = NO; + } else { + inBackticks = YES; + } + break; + + case '\\': + // advance to escaped character + c = *source++; + // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping. + FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath); + // Fall through + + default: + // copy into the current segment + *segmentEnd++ = c; + break; + } + } while (c); + + FSTAssert(!inBackticks, @"Unterminated ` in path %@", fieldPath); + + free(segment); + return [FSTFieldPath pathWithSegments:segments]; +} + ++ (instancetype)keyFieldPath { + static FSTFieldPath *keyFieldPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyFieldPath = [FSTFieldPath pathWithSegments:@[ kDocumentKeyPath ]]; + }); + return keyFieldPath; +} + ++ (instancetype)emptyPath { + static FSTFieldPath *emptyPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + emptyPath = [FSTFieldPath pathWithSegments:@[]]; + }); + return emptyPath; +} + +/** Return YES if the string could be used as a segment in a field path without escaping. */ ++ (BOOL)isValidIdentifier:(NSString *)segment { + if (segment.length == 0) { + return NO; + } + unichar first = [segment characterAtIndex:0]; + if (first != '_' && (first < 'a' || first > 'z') && (first < 'A' || first > 'Z')) { + return NO; + } + for (int i = 1; i < segment.length; i++) { + unichar c = [segment characterAtIndex:i]; + if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9')) { + return NO; + } + } + return YES; +} + +- (BOOL)isKeyFieldPath { + return [self isEqual:FSTFieldPath.keyFieldPath]; +} + +- (NSString *)canonicalString { + NSMutableString *result = [NSMutableString string]; + for (int i = 0; i < self.length; i++) { + if (i > 0) { + [result appendString:@"."]; + } + + NSString *escaped = [self[i] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + escaped = [escaped stringByReplacingOccurrencesOfString:@"`" withString:@"\\`"]; + if (![FSTFieldPath isValidIdentifier:escaped]) { + escaped = [NSString stringWithFormat:@"`%@`", escaped]; + } + + [result appendString:escaped]; + } + return result; +} + +@end + +@implementation FSTResourcePath ++ (instancetype)pathWithSegments:(NSArray *)segments { + return [[FSTResourcePath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; +} + ++ (instancetype)pathWithString:(NSString *)resourcePath { + // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) + // and just passes them through raw (they exist for legacy reasons and should not be used + // frequently). + + if ([resourcePath rangeOfString:@"//"].location != NSNotFound) { + FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", resourcePath); + } + + NSMutableArray *segments = [[resourcePath componentsSeparatedByString:@"/"] mutableCopy]; + // We may still have an empty segment at the beginning or end if they had a leading or trailing + // slash (which we allow). + [segments removeObject:@""]; + + return [self pathWithSegments:segments]; +} + +- (NSString *)canonicalString { + // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) + // and just passes them through raw (they exist for legacy reasons and should not be used + // frequently). + + NSMutableString *result = [NSMutableString string]; + for (int i = 0; i < self.length; i++) { + if (i > 0) { + [result appendString:@"/"]; + } + [result appendString:self[i]]; + } + return result; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRCollectionReference.h b/Firestore/Source/Public/FIRCollectionReference.h new file mode 100644 index 0000000..11cb969 --- /dev/null +++ b/Firestore/Source/Public/FIRCollectionReference.h @@ -0,0 +1,99 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" +#import "FIRQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentReference; + +/** + * A `FIRCollectionReference` object can be used for adding documents, getting document references, + * and querying for documents (using the methods inherited from `FIRQuery`). + */ +FIR_SWIFT_NAME(CollectionReference) +@interface FIRCollectionReference : FIRQuery + +/** */ +- (id)init __attribute__((unavailable("FIRCollectionReference cannot be created directly."))); + +/** ID of the referenced collection. */ +@property(nonatomic, strong, readonly) NSString *collectionID; + +/** + * For subcollections, `parent` returns the containing `FIRDocumentReference`. For root + * collections, nil is returned. + */ +@property(nonatomic, strong, nullable, readonly) FIRDocumentReference *parent; + +/** + * A string containing the slash-separated path to this this `FIRCollectionReference` (relative to + * the root of the database). + */ +@property(nonatomic, strong, readonly) NSString *path; + +/** + * Returns a FIRDocumentReference pointing to a new document with an auto-generated ID. + * + * @return A FIRDocumentReference pointing to a new document with an auto-generated ID. + */ +- (FIRDocumentReference *)documentWithAutoID FIR_SWIFT_NAME(document()); + +/** + * Gets a `FIRDocumentReference` referring to the document at the specified path, relative to this + * collection's own path. + * + * @param documentPath The slash-separated relative path of the document for which to get a + * `FIRDocumentReference`. + * + * @return The `FIRDocumentReference` for the specified document path. + */ +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath FIR_SWIFT_NAME(document(_:)); + +/** + * Add a new document to this collection with the specified data, assigning it a document ID + * automatically. + * + * @param data An `NSDictionary` containing the data for the new document. + * + * @return A `FIRDocumentReference` pointing to the newly created document. + */ +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary *)data + FIR_SWIFT_NAME(addDocument(data:)); + +/** + * Add a new document to this collection with the specified data, assigning it a document ID + * automatically. + * + * @param data An `NSDictionary` containing the data for the new document. + * @param completion A block to execute once the document has been successfully written. + * + * @return A `FIRDocumentReference` pointing to the newly created document. + */ +// clang-format off +// clang-format breaks the FIR_SWIFT_NAME attribute +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary *)data + completion: + (nullable void (^)(NSError *_Nullable error))completion + FIR_SWIFT_NAME(addDocument(data:completion:)); +// clang-format on + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h new file mode 100644 index 0000000..674e3b2 --- /dev/null +++ b/Firestore/Source/Public/FIRDocumentChange.h @@ -0,0 +1,70 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentSnapshot; + +/** An enumeration of document change types. */ +typedef NS_ENUM(NSInteger, FIRDocumentChangeType) { + /** Indicates a new document was added to the set of documents matching the query. */ + FIRDocumentChangeTypeAdded, + /** Indicates a document within the query was modified. */ + FIRDocumentChangeTypeModified, + /** + * Indicates a document within the query was removed (either deleted or no longer matches + * the query. + */ + FIRDocumentChangeTypeRemoved +} FIR_SWIFT_NAME(DocumentChangeType); + +/** + * A `FIRDocumentChange` represents a change to the documents matching a query. It contains the + * document affected and the type of change that occurred (added, modified, or removed). + */ +FIR_SWIFT_NAME(DocumentChange) +@interface FIRDocumentChange : NSObject + +/** */ +- (id)init __attribute__((unavailable("FIRDocumentChange cannot be created directly."))); + +/** The type of change that occurred (added, modified, or removed). */ +@property(nonatomic, readonly) FIRDocumentChangeType type; + +/** The document affected by this change. */ +@property(nonatomic, strong, readonly) FIRDocumentSnapshot *document; + +/** + * The index of the changed document in the result set immediately prior to this FIRDocumentChange + * (i.e. supposing that all prior FIRDocumentChange objects have been applied). NSNotFound for + * FIRDocumentChangeTypeAdded events. + */ +@property(nonatomic, readonly) NSUInteger oldIndex; + +/** + * The index of the changed document in the result set immediately after this FIRDocumentChange + * (i.e. supposing that all prior FIRDocumentChange objects and the current FIRDocumentChange object + * have been applied). NSNotFound for FIRDocumentChangeTypeRemoved events. + */ +@property(nonatomic, readonly) NSUInteger newIndex; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h new file mode 100644 index 0000000..03340c1 --- /dev/null +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -0,0 +1,219 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" +#import "FIRListenerRegistration.h" + +@class FIRFirestore; +@class FIRCollectionReference; +@class FIRDocumentSnapshot; +@class FIRSetOptions; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Options for use with `[FIRDocumentReference addSnapshotListener]` to control the behavior of the + * snapshot listener. + */ +FIR_SWIFT_NAME(DocumentListenOptions) +@interface FIRDocumentListenOptions : NSObject + ++ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer"); + +- (instancetype)init; + +@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; + +/** + * Sets the includeMetadataChanges option which controls whether metadata-only changes (i.e. only + * `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events. Default is NO. + * + * @param includeMetadataChanges Whether to raise events for metadata-only changes. + * @return The receiver is returned for optional method chaining. + */ +- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges + FIR_SWIFT_NAME(includeMetadataChanges(_:)); + +@end + +typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot, + NSError *_Nullable error); + +/** + * A `FIRDocumentReference` refers to a document location in a Firestore database and can be + * used to write, read, or listen to the location. The document at the referenced location + * may or may not exist. A `FIRDocumentReference` can also be used to create a + * `FIRCollectionReference` to a subcollection. + */ +FIR_SWIFT_NAME(DocumentReference) +@interface FIRDocumentReference : NSObject + +/** */ +- (instancetype)init + __attribute__((unavailable("FIRDocumentReference cannot be created directly."))); + +/** The ID of the document referred to. */ +@property(nonatomic, strong, readonly) NSString *documentID; + +/** A reference to the collection to which this `DocumentReference` belongs. */ +@property(nonatomic, strong, readonly) FIRCollectionReference *parent; + +/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */ +@property(nonatomic, strong, readonly) FIRFirestore *firestore; + +/** + * A string representing the path of the referenced document (relative to the root of the + * database). + */ +@property(nonatomic, strong, readonly) NSString *path; + +/** + * Gets a `FIRCollectionReference` referring to the collection at the specified + * path, relative to this document. + * + * @param collectionPath The slash-separated relative path of the collection for which to get a + * `FIRCollectionReference`. + * + * @return The `FIRCollectionReference` at the specified _collectionPath_. + */ +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath + FIR_SWIFT_NAME(collection(_:)); + +#pragma mark - Writing Data + +/** + * Writes to the document referred to by `FIRDocumentReference`. If the document doesn't yet exist, + * this method creates it and then sets the data. If the document exists, this method overwrites + * the document data with the new values. + * + * @param documentData An `NSDictionary` that contains the fields and data to write to the + * document. + */ +- (void)setData:(NSDictionary *)documentData; + +/** + * Writes to the document referred to by this DocumentReference. If the document does not yet + * exist, it will be created. If you pass `FIRSetOptions`, the provided data will be merged into + * an existing document. + * + * @param documentData An `NSDictionary` that contains the fields and data to write to the + * document. + * @param options A `FIRSetOptions` used to configure the set behavior. + */ +- (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options; + +/** + * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it + * is created. If a document already exists, it is overwritten. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param completion A block to execute once the document has been successfully written. + */ +- (void)setData:(NSDictionary *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion; + +/** + * Writes to the document referred to by this DocumentReference. If the document does not yet + * exist, it will be created. If you pass `FIRSetOptions`, the provided data will be merged into + * an existing document. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param options A `FIRSetOptions` used to configure the set behavior. + * @param completion A block to execute once the document has been successfully written. + */ +- (void)setData:(NSDictionary *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion; + +/** + * Updates fields in the document referred to by this `FIRDocumentReference`. + * If the document does not exist, the update fails (specify a completion block to be notified). + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + */ +- (void)updateData:(NSDictionary *)fields; + +/** + * Updates fields in the document referred to by this `FIRDocumentReference`. If the document + * does not exist, the update fails and the specified completion block receives an error. + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + * @param completion A block to execute when the update is complete. If the update is successful the + * error parameter will be nil, otherwise it will give an indication of how the update failed. + */ +- (void)updateData:(NSDictionary *)fields + completion:(nullable void (^)(NSError *_Nullable error))completion; + +// NOTE: this is named 'deleteDocument' because 'delete' is a keyword in Objective-C++. +/** Deletes the document referred to by this `FIRDocumentReference`. */ +// clang-format off +- (void)deleteDocument FIR_SWIFT_NAME(delete()); +// clang-format on + +/** + * Deletes the document referred to by this `FIRDocumentReference`. + * + * @param completion A block to execute once the document has been successfully deleted. + */ +// clang-format off +- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion + FIR_SWIFT_NAME(delete(completion:)); +// clang-format on + +#pragma mark - Retrieving Data + +/** + * Reads the document referenced by this `FIRDocumentReference`. + * + * @param completion a block to execute once the document has been successfully read. + */ +- (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion + FIR_SWIFT_NAME(getDocument(completion:)); + +/** + * Attaches a listener for DocumentSnapshot events. + * + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +- (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(_:)); + +/** + * Attaches a listener for DocumentSnapshot events. + * + * @param options Options controlling the listener behavior. + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +// clang-format off +- (id)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(options:listener:)); +// clang-format on + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h new file mode 100644 index 0000000..e923e3e --- /dev/null +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -0,0 +1,68 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +@class FIRDocumentReference; +@class FIRSnapshotMetadata; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A `FIRDocumentSnapshot` contains data read from a document in your Firestore database. The data + * can be extracted with the `data` property or by using subscript syntax to access a specific + * field. + */ +FIR_SWIFT_NAME(DocumentSnapshot) +@interface FIRDocumentSnapshot : NSObject + +/** */ +- (instancetype)init + __attribute__((unavailable("FIRDocumentSnapshot cannot be created directly."))); + +/** True if the document exists. */ +@property(nonatomic, assign, readonly) BOOL exists; + +/** A `FIRDocumentReference` to the document location. */ +@property(nonatomic, strong, readonly) FIRDocumentReference *reference; + +/** The ID of the document for which this `FIRDocumentSnapshot` contains data. */ +@property(nonatomic, copy, readonly) NSString *documentID; + +/** Metadata about this snapshot concerning its source and if it has local modifications. */ +@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; + +/** + * Retrieves all fields in the document as an `NSDictionary`. + * + * @return An `NSDictionary` containing all fields in the document. + */ +- (NSDictionary *)data; + +/** + * Retrieves a specific field from the document. + * + * @param key The field to retrieve. + * + * @return The value contained in the field or `nil` if the field doesn't exist. + */ +- (nullable id)objectForKeyedSubscript:(id)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFieldPath.h b/Firestore/Source/Public/FIRFieldPath.h new file mode 100644 index 0000000..b80eda7 --- /dev/null +++ b/Firestore/Source/Public/FIRFieldPath.h @@ -0,0 +1,50 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A `FieldPath` refers to a field in a document. The path may consist of a single field name + * (referring to a top level field in the document), or a list of field names (referring to a nested + * field in the document). + */ +FIR_SWIFT_NAME(FieldPath) +@interface FIRFieldPath : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a `FieldPath` from the provided field names. If more than one field name is provided, the + * path will point to a nested field in a document. + * + * @param fieldNames A list of field names. + * @return A `FieldPath` that points to a field location in a document. + */ +- (instancetype)initWithFields:(NSArray *)fieldNames FIR_SWIFT_NAME(init(_:)); + +/** + * A special sentinel `FieldPath` to refer to the ID of a document. It can be used in queries to + * sort or filter by the document ID. + */ ++ (instancetype)documentID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFieldValue.h b/Firestore/Source/Public/FIRFieldValue.h new file mode 100644 index 0000000..f7d19f0 --- /dev/null +++ b/Firestore/Source/Public/FIRFieldValue.h @@ -0,0 +1,45 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Sentinel values that can be used when writing document fields with setData() or updateData(). + */ +FIR_SWIFT_NAME(FieldValue) +@interface FIRFieldValue : NSObject + +/** */ +- (instancetype)init NS_UNAVAILABLE; + +/** Used with updateData() to mark a field for deletion. */ +// clang-format off ++ (instancetype)fieldValueForDelete FIR_SWIFT_NAME(delete()); +// clang-format on + +/** + * Used with setData() or updateData() to include a server-generated timestamp in the written + * data. + */ ++ (instancetype)fieldValueForServerTimestamp FIR_SWIFT_NAME(serverTimestamp()); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h new file mode 100644 index 0000000..c31fef6 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestore.h @@ -0,0 +1,145 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +@class FIRApp; +@class FIRCollectionReference; +@class FIRDocumentReference; +@class FIRFirestoreSettings; +@class FIRTransaction; +@class FIRWriteBatch; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `FIRFirestore` represents a Firestore Database and is the entry point for all Firestore + * operations. + */ +FIR_SWIFT_NAME(Firestore) +@interface FIRFirestore : NSObject + +#pragma mark - Initializing +/** */ +- (instancetype)init __attribute__((unavailable("Use a static constructor method."))); + +/** + * Creates, caches, and returns a `FIRFirestore` using the default `FIRApp`. Each subsequent + * invocation returns the same `FIRFirestore` object. + * + * @return The `FIRFirestore` instance. + */ ++ (instancetype)firestore FIR_SWIFT_NAME(firestore()); + +/** + * Creates, caches, and returns a `FIRFirestore` object for the specified _app_. Each subsequent + * invocation returns the same `FIRFirestore` object. + * + * @param app The `FIRApp` instance to use for authentication and as a source of the Google Cloud + * Project ID for your Firestore Database. If you want the default instance, you should explicitly + * set it to `[FIRApp defaultApp]`. + * + * @return The `FIRFirestore` instance. + */ ++ (instancetype)firestoreForApp:(FIRApp *)app FIR_SWIFT_NAME(firestore(app:)); + +/** + * Custom settings used to configure this `FIRFirestore` object. + */ +@property(nonatomic, copy) FIRFirestoreSettings *settings; + +/** + * The Firebase App associated with this Firestore instance. + */ +@property(strong, nonatomic, readonly) FIRApp *app; + +#pragma mark - Collections and Documents + +/** + * Gets a `FIRCollectionReference` referring to the collection at the specified path within the + * database. + * + * @param collectionPath The slash-separated path of the collection for which to get a + * `FIRCollectionReference`. + * + * @return The `FIRCollectionReference` at the specified _collectionPath_. + */ +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath + FIR_SWIFT_NAME(collection(_:)); + +/** + * Gets a `FIRDocumentReference` referring to the document at the specified path within the + * database. + * + * @param documentPath The slash-separated path of the document for which to get a + * `FIRDocumentReference`. + * + * @return The `FIRDocumentReference` for the specified _documentPath_. + */ +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath FIR_SWIFT_NAME(document(_:)); + +#pragma mark - Transactions and Write Batches + +/** + * Executes the given updateBlock and then attempts to commit the changes applied within an atomic + * transaction. + * + * In the updateBlock, a set of reads and writes can be performed atomically using the + * `FIRTransaction` object passed to the block. After the updateBlock is run, Firestore will attempt + * to apply the changes to the server. If any of the data read has been modified outside of this + * transaction since being read, then the transaction will be retried by executing the updateBlock + * again. If the transaction still fails after 5 retries, then the transaction will fail. + * + * Since the updateBlock may be executed multiple times, it should avoiding doing anything that + * would cause side effects. + * + * Any value maybe be returned from the updateBlock. If the transaction is successfully committed, + * then the completion block will be passed that value. The updateBlock also has an `NSError` out + * parameter. If this is set, then the transaction will not attempt to commit, and the given error + * will be passed to the completion block. + * + * The `FIRTransaction` object passed to the updateBlock contains methods for accessing documents + * and collections. Unlike other firestore access, data accessed with the transaction will not + * reflect local changes that have not been committed. For this reason, it is required that all + * reads are performed before any writes. Transactions must be performed while online. Otherwise, + * reads will fail, and the final commit will fail. + * + * @param updateBlock The block to execute within the transaction context. + * @param completion The block to call with the result or error of the transaction. + */ +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock + completion:(void (^)(id _Nullable result, NSError *_Nullable error))completion; + +/** + * Creates a write batch, used for performing multiple writes as a single + * atomic operation. + * + * Unlike transactions, write batches are persisted offline and therefore are preferable when you + * don't need to condition your writes on read data. + */ +- (FIRWriteBatch *)batch; + +#pragma mark - Logging + +/** Enables or disables logging from the Firestore client. */ ++ (void)enableLogging:(BOOL)logging + DEPRECATED_MSG_ATTRIBUTE("Use FIRSetLoggerLevel(FIRLoggerLevelDebug) to enable logging"); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreErrors.h b/Firestore/Source/Public/FIRFirestoreErrors.h new file mode 100644 index 0000000..f2e19d9 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreErrors.h @@ -0,0 +1,105 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The Cloud Firestore error domain. */ +FOUNDATION_EXPORT NSString *const FIRFirestoreErrorDomain FIR_SWIFT_NAME(FirestoreErrorDomain); + +/** Error codes used by Cloud Firestore. */ +typedef NS_ENUM(NSInteger, FIRFirestoreErrorCode) { + /** + * The operation completed successfully. NSError objects will never have a code with this value. + */ + FIRFirestoreErrorCodeOK = 0, + + /** The operation was cancelled (typically by the caller). */ + FIRFirestoreErrorCodeCancelled = 1, + + /** Unknown error or an error from a different error domain. */ + FIRFirestoreErrorCodeUnknown = 2, + + /** + * Client specified an invalid argument. Note that this differs from FailedPrecondition. + * InvalidArgument indicates arguments that are problematic regardless of the state of the + * system (e.g., an invalid field name). + */ + FIRFirestoreErrorCodeInvalidArgument = 3, + + /** + * Deadline expired before operation could complete. For operations that change the state of the + * system, this error may be returned even if the operation has completed successfully. For + * example, a successful response from a server could have been delayed long enough for the + * deadline to expire. + */ + FIRFirestoreErrorCodeDeadlineExceeded = 4, + + /** Some requested document was not found. */ + FIRFirestoreErrorCodeNotFound = 5, + + /** Some document that we attempted to create already exists. */ + FIRFirestoreErrorCodeAlreadyExists = 6, + + /** The caller does not have permission to execute the specified operation. */ + FIRFirestoreErrorCodePermissionDenied = 7, + + /** + * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + * is out of space. + */ + FIRFirestoreErrorCodeResourceExhausted = 8, + + /** + * Operation was rejected because the system is not in a state required for the operation's + * execution. + */ + FIRFirestoreErrorCodeFailedPrecondition = 9, + + /** + * The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. + */ + FIRFirestoreErrorCodeAborted = 10, + + /** Operation was attempted past the valid range. */ + FIRFirestoreErrorCodeOutOfRange = 11, + + /** Operation is not implemented or not supported/enabled. */ + FIRFirestoreErrorCodeUnimplemented = 12, + + /** + * Internal errors. Means some invariants expected by underlying system has been broken. If you + * see one of these errors, something is very broken. + */ + FIRFirestoreErrorCodeInternal = 13, + + /** + * The service is currently unavailable. This is a most likely a transient condition and may be + * corrected by retrying with a backoff. + */ + FIRFirestoreErrorCodeUnavailable = 14, + + /** Unrecoverable data loss or corruption. */ + FIRFirestoreErrorCodeDataLoss = 15, + + /** The request does not have valid authentication credentials for the operation. */ + FIRFirestoreErrorCodeUnauthenticated = 16 +} FIR_SWIFT_NAME(FirestoreErrorCode); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreSettings.h b/Firestore/Source/Public/FIRFirestoreSettings.h new file mode 100644 index 0000000..7097e60 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreSettings.h @@ -0,0 +1,51 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Settings used to configure a `FIRFirestore` instance. */ +FIR_SWIFT_NAME(FirestoreSettings) +@interface FIRFirestoreSettings : NSObject + +/** + * Creates and returns an empty `FIRFirestoreSettings` object. + * + * @return The created `FIRFirestoreSettings` object. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** The hostname to connect to. */ +@property(nonatomic, copy) NSString *host; + +/** Whether to use SSL when connecting. */ +@property(nonatomic, getter=isSSLEnabled) BOOL sslEnabled; + +/** + * A dispatch queue to be used to execute all completion handlers and event handlers. By default, + * the main queue is used. + */ +@property(nonatomic, strong) dispatch_queue_t dispatchQueue; + +/** Set to false to disable local persistent storage. */ +@property(nonatomic, getter=isPersistenceEnabled) BOOL persistenceEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h b/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h new file mode 100644 index 0000000..216c047 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.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. + */ + +#ifndef FIR_SWIFT_NAME + +#import + +// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. +// Wrap it in our own macro if it's a non-compatible SDK. +#ifdef __IPHONE_9_3 +#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) +#else +#define FIR_SWIFT_NAME(X) // Intentionally blank. +#endif // #ifdef __IPHONE_9_3 + +#endif // FIR_SWIFT_NAME diff --git a/Firestore/Source/Public/FIRGeoPoint.h b/Firestore/Source/Public/FIRGeoPoint.h new file mode 100644 index 0000000..de409b5 --- /dev/null +++ b/Firestore/Source/Public/FIRGeoPoint.h @@ -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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An immutable object representing a geographical point in Firestore. The point is represented as + * a latitude/longitude pair. + * + * Latitude values are in the range of [-90, 90]. + * Longitude values are in the range of [-180, 180]. + */ +FIR_SWIFT_NAME(GeoPoint) +@interface FIRGeoPoint : NSObject + +/** */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a `GeoPoint` from the provided latitude and longitude degrees. + * @param latitude The latitude as number between -90 and 90. + * @param longitude The longitude as number between -180 and 180. + */ +- (instancetype)initWithLatitude:(double)latitude + longitude:(double)longitude NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, readonly) double latitude; +@property(nonatomic, readonly) double longitude; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRListenerRegistration.h b/Firestore/Source/Public/FIRListenerRegistration.h new file mode 100644 index 0000000..5fe7fd5 --- /dev/null +++ b/Firestore/Source/Public/FIRListenerRegistration.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 + +NS_ASSUME_NONNULL_BEGIN + +/** Represents a listener that can be removed by calling remove. */ +@protocol FIRListenerRegistration + +/** + * Removes the listener being tracked by this FIRListenerRegistration. After the initial call, + * subsequent calls have no effect. + */ +- (void)remove; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h new file mode 100644 index 0000000..5c5546d --- /dev/null +++ b/Firestore/Source/Public/FIRQuery.h @@ -0,0 +1,414 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" +#import "FIRListenerRegistration.h" + +@class FIRFieldPath; +@class FIRFirestore; +@class FIRQuerySnapshot; +@class FIRDocumentSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Options for use with `[FIRQuery addSnapshotListener]` to control the behavior of the snapshot + * listener. + */ +FIR_SWIFT_NAME(QueryListenOptions) +@interface FIRQueryListenOptions : NSObject + ++ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer"); + +- (instancetype)init; + +@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges; + +/** + * Sets the includeQueryMetadataChanges option which controls whether metadata-only changes on the + * query (i.e. only `FIRQuerySnapshot.metadata` changed) should trigger snapshot events. Default is + * NO. + * + * @param includeQueryMetadataChanges Whether to raise events for metadata-only changes on the + * query. + * @return The receiver is returned for optional method chaining. + */ +- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + FIR_SWIFT_NAME(includeQueryMetadataChanges(_:)); + +@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges; + +/** + * Sets the includeDocumentMetadataChanges option which controls whether document metadata-only + * changes (i.e. only `FIRDocumentSnapshot.metadata` on a document contained in the query + * changed) should trigger snapshot events. Default is NO. + * + * @param includeDocumentMetadataChanges Whether to raise events for document metadata-only changes. + * @return The receiver is returned for optional method chaining. + */ +- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + FIR_SWIFT_NAME(includeDocumentMetadataChanges(_:)); + +@end + +typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error); + +/** + * A `FIRQuery` refers to a Query which you can read or listen to. You can also construct + * refined `FIRQuery` objects by adding filters and ordering. + */ +FIR_SWIFT_NAME(Query) +@interface FIRQuery : NSObject +/** */ +- (id)init __attribute__((unavailable("FIRQuery cannot be created directly."))); + +/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */ +@property(nonatomic, strong, readonly) FIRFirestore *firestore; + +#pragma mark - Retrieving Data +/** + * Reads the documents matching this query. + * + * @param completion a block to execute once the documents have been successfully read. + * documentSet will be `nil` only if error is `non-nil`. + */ +- (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion + FIR_SWIFT_NAME(getDocuments(completion:)); + +/** + * Attaches a listener for QuerySnapshot events. + * + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +- (id)addSnapshotListener:(FIRQuerySnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(_:)); + +/** + * Attaches a listener for QuerySnapshot events. + * + * @param options Options controlling the listener behavior. + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +// clang-format off +- (id)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(options:listener:)); +// clang-format on + +#pragma mark - Filtering Data +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be equal to the specified value. + * + * @param field The name of the field to compare. + * @param value The value the field must be equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be equal to the specified value. + * + * @param path The path of the field to compare. + * @param value The value the field must be equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than the specified value. + * + * @param field The name of the field to compare. + * @param value The value the field must be less than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isLessThan:(id)value FIR_SWIFT_NAME(whereField(_:isLessThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than the specified value. + * + * @param path The path of the field to compare. + * @param value The value the field must be less than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isLessThan:(id)value FIR_SWIFT_NAME(whereField(_:isLessThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than or equal to the specified value. + * + * @param field The name of the field to compare + * @param value The value the field must be less than or equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isLessThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than or equal to the specified value. + * + * @param path The path of the field to compare + * @param value The value the field must be less than or equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isLessThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must greater than the specified value. + * + * @param field The name of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isGreaterThan:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must greater than the specified value. + * + * @param path The path of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isGreaterThan:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be greater than or equal to the specified value. + * + * @param field The name of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isGreaterThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be greater than or equal to the specified value. + * + * @param path The path of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isGreaterThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:)); +// clang-format on + +#pragma mark - Sorting Data +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field. + * + * @param field The field to sort by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryOrderedByField:(NSString *)field FIR_SWIFT_NAME(order(by:)); + +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field. + * + * @param path The field to sort by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path FIR_SWIFT_NAME(order(by:)); + +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field, + * optionally in descending order instead of ascending. + * + * @param field The field to sort by. + * @param descending Whether to sort descending. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryOrderedByField:(NSString *)field + descending:(BOOL)descending FIR_SWIFT_NAME(order(by:descending:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field, + * optionally in descending order instead of ascending. + * + * @param path The field to sort by. + * @param descending Whether to sort descending. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path + descending:(BOOL)descending FIR_SWIFT_NAME(order(by:descending:)); +// clang-format on + +#pragma mark - Limiting Data +/** + * Creates and returns a new `FIRQuery` that's additionally limited to only return up to + * the specified number of documents. + * + * @param limit The maximum number of items to return. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryLimitedTo:(NSInteger)limit FIR_SWIFT_NAME(limit(to:)); + +#pragma mark - Choosing Endpoints +/** + * Creates and returns a new `FIRQuery` that starts at the provided document (inclusive). The + * starting position is relative to the order of the query. The document must contain all of the + * fields provided in the orderBy of this query. + * + * @param document The snapshot of the document to start at. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(start(atDocument:)); + +/** + * Creates and returns a new `FIRQuery` that starts at the provided fields relative to the order of + * the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to start this query at, in order of the query's order by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues FIR_SWIFT_NAME(start(at:)); + +/** + * Creates and returns a new `FIRQuery` that starts after the provided document (exclusive). The + * starting position is relative to the order of the query. The document must contain all of the + * fields provided in the orderBy of this query. + * + * @param document The snapshot of the document to start after. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(start(afterDocument:)); + +/** + * Creates and returns a new `FIRQuery` that starts after the provided fields relative to the order + * of the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to start this query after, in order of the query's order + * by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues FIR_SWIFT_NAME(start(after:)); + +/** + * Creates and returns a new `FIRQuery` that ends before the provided document (exclusive). The end + * position is relative to the order of the query. The document must contain all of the fields + * provided in the orderBy of this query. + * + * @param document The snapshot of the document to end before. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(end(beforeDocument:)); + +/** + * Creates and returns a new `FIRQuery` that ends before the provided fields relative to the order + * of the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to end this query before, in order of the query's order by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues FIR_SWIFT_NAME(end(before:)); + +/** + * Creates and returns a new `FIRQuery` that ends at the provided document (exclusive). The end + * position is relative to the order of the query. The document must contain all of the fields + * provided in the orderBy of this query. + * + * @param document The snapshot of the document to end at. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(end(atDocument:)); + +/** + * Creates and returns a new `FIRQuery` that ends at the provided fields relative to the order of + * the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to end this query at, in order of the query's order by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues FIR_SWIFT_NAME(end(at:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h new file mode 100644 index 0000000..800368d --- /dev/null +++ b/Firestore/Source/Public/FIRQuerySnapshot.h @@ -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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentChange; +@class FIRDocumentSnapshot; +@class FIRQuery; +@class FIRSnapshotMetadata; + +/** + * A `FIRQuerySnapshot` contains zero or more `FIRDocumentSnapshot` objects. It can be enumerated + * using "for ... in documentSet.documents" and its size can be inspected with `isEmpty` and + * `count`. + */ +FIR_SWIFT_NAME(QuerySnapshot) +@interface FIRQuerySnapshot : NSObject + +/** */ +- (id)init __attribute__((unavailable("FIRQuerySnapshot cannot be created directly."))); + +/** + * The query on which you called `getDocuments` or listened to in order to get this + * `FIRQuerySnapshot`. + */ +@property(nonatomic, strong, readonly) FIRQuery *query; + +/** Metadata about this snapshot, concerning its source and if it has local modifications. */ +@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; + +/** Indicates whether this `FIRQuerySnapshot` is empty (contains no documents). */ +@property(nonatomic, readonly, getter=isEmpty) BOOL empty; + +/** The count of documents in this `FIRQuerySnapshot`. */ +@property(nonatomic, readonly) NSInteger count; + +/** An Array of the `FIRDocumentSnapshots` that make up this document set. */ +@property(nonatomic, strong, readonly) NSArray *documents; + +/** + * An array of the documents that changed since the last snapshot. If this is the first snapshot, + * all documents will be in the list as Added changes. + */ +@property(nonatomic, strong, readonly) NSArray *documentChanges; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRSetOptions.h b/Firestore/Source/Public/FIRSetOptions.h new file mode 100644 index 0000000..c865e06 --- /dev/null +++ b/Firestore/Source/Public/FIRSetOptions.h @@ -0,0 +1,46 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * An options object that configures the behavior of setData() calls. By providing the + * `FIRSetOptions` objects returned by `merge:`, the setData() methods in `FIRDocumentReference`, + * `FIRWriteBatch` and `FIRTransaction` can be configured to perform granular merges instead + * of overwriting the target documents in their entirety. + */ +NS_SWIFT_NAME(SetOptions) +@interface FIRSetOptions : NSObject + +/** */ +- (id)init NS_UNAVAILABLE; +/** + * Changes the behavior of setData() calls to only replace the values specified in its data + * argument. Fields with no corresponding values in the data passed to setData() will remain + * untouched. + * + * @return The created `FIRSetOptions` object + */ ++ (instancetype)merge; + +/** Whether setData() should merge existing data instead of performing an overwrite. */ +@property(nonatomic, readonly, getter=isMerge) BOOL merge; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRSnapshotMetadata.h b/Firestore/Source/Public/FIRSnapshotMetadata.h new file mode 100644 index 0000000..7fdd49c --- /dev/null +++ b/Firestore/Source/Public/FIRSnapshotMetadata.h @@ -0,0 +1,44 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** Metadata about a snapshot, describing the state of the snapshot. */ +@interface FIRSnapshotMetadata : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Returns YES if the snapshot contains the result of local writes (e.g. set() or update() calls) + * that have not yet been committed to the backend. If your listener has opted into metadata updates + * (via `FIRDocumentListenOptions` or `FIRQueryListenOptions`) you will receive another snapshot + * with `hasPendingWrites` equal to NO once the writes have been committed to the backend. + */ +@property(nonatomic, assign, readonly, getter=hasPendingWrites) BOOL pendingWrites; + +/** + * Returns YES if the snapshot was created from cached data rather than guaranteed up-to-date server + * data. If your listener has opted into metadata updates (via `FIRDocumentListenOptions` or + * `FIRQueryListenOptions`) you will receive another snapshot with `isFromCache` equal to NO once + * the client has received up-to-date data from the backend. + */ +@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h new file mode 100644 index 0000000..68e4600 --- /dev/null +++ b/Firestore/Source/Public/FIRTransaction.h @@ -0,0 +1,106 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentReference; +@class FIRDocumentSnapshot; +@class FIRSetOptions; + +/** + * `FIRTransaction` provides methods to read and write data within a transaction. + * + * @see FIRFirestore#transaction:completion: + */ +FIR_SWIFT_NAME(Transaction) +@interface FIRTransaction : NSObject + +/** */ +- (id)init __attribute__((unavailable("FIRTransaction cannot be created directly."))); + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If the document exists, this method overwrites + * the document data with the new values. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(setData(_:forDocument:)); +// clang-format on + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If you pass `FIRSetOptions`, the provided data + * will be merged into an existing document. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @param options A `FIRSetOptions` used to configure the set behavior. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options + FIR_SWIFT_NAME(setData(_:forDocument:options:)); +// clang-format on + +/** + * Updates fields in the document referred to by `document`. + * If the document does not exist, the transaction will fail. + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + * @param document A reference to the document whose data should be updated. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(updateData(_:forDocument:)); +// clang-format on + +/** + * Deletes the document referred to by `document`. + * + * @param document A reference to the document that should be deleted. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(deleteDocument(_:)); + +/** + * Reads the document referenced by `document`. + * + * @param document A reference to the document to be read. + * @param error An out parameter to capture an error, if one occurred. + */ +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error + FIR_SWIFT_NAME(getDocument(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h new file mode 100644 index 0000000..b88e6cc --- /dev/null +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -0,0 +1,107 @@ +/* + * 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 + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentReference; +@class FIRSetOptions; + +/** + * A write batch is used to perform multiple writes as a single atomic unit. + * + * A WriteBatch object can be acquired by calling [FIRFirestore batch]. It provides methods for + * adding writes to the write batch. None of the writes will be committed (or visible locally) + * until [FIRWriteBatch commit] is called. + * + * Unlike transactions, write batches are persisted offline and therefore are preferable when you + * don't need to condition your writes on read data. + */ +FIR_SWIFT_NAME(WriteBatch) +@interface FIRWriteBatch : NSObject + +/** :nodoc: */ +- (id)init __attribute__((unavailable("FIRWriteBatch cannot be created directly."))); + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If the document exists, this method overwrites + * the document data with the new values. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document FIR_SWIFT_NAME(setData(_:forDocument:)); +// clang-format on + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If you pass `FIRSetOptions`, the provided data + * will be merged into an existing document. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @param options A `FIRSetOptions` used to configure the set behavior. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options + FIR_SWIFT_NAME(setData(_:forDocument:options:)); +// clang-format on + +/** + * Updates fields in the document referred to by `document`. + * If document does not exist, the write batch will fail. + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + * @param document A reference to the document whose data should be updated. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)updateData:(NSDictionary *)fields + forDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(updateData(_:forDocument:)); +// clang-format on + +/** + * Deletes the document referred to by `document`. + * + * @param document A reference to the document that should be deleted. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(deleteDocument(_:)); + +/** + * Commits all of the writes in this write batch as a single atomic unit. + * + * @param completion A block to be called once all of the writes in the batch have been + * successfully written to the backend as an atomic unit. + */ +- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTBufferedWriter.h b/Firestore/Source/Remote/FSTBufferedWriter.h new file mode 100644 index 0000000..83fada6 --- /dev/null +++ b/Firestore/Source/Remote/FSTBufferedWriter.h @@ -0,0 +1,44 @@ +/* + * 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 +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A buffered GRXWriter. + * + * GRPC only allows a single message to be written to a channel at a time. While the channel is + * sending, GRPC sets the state of the GRXWriter representing the request stream to + * GRXWriterStatePaused. Once the channel is ready to accept more messages GRPC sets the state of + * the writer to GRXWriterStateStarted. + * + * This class is NOT thread safe, even though it is accessed from multiple threads. To conform with + * the contract GRPC uses, all method calls on the FSTBufferedWriter must be @synchronized on the + * receiver. + */ +@interface FSTBufferedWriter : GRXWriter + +/** + * Writes a message into the buffer. Must be called inside an @synchronized block on the receiver. + */ +- (void)writeValue:(id)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTBufferedWriter.m b/Firestore/Source/Remote/FSTBufferedWriter.m new file mode 100644 index 0000000..d86e03a --- /dev/null +++ b/Firestore/Source/Remote/FSTBufferedWriter.m @@ -0,0 +1,134 @@ +/* + * 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 + +#import "FSTBufferedWriter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTBufferedWriter { + GRXWriterState _state; + NSMutableArray *_queue; + + id _writeable; +} + +- (instancetype)init { + if (self = [super init]) { + _state = GRXWriterStateNotStarted; + _queue = [[NSMutableArray alloc] init]; + } + return self; +} + +#pragma mark - GRXWriteable implementation + +/** Push the next value of the sequence to the receiving object. */ +- (void)writeValue:(id)value { + if (_state == GRXWriterStateStarted && _queue.count == 0) { + // Skip the queue. + [_writeable writeValue:value]; + } else { + // Buffer the new value. Note that the value is assumed to be transient and doesn't need to + // be copied. + [_queue addObject:value]; + } +} + +/** + * Signal that the sequence is completed, or that an error ocurred. After this message is sent to + * the receiver, neither it nor writeValue: may be called again. + */ +- (void)writesFinishedWithError:(nullable NSError *)error { + // Unimplemented. If we ever wanted to implement sender-side initiated half close we could do so + // by buffering (or sending) and error. + [self doesNotRecognizeSelector:_cmd]; +} + +#pragma mark GRXWriter implementation +// The GRXWriter implementation defines the send side of the RPC stream. Once the RPC is ready it +// will call startWithWriteable passing a GRXWriteable into which requests can be written but only +// when the GRXWriter is in the started state. + +/** + * Called by GRPCCall when it is ready to accept for the first request. Requests should be written + * to the passed writeable. + * + * GRPCCall will synchronize on the receiver around this call. + */ +- (void)startWithWriteable:(id)writeable { + _state = GRXWriterStateStarted; + _writeable = writeable; +} + +/** + * Called by GRPCCall to implement flow control on the sending side of the stream. After each + * writeValue: on the requestsWriteable, GRPCCall will call setState:GRXWriterStatePaused to apply + * backpressure. Once the stream is ready to accept another message, GRPCCall will call + * setState:GRXWriterStateStarted. + * + * GRPCCall will synchronize on the receiver around this call. + */ +- (void)setState:(GRXWriterState)newState { + // Manual transitions are only allowed from the started or paused states. + if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { + return; + } + + switch (newState) { + case GRXWriterStateFinished: + _state = newState; + // Per GRXWriter's contract, setting the state to Finished manually means one doesn't wish the + // writeable to be messaged anymore. + _queue = nil; + _writeable = nil; + return; + case GRXWriterStatePaused: + _state = newState; + return; + case GRXWriterStateStarted: + if (_state == GRXWriterStatePaused) { + _state = newState; + [self writeBufferedMessages]; + } + return; + case GRXWriterStateNotStarted: + return; + } +} + +- (void)finishWithError:(nullable NSError *)error { + [_writeable writesFinishedWithError:error]; + self.state = GRXWriterStateFinished; +} + +- (void)writeBufferedMessages { + while (_state == GRXWriterStateStarted && _queue.count > 0) { + id value = _queue[0]; + [_queue removeObjectAtIndex:0]; + + // In addition to writing the value here GRPC will apply backpressure by pausing the GRXWriter + // wrapping this buffer. That writer must call -pauseMessages which will cause this loop to + // exit. Synchronization is not required since the callback happens within the body of the + // writeValue implementation. + [_writeable writeValue:value]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTDatastore.h b/Firestore/Source/Remote/FSTDatastore.h new file mode 100644 index 0000000..840d2fe --- /dev/null +++ b/Firestore/Source/Remote/FSTDatastore.h @@ -0,0 +1,365 @@ +/* + * 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 + +#import "FSTTypes.h" + +@class FSTDatabaseInfo; +@class FSTDocumentKey; +@class FSTDispatchQueue; +@class FSTMutation; +@class FSTMutationResult; +@class FSTQueryData; +@class FSTSnapshotVersion; +@class FSTWatchChange; +@class FSTWatchStream; +@class FSTWriteStream; +@class GRPCCall; +@class GRXWriter; + +@protocol FSTCredentialsProvider; +@protocol FSTWatchStreamDelegate; +@protocol FSTWriteStreamDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTDatastore represents a proxy for the remote server, hiding details of the RPC layer. It: + * + * - Manages connections to the server + * - Authenticates to the server + * - Manages threading and keeps higher-level code running on the worker queue + * - Serializes internal model objects to and from protocol buffers + * + * The FSTDatastore is generally not responsible for understanding the higher-level protocol + * involved in actually making changes or reading data, and aside from the connections it manages + * is otherwise stateless. + */ +@interface FSTDatastore : NSObject + +/** Creates a new Datastore instance with the given database info. */ ++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials; + +- (instancetype)init __attribute__((unavailable("Use a static constructor method."))); + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + NS_DESIGNATED_INITIALIZER; + +/** Converts the error to a FIRFirestoreErrorDomain error. */ ++ (NSError *)firestoreErrorForError:(NSError *)error; + +/** Returns YES if the given error indicates the RPC associated with it may not be retried. */ ++ (BOOL)isPermanentWriteError:(NSError *)error; + +/** Returns YES if the given error is a GRPC ABORTED error. **/ ++ (BOOL)isAbortedError:(NSError *)error; + +/** Looks up a list of documents in datastore. */ +- (void)lookupDocuments:(NSArray *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion; + +/** Commits data to datastore. */ +- (void)commitMutations:(NSArray *)mutations + completion:(FSTVoidErrorBlock)completion; + +/** Creates a new watch stream. */ +- (FSTWatchStream *)createWatchStreamWithDelegate:(id)delegate; + +/** Creates a new write stream. */ +- (FSTWriteStream *)createWriteStreamWithDelegate:(id)delegate; + +/** The name of the database and the backend. */ +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; + +@end + +/** + * An FSTStream is an abstract base class that represents a restartable streaming RPC to the + * Firestore backend. It's built on top of GRPC's own support for streaming RPCs, and adds several + * critical features for our clients: + * + * - Restarting a stream is allowed (after failure) + * - Exponential backoff on failure (independent of the underlying channel) + * - Authentication via FSTCredentialsProvider + * - Dispatching all callbacks into the shared worker queue + * + * Subclasses of FSTStream implement serialization of models to and from bytes (via protocol + * buffers) for a specific streaming RPC and emit events specific to the stream. + * + * ## Starting and Stopping + * + * Streaming RPCs are stateful and need to be started before messages can be sent and received. + * The FSTStream will call its delegate's specific streamDidOpen method once the stream is ready + * to accept requests. + * + * Should a `start` fail, FSTStream will call its delegate's specific streamDidClose method with an + * NSError indicating what went wrong. The delegate is free to call start again. + * + * An FSTStream can also be explicitly stopped which indicates that the caller has discarded the + * stream and no further events should be emitted. Once explicitly stopped, a stream cannot be + * restarted. + * + * ## Subclassing Notes + * + * An implementation of FSTStream needs to implement the following methods: + * - `createRPCWithRequestsWriter`, should create the specific RPC (a GRPCCall object). + * - `handleStreamOpen`, should call through to the stream-specific streamDidOpen method. + * - `handleStreamMessage`, receives protocol buffer responses from GRPC and must deserialize and + * delegate to some stream specific response method. + * - `handleStreamClose`, calls through to the stream-specific streamDidClose method. + * + * Additionally, beyond these required methods, subclasses will want to implement methods that + * take request models, serialize them, and write them to using writeRequest:. + * + * ## RPC Message Type + * + * FSTStream intentionally uses the GRPCCall interface to GRPC directly, bypassing both GRPCProtoRPC + * and GRXBufferedPipe for sending data. This has been done to avoid race conditions that come out + * of a loosely specified locking contract on GRXWriter. There's essentially no way to safely use + * any of the wrapper objects for GRXWriter (that perform buffering or conversion to/from protos). + * + * See https://github.com/grpc/grpc/issues/10957 for the kinds of things we're trying to avoid. + */ +@interface FSTStream : NSObject + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * An abstract method used by `start` to create a streaming RPC specific to this type of stream. + * The RPC should be created such that requests are taken from `self`. + * + * Note that the returned GRPCCall must not be a GRPCProtoRPC, since the rest of the streaming + * mechanism assumes it is dealing in bytes-level requests and responses. + */ +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter; + +/** + * Returns YES if `start` has been called and no error has occurred. YES indicates the stream is + * open or in the process of opening (which encompasses respecting backoff, getting auth tokens, + * and starting the actual RPC). Use `isOpen` to determine if the stream is open and ready for + * outbound requests. + */ +- (BOOL)isStarted; + +/** Returns YES if the underlying RPC is open and the stream is ready for outbound requests. */ +- (BOOL)isOpen; + +/** + * Starts the RPC. Only allowed if isStarted returns NO. The stream is not immediately ready for + * use: the delegate's watchStreamDidOpen method will be invoked when the RPC is ready for outbound + * requests, at which point `isOpen` will return YES. + * + * When start returns, -isStarted will return YES. + */ +- (void)start; + +/** + * Stops the RPC. This call is idempotent and allowed regardless of the current isStarted state. + * + * Unlike a transient stream close, stopping a stream is permanent. This is guaranteed NOT to emit + * any further events on the stream-specific delegate, including the streamDidClose method. + * + * NOTE: This no-events contract may seem counter-intuitive but allows the caller to + * straightforwardly sequence stream tear-down without having to worry about when the delegate's + * streamDidClose methods will get called. For example if the stream must be exchanged for another + * during a user change this allows `stop` to be called eagerly without worrying about the + * streamDidClose method accidentally restarting the stream before the new one is ready. + * + * When stop returns, -isStarted and -isOpen will both return NO. + */ +- (void)stop; + +/** + * After an error the stream will usually back off on the next attempt to start it. If the error + * warrants an immediate restart of the stream, the sender can use this to indicate that the + * receiver should not back off. + * + * Each error will call the stream-specific streamDidClose method. That method can decide to + * inhibit backoff if required. + */ +- (void)inhibitBackoff; + +@end + +#pragma mark - FSTWatchStream + +/** A protocol defining the events that can be emitted by the FSTWatchStream. */ +@protocol FSTWatchStreamDelegate + +/** Called by the FSTWatchStream when it is ready to accept outbound request messages. */ +- (void)watchStreamDidOpen; + +/** + * Called by the FSTWatchStream with changes and the snapshot versions included in in the + * WatchChange responses sent back by the server. + */ +- (void)watchStreamDidChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion; + +/** + * Called by the FSTWatchStream when the underlying streaming RPC is closed for whatever reason, + * usually because of an error, but possibly due to an idle timeout. The error passed to this + * method may be nil, in which case the stream was closed without attributable fault. + * + * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" + * on FSTStream for details. + */ +- (void)watchStreamDidClose:(NSError *_Nullable)error; + +@end + +/** + * An FSTStream that implements the StreamingWatch RPC. + * + * Once the FSTWatchStream has called the streamDidOpen method, any number of watchQuery and + * unwatchTargetId calls can be sent to control what changes will be sent from the server for + * WatchChanges. + */ +@interface FSTWatchStream : FSTStream + +/** + * Initializes the watch stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Registers interest in the results of the given query. If the query includes a resumeToken it + * will be included in the request. Results that affect the query will be streamed back as + * WatchChange messages that reference the targetID included in |query|. + */ +- (void)watchQuery:(FSTQueryData *)query; + +/** Unregisters interest in the results of the query associated with the given target ID. */ +- (void)unwatchTargetID:(FSTTargetID)targetID; + +@property(nonatomic, weak, readonly) id delegate; + +@end + +#pragma mark - FSTWriteStream + +@protocol FSTWriteStreamDelegate + +/** Called by the FSTWriteStream when it is ready to accept outbound request messages. */ +- (void)writeStreamDidOpen; + +/** + * Called by the FSTWriteStream upon a successful handshake response from the server, which is the + * receiver's cue to send any pending writes. + */ +- (void)writeStreamDidCompleteHandshake; + +/** + * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that + * contains mutation results. + */ +- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results; + +/** + * Called when the FSTWriteStream's underlying RPC is closed for whatever reason, usually because + * of an error, but possibly due to an idle timeout. The error passed to this method may be nil, in + * which case the stream was closed without attributable fault. + * + * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" + * on FSTStream for details. + */ +- (void)writeStreamDidClose:(NSError *_Nullable)error; + +@end + +/** + * An FSTStream that implements the StreamingWrite RPC. + * + * The StreamingWrite RPC requires the caller to maintain special `streamToken` state in between + * calls, to help the server understand which responses the client has processed by the time the + * next request is made. Every response may contain a `streamToken`; this value must be passed to + * the next request. + * + * After calling `start` on this stream, the next request must be a handshake, containing whatever + * streamToken is on hand. Once a response to this request is received, all pending mutations may + * be submitted. When submitting multiple batches of mutations at the same time, it's okay to use + * the same streamToken for the calls to `writeMutations:`. + */ +@interface FSTWriteStream : FSTStream + +/** + * Initializes the write stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Sends an initial streamToken to the server, performing the handshake required to make the + * StreamingWrite RPC work. Subsequent `writeMutations:` calls should wait until a response has + * been delivered to the delegate's writeStreamDidCompleteHandshake method. + */ +- (void)writeHandshake; + +/** Sends a group of mutations to the Firestore backend to apply. */ +- (void)writeMutations:(NSArray *)mutations; + +@property(nonatomic, weak, readonly) id delegate; + +/** + * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to + * accept mutations. + */ +@property(nonatomic, assign, readwrite, getter=isHandshakeComplete) BOOL handshakeComplete; + +/** + * The last received stream token from the server, used to acknowledge which responses the client + * has processed. Stream tokens are opaque checkpoint markers whose only real value is their + * inclusion in the next request. + * + * FSTWriteStream manages propagating this value from responses to the next request. + */ +@property(nonatomic, strong, nullable) NSData *lastStreamToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTDatastore.m b/Firestore/Source/Remote/FSTDatastore.m new file mode 100644 index 0000000..3ed2729 --- /dev/null +++ b/Firestore/Source/Remote/FSTDatastore.m @@ -0,0 +1,1027 @@ +/* + * 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 "FSTDatastore.h" + +#import +#import +#import + +#import "FIRFirestore+Internal.h" +#import "FIRFirestoreErrors.h" +#import "FIRFirestoreVersion.h" +#import "FSTAssert.h" +#import "FSTBufferedWriter.h" +#import "FSTClasses.h" +#import "FSTCredentialsProvider.h" +#import "FSTDatabaseID.h" +#import "FSTDatabaseInfo.h" +#import "FSTDispatchQueue.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExponentialBackoff.h" +#import "FSTLocalStore.h" +#import "FSTLogger.h" +#import "FSTMutation.h" +#import "FSTQueryData.h" +#import "FSTSerializerBeta.h" + +#import "Firestore.pbrpc.h" + +NS_ASSUME_NONNULL_BEGIN + +// GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore +// directly exposes an sslEnabled setting so this is required to plumb that through. Note that our +// own tests depend on this working so we'll know if this changes upstream. +@interface GRPCHost ++ (nullable instancetype)hostWithAddress:(NSString *)address; +@property(nonatomic, getter=isSecure) BOOL secure; +@end + +/** + * Initial backoff time in seconds after an error. + * Set to 1s according to https://cloud.google.com/apis/design/errors. + */ +static const NSTimeInterval kBackoffInitialDelay = 1; +static const NSTimeInterval kBackoffMaxDelay = 60.0; +static const double kBackoffFactor = 1.5; +static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client"; +static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix"; + +/** Function typedef used to create RPCs. */ +typedef GRPCProtoCall * (^RPCFactory)(); + +#pragma mark - FSTStream + +/** The state of a stream. */ +typedef NS_ENUM(NSInteger, FSTStreamState) { + /** + * The streaming RPC is not running and there's no error condition. Calling `start` will + * start the stream immediately without backoff. While in this state -isStarted will return NO. + */ + FSTStreamStateInitial = 0, + + /** + * The stream is starting, and is waiting for an auth token to attach to the initial request. + * While in this state, isStarted will return YES but isOpen will return NO. + */ + FSTStreamStateAuth, + + /** + * The streaming RPC is up and running. Requests and responses can flow freely. Both + * isStarted and isOpen will return YES. + */ + FSTStreamStateOpen, + + /** + * The stream encountered an error. The next start attempt will back off. While in this state + * -isStarted will return NO. + */ + FSTStreamStateError, + + /** + * An in-between state after an error where the stream is waiting before re-starting. After + * waiting is complete, the stream will try to open. While in this state -isStarted will + * return YES but isOpen will return NO. + */ + FSTStreamStateBackoff, + + /** + * The stream has been explicitly stopped; no further events will be emitted. + */ + FSTStreamStateStopped, +}; + +// We need to declare these classes first so that Datastore can alloc them. + +@interface FSTWatchStream () + +/** The delegate that will receive events generated by the watch stream. */ +@property(nonatomic, weak, nullable) id delegate; + +@end + +@interface FSTBetaWatchStream : FSTWatchStream + +/** + * Initializes the watch stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate NS_UNAVAILABLE; + +@end + +@interface FSTWriteStream () + +@property(nonatomic, weak, nullable) id delegate; + +@end + +@interface FSTBetaWriteStream : FSTWriteStream + +/** + * Initializes the write stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate NS_UNAVAILABLE; + +@end + +@interface FSTStream () + +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; +@property(nonatomic, strong, readonly) id credentials; +@property(nonatomic, unsafe_unretained, readonly) Class responseMessageClass; +@property(nonatomic, strong, readonly) FSTExponentialBackoff *backoff; + +/** A flag tracking whether the stream received a message from the backend. */ +@property(nonatomic, assign) BOOL messageReceived; + +/** + * Stream state as exposed to consumers of FSTStream. This differs from GRXWriter's notion of the + * state of the stream. + */ +@property(nonatomic, assign) FSTStreamState state; + +/** The RPC handle. Used for cancellation. */ +@property(nonatomic, strong, nullable) GRPCCall *rpc; + +/** + * The send-side of the RPC stream in which to submit requests, but only once the underlying RPC has + * started. + */ +@property(nonatomic, strong, nullable) FSTBufferedWriter *requestsWriter; + +@end + +#pragma mark - FSTDatastore + +@interface FSTDatastore () + +/** The GRPC service for Firestore. */ +@property(nonatomic, strong, readonly) GCFSFirestore *service; + +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; + +/** An object for getting an auth token before each request. */ +@property(nonatomic, strong, readonly) id credentials; + +@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; + +@end + +@implementation FSTDatastore + ++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials { + return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials]; +} + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials { + if (self = [super init]) { + _databaseInfo = databaseInfo; + if (!databaseInfo.isSSLEnabled) { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:databaseInfo.host]; + hostConfig.secure = NO; + } + _service = [GCFSFirestore serviceWithHost:databaseInfo.host]; + _workerDispatchQueue = workerDispatchQueue; + _credentials = credentials; + _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseInfo.databaseID]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", self.databaseInfo]; +} + +/** + * Converts the error to an error within the domain FIRFirestoreErrorDomain. + */ ++ (NSError *)firestoreErrorForError:(NSError *)error { + if (!error) { + return error; + } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) { + return error; + } else if ([error.domain isEqualToString:kGRPCErrorDomain]) { + FSTAssert(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated, + @"Unknown GRPC error code: %ld", (long)error.code); + return + [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo]; + } else { + return [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnknown + userInfo:@{NSUnderlyingErrorKey : error}]; + } +} + ++ (BOOL)isAbortedError:(NSError *)error { + FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], + @"isAbortedError: only works with errors emitted by FSTDatastore."); + return error.code == FIRFirestoreErrorCodeAborted; +} + ++ (BOOL)isPermanentWriteError:(NSError *)error { + FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], + @"isPerminanteWriteError: only works with errors emitted by FSTDatastore."); + switch (error.code) { + case FIRFirestoreErrorCodeCancelled: + case FIRFirestoreErrorCodeUnknown: + case FIRFirestoreErrorCodeDeadlineExceeded: + case FIRFirestoreErrorCodeResourceExhausted: + case FIRFirestoreErrorCodeInternal: + case FIRFirestoreErrorCodeUnavailable: + case FIRFirestoreErrorCodeUnauthenticated: + // Unauthenticated means something went wrong with our token and we need + // to retry with new credentials which will happen automatically. + // TODO(b/37325376): Give up after second unauthenticated error. + return NO; + case FIRFirestoreErrorCodeInvalidArgument: + case FIRFirestoreErrorCodeNotFound: + case FIRFirestoreErrorCodeAlreadyExists: + case FIRFirestoreErrorCodePermissionDenied: + case FIRFirestoreErrorCodeFailedPrecondition: + case FIRFirestoreErrorCodeAborted: + // Aborted might be retried in some scenarios, but that is dependant on + // the context and should handled individually by the calling code. + // See https://cloud.google.com/apis/design/errors + case FIRFirestoreErrorCodeOutOfRange: + case FIRFirestoreErrorCodeUnimplemented: + case FIRFirestoreErrorCodeDataLoss: + default: + return YES; + } +} + +/** Returns the string to be used as x-goog-api-client header value. */ ++ (NSString *)googAPIClientHeaderValue { + // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the + // version as a macro, so it would be hardcoded based on version we have at compile time of + // the Firestore library, rather than the version available at runtime/at compile time by the + // user of the library. + return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FirebaseFirestoreVersionString]; +} + +/** Returns the string to be used as google-cloud-resource-prefix header value. */ ++ (NSString *)googleCloudResourcePrefixForDatabaseID:(FSTDatabaseID *)databaseID { + return [NSString + stringWithFormat:@"projects/%@/databases/%@", databaseID.projectID, databaseID.databaseID]; +} +/** + * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers + * (for logging purposes). + */ ++ (NSDictionary *)extractWhiteListedHeaders: + (NSDictionary *)headers { + NSMutableDictionary *whiteListedHeaders = + [NSMutableDictionary dictionary]; + NSArray *whiteList = @[ + @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service", + @"x-google-gfe-request-trace" + ]; + [headers + enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { + if ([whiteList containsObject:[headerName lowercaseString]]) { + whiteListedHeaders[headerName] = headerValue; + } + }]; + return whiteListedHeaders; +} + +/** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */ ++ (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName { + if ([FIRFirestore isLoggingEnabled]) { + FSTLog(@"RPC %@ returned headers (whitelisted): %@", rpcName, + [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]); + } +} + +- (void)commitMutations:(NSArray *)mutations + completion:(FSTVoidErrorBlock)completion { + GCFSCommitRequest *request = [GCFSCommitRequest message]; + request.database = [self.serializer encodedDatabaseID]; + + NSMutableArray *mutationProtos = [NSMutableArray array]; + for (FSTMutation *mutation in mutations) { + [mutationProtos addObject:[self.serializer encodedMutation:mutation]]; + } + request.writesArray = mutationProtos; + + RPCFactory rpcFactory = ^GRPCProtoCall * { + __block GRPCProtoCall *rpc = [self.service + RPCToCommitWithRequest:request + handler:^(GCFSCommitResponse *response, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsync:^{ + FSTLog(@"RPC CommitRequest completed. Error: %@", error); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"]; + completion(error); + }]; + }]; + return rpc; + }; + + [self invokeRPCWithFactory:rpcFactory errorHandler:completion]; +} + +- (void)lookupDocuments:(NSArray *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { + GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message]; + request.database = [self.serializer encodedDatabaseID]; + for (FSTDocumentKey *key in keys) { + [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]]; + } + + __block FSTMaybeDocumentDictionary *results = + [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + + RPCFactory rpcFactory = ^GRPCProtoCall * { + __block GRPCProtoCall *rpc = [self.service + RPCToBatchGetDocumentsWithRequest:request + eventHandler:^(BOOL done, + GCFSBatchGetDocumentsResponse *_Nullable response, + NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsync:^{ + if (error) { + FSTLog(@"RPC BatchGetDocuments completed. Error: %@", error); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; + completion(nil, error); + return; + } + + if (!done) { + // Streaming response, accumulate result + FSTMaybeDocument *doc = + [self.serializer decodedMaybeDocumentFromBatch:response]; + results = [results dictionaryBySettingObject:doc forKey:doc.key]; + } else { + // Streaming response is done, call completion + FSTLog(@"RPC BatchGetDocuments completed successfully."); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; + FSTAssert(!response, @"Got response after done."); + NSMutableArray *docs = + [NSMutableArray arrayWithCapacity:keys.count]; + for (FSTDocumentKey *key in keys) { + [docs addObject:results[key]]; + } + completion(docs, nil); + } + }]; + }]; + return rpc; + }; + + [self invokeRPCWithFactory:rpcFactory + errorHandler:^(NSError *_Nonnull error) { + error = [FSTDatastore firestoreErrorForError:error]; + completion(nil, error); + }]; +} + +- (void)invokeRPCWithFactory:(GRPCProtoCall * (^)())rpcFactory + errorHandler:(FSTVoidErrorBlock)errorHandler { + // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, + // but I'm not sure how to detect that right now. http://b/32762461 + [self.credentials + getTokenForcingRefresh:NO + completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ + if (error) { + errorHandler(error); + } else { + GRPCProtoCall *rpc = rpcFactory(); + [FSTDatastore prepareHeadersForRPC:rpc + databaseID:self.databaseInfo.databaseID + token:result.token]; + [rpc start]; + } + }]; + }]; +} + +- (FSTWatchStream *)createWatchStreamWithDelegate:(id)delegate { + return [[FSTBetaWatchStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + serializer:_serializer + delegate:delegate]; +} + +- (FSTWriteStream *)createWriteStreamWithDelegate:(id)delegate { + return [[FSTBetaWriteStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + serializer:_serializer + delegate:delegate]; +} + +/** Adds headers to the RPC including any OAuth access token if provided .*/ ++ (void)prepareHeadersForRPC:(GRPCCall *)rpc + databaseID:(FSTDatabaseID *)databaseID + token:(nullable NSString *)token { + rpc.oauth2AccessToken = token; + rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue]; + // This header is used to improve routing and project isolation by the backend. + rpc.requestHeaders[kGoogleCloudResourcePrefix] = + [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID]; +} + +@end + +#pragma mark - FSTStream + +@implementation FSTStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass { + if (self = [super init]) { + _databaseInfo = database; + _workerDispatchQueue = workerDispatchQueue; + _credentials = credentials; + _responseMessageClass = responseMessageClass; + + _backoff = [FSTExponentialBackoff exponentialBackoffWithDispatchQueue:workerDispatchQueue + initialDelay:kBackoffInitialDelay + backoffFactor:kBackoffFactor + maxDelay:kBackoffMaxDelay]; + _state = FSTStreamStateInitial; + } + return self; +} + +- (BOOL)isStarted { + [self.workerDispatchQueue verifyIsCurrentQueue]; + FSTStreamState state = self.state; + return state == FSTStreamStateBackoff || state == FSTStreamStateAuth || + state == FSTStreamStateOpen; +} + +- (BOOL)isOpen { + [self.workerDispatchQueue verifyIsCurrentQueue]; + return self.state == FSTStreamStateOpen; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)start { + [self.workerDispatchQueue verifyIsCurrentQueue]; + + if (self.state == FSTStreamStateError) { + [self performBackoff]; + return; + } + + FSTLog(@"%@ %p start", NSStringFromClass([self class]), (__bridge void *)self); + FSTAssert(self.state == FSTStreamStateInitial, @"Already started"); + + self.state = FSTStreamStateAuth; + + [self.credentials + getTokenForcingRefresh:NO + completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ + [self resumeStartWithToken:result error:error]; + }]; + }]; +} + +/** Add an access token to our RPC, after obtaining one from the credentials provider. */ +- (void)resumeStartWithToken:(FSTGetTokenResult *)token error:(NSError *)error { + if (self.state == FSTStreamStateStopped) { + // Streams can be stopped while waiting for authorization. + return; + } + + [self.workerDispatchQueue verifyIsCurrentQueue]; + FSTAssert(self.state == FSTStreamStateAuth, @"State should still be auth (was %ld)", + (long)self.state); + + // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, + // but I'm not sure how to detect that right now. http://b/32762461 + if (error) { + // RPC has not been started yet, so just invoke higher-level close handler. + [self handleStreamClose:error]; + return; + } + + self.requestsWriter = [[FSTBufferedWriter alloc] init]; + _rpc = [self createRPCWithRequestsWriter:self.requestsWriter]; + [FSTDatastore prepareHeadersForRPC:_rpc + databaseID:self.databaseInfo.databaseID + token:token.token]; + [_rpc startWithWriteable:self]; + + self.state = FSTStreamStateOpen; + [self handleStreamOpen]; +} + +/** Backs off after an error. */ +- (void)performBackoff { + FSTLog(@"%@ %p backoff", NSStringFromClass([self class]), (__bridge void *)self); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + FSTAssert(self.state == FSTStreamStateError, @"Should only perform backoff in an error case"); + self.state = FSTStreamStateBackoff; + + FSTWeakify(self); + [self.backoff backoffAndRunBlock:^{ + FSTStrongify(self); + [self resumeStartFromBackoff]; + }]; +} + +/** Resumes stream start after backing off. */ +- (void)resumeStartFromBackoff { + if (self.state == FSTStreamStateStopped) { + // Streams can be stopped while waiting for backoff to complete. + return; + } + + // In order to have performed a backoff the stream must have been in an error state just prior + // to entering the backoff state. If we weren't stopped we must be in the backoff state. + FSTAssert(self.state == FSTStreamStateBackoff, @"State should still be backoff (was %ld)", + (long)self.state); + + // Momentarily set state to FSTStreamStateInitial as `start` expects it. + self.state = FSTStreamStateInitial; + [self start]; + FSTAssert([self isStarted], @"Stream should have started."); +} + +- (void)stop { + FSTLog(@"%@ %p stop", NSStringFromClass([self class]), (__bridge void *)self); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Prevent any possible future restart of this stream. + self.state = FSTStreamStateStopped; + + // Close the stream client side. + FSTBufferedWriter *requestsWriter = self.requestsWriter; + @synchronized(requestsWriter) { + [requestsWriter finishWithError:nil]; + } +} + +- (void)inhibitBackoff { + FSTAssert(![self isStarted], @"Can only inhibit backoff after an error (was %ld)", + (long)self.state); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Clear the error condition. + self.state = FSTStreamStateInitial; + [self.backoff reset]; +} + +/** + * Parses a protocol buffer response from the server. If the message fails to parse, generates + * an error and closes the stream. + * + * @param protoClass A protocol buffer message class object, that responds to parseFromData:error:. + * @param data The bytes in the response as returned from GRPC. + * @return An instance of the protocol buffer message, parsed from the data if parsing was + * successful, or nil otherwise. + */ +- (nullable id)parseProto:(Class)protoClass data:(NSData *)data error:(NSError **)error { + NSError *parseError; + id parsed = [protoClass parseFromData:data error:&parseError]; + if (parsed) { + *error = nil; + return parsed; + } else { + NSDictionary *info = @{ + NSLocalizedDescriptionKey : @"Unable to parse response from the server", + NSUnderlyingErrorKey : parseError, + @"Expected class" : protoClass, + @"Received value" : data, + }; + *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:info]; + return nil; + } +} + +/** + * Writes a request proto into the stream. + */ +- (void)writeRequest:(GPBMessage *)request { + NSData *data = [request data]; + + FSTBufferedWriter *requestsWriter = self.requestsWriter; + @synchronized(requestsWriter) { + [requestsWriter writeValue:data]; + } +} + +#pragma mark Template methods for subclasses + +/** + * Called by the stream after the stream has been successfully connected, authenticated, and is now + * ready to accept messages. + * + * Subclasses should relay to their stream-specific delegate. Calling [super handleStreamOpen] is + * not required. + */ +- (void)handleStreamOpen { +} + +/** + * Called by the stream for each incoming protocol message coming from the server. + * + * Subclasses should implement this to deserialize the value and relay to their stream-specific + * delegate, if appropriate. Calling [super handleStreamMessage] is not required. + */ +- (void)handleStreamMessage:(id)value { +} + +/** + * Called by the stream when the underlying RPC has been closed for whatever reason. + * + * Subclasses should first call [super handleStreamClose:] and then call to their + * stream-specific delegate. + */ +- (void)handleStreamClose:(NSError *_Nullable)error { + FSTLog(@"%@ %p close: %@", NSStringFromClass([self class]), (__bridge void *)self, error); + FSTAssert([self isStarted], @"Can't handle server close in non-started state."); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + self.messageReceived = NO; + self.rpc = nil; + self.requestsWriter = nil; + + // In theory the stream could close cleanly, however, in our current model we never expect this + // to happen because if we stop a stream ourselves, this callback will never be called. To + // prevent cases where we retry without a backoff accidentally, we set the stream to error + // in all cases. + self.state = FSTStreamStateError; + + if (error.code == FIRFirestoreErrorCodeResourceExhausted) { + FSTLog(@"%@ %p Using maximum backoff delay to prevent overloading the backend.", [self class], + (__bridge void *)self); + [self.backoff resetToMax]; + } +} + +#pragma mark GRXWriteable implementation +// The GRXWriteable implementation defines the receive side of the RPC stream. + +/** + * Called by GRPC when it publishes a value. It is called from GRPC's own queue so we immediately + * redispatch back onto our own worker queue. + */ +- (void)writeValue:(id)value __used { + // TODO(mcg): remove the double-dispatch once GRPCCall at head is released. + // Once released we can set the responseDispatchQueue property on the GRPCCall and then this + // method can call handleStreamMessage directly. + FSTWeakify(self); + [self.workerDispatchQueue dispatchAsync:^{ + FSTStrongify(self); + if (!self || self.state == FSTStreamStateStopped) { + return; + } + if (!self.messageReceived) { + self.messageReceived = YES; + if ([FIRFirestore isLoggingEnabled]) { + FSTLog(@"%@ %p headers (whitelisted): %@", NSStringFromClass([self class]), + (__bridge void *)self, + [FSTDatastore extractWhiteListedHeaders:self.rpc.responseHeaders]); + } + } + NSError *error; + id proto = [self parseProto:self.responseMessageClass data:value error:&error]; + if (proto) { + [self handleStreamMessage:proto]; + } else { + [_rpc finishWithError:error]; + } + }]; +} + +/** + * Called by GRPC when it closed the stream with an error representing the final state of the + * stream. + * + * Do not call directly, since it dispatches via the worker queue. Call handleStreamClose to + * directly inform stream-specific logic, or call stop to tear down the stream. + */ +- (void)writesFinishedWithError:(NSError *_Nullable)error __used { + error = [FSTDatastore firestoreErrorForError:error]; + FSTWeakify(self); + [self.workerDispatchQueue dispatchAsync:^{ + FSTStrongify(self); + if (!self || self.state == FSTStreamStateStopped) { + return; + } + [self handleStreamClose:error]; + }]; +} + +@end + +#pragma mark - FSTWatchStream + +@implementation FSTWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:responseMessageClass]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (void)stop { + // Clear the delegate to avoid any possible bleed through of events from GRPC. + self.delegate = nil; + + [super stop]; +} + +- (void)watchQuery:(FSTQueryData *)query { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)handleStreamOpen { + [self.delegate watchStreamDidOpen]; +} + +- (void)handleStreamClose:(NSError *_Nullable)error { + [super handleStreamClose:error]; + [self.delegate watchStreamDidClose:error]; +} + +@end + +#pragma mark - FSTBetaWatchStream + +@implementation FSTBetaWatchStream { + FSTSerializerBeta *_serializer; +} + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[GCFSListenResponse class] + delegate:delegate]; + if (self) { + _serializer = serializer; + } + return self; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + return [[GRPCCall alloc] initWithHost:self.databaseInfo.host + path:@"/google.firestore.v1beta1.Firestore/Listen" + requestsWriter:requestsWriter]; +} + +- (void)watchQuery:(FSTQueryData *)query { + FSTAssert([self isOpen], @"Not yet open"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSListenRequest *request = [GCFSListenRequest message]; + request.database = [_serializer encodedDatabaseID]; + request.addTarget = [_serializer encodedTarget:query]; + request.labels = [_serializer encodedListenRequestLabelsForQueryData:query]; + + FSTLog(@"FSTWatchStream %p watch: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + FSTAssert([self isOpen], @"Not yet open"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSListenRequest *request = [GCFSListenRequest message]; + request.database = [_serializer encodedDatabaseID]; + request.removeTarget = targetID; + + FSTLog(@"FSTWatchStream %p unwatch: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +/** + * Receives an inbound message from GRPC, deserializes, and then passes that on to the delegate's + * watchStreamDidChange:snapshotVersion: callback. + */ +- (void)handleStreamMessage:(GCFSListenResponse *)proto { + FSTLog(@"FSTWatchStream %p response: %@", (__bridge void *)self, proto); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // A successful response means the stream is healthy. + [self.backoff reset]; + + FSTWatchChange *change = [_serializer decodedWatchChange:proto]; + FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto]; + [self.delegate watchStreamDidChange:change snapshotVersion:snap]; +} + +@end + +#pragma mark - FSTWriteStream + +@implementation FSTWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:responseMessageClass]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (void)start { + self.handshakeComplete = NO; + [super start]; +} + +- (void)stop { + // Clear the delegate to avoid any possible bleed through of events from GRPC. + self.delegate = nil; + + [super stop]; +} + +- (void)writeHandshake { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)writeMutations:(NSArray *)mutations { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)handleStreamOpen { + [self.delegate writeStreamDidOpen]; +} + +- (void)handleStreamClose:(NSError *_Nullable)error { + [super handleStreamClose:error]; + + [self.delegate writeStreamDidClose:error]; +} + +@end + +#pragma mark - FSTBetaWriteStream + +@implementation FSTBetaWriteStream { + FSTSerializerBeta *_serializer; +} + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]; + if (self) { + _serializer = serializer; + } + return self; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + return [[GRPCCall alloc] initWithHost:self.databaseInfo.host + path:@"/google.firestore.v1beta1.Firestore/Write" + requestsWriter:requestsWriter]; +} + +- (void)writeHandshake { + // The initial request cannot contain mutations, but must contain a projectID. + FSTAssert([self isOpen], @"Not yet open"); + FSTAssert(!self.handshakeComplete, @"Handshake sent out of turn"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSWriteRequest *request = [GCFSWriteRequest message]; + request.database = [_serializer encodedDatabaseID]; + // TODO(dimond): Support stream resumption. We intentionally do not set the stream token on the + // handshake, ignoring any stream token we might have. + + FSTLog(@"FSTWriteStream %p initial request: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +- (void)writeMutations:(NSArray *)mutations { + FSTAssert([self isOpen], @"Not yet open"); + FSTAssert(self.handshakeComplete, @"Mutations sent out of turn"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + NSMutableArray *protos = [NSMutableArray arrayWithCapacity:mutations.count]; + for (FSTMutation *mutation in mutations) { + [protos addObject:[_serializer encodedMutation:mutation]]; + }; + + GCFSWriteRequest *request = [GCFSWriteRequest message]; + request.writesArray = protos; + request.streamToken = self.lastStreamToken; + + FSTLog(@"FSTWriteStream %p mutation request: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +/** + * Implements GRXWriteable to receive an inbound message from GRPC, deserialize, and then pass + * that on to the mutationResultsHandler. + */ +- (void)handleStreamMessage:(GCFSWriteResponse *)response { + FSTLog(@"FSTWriteStream %p response: %@", (__bridge void *)self, response); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // A successful response means the stream is healthy. + [self.backoff reset]; + + // Always capture the last stream token. + self.lastStreamToken = response.streamToken; + + if (!self.handshakeComplete) { + // The first response is the handshake response + self.handshakeComplete = YES; + + [self.delegate writeStreamDidCompleteHandshake]; + } else { + FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime]; + NSMutableArray *protos = response.writeResultsArray; + NSMutableArray *results = [NSMutableArray arrayWithCapacity:protos.count]; + for (GCFSWriteResult *proto in protos) { + [results addObject:[_serializer decodedMutationResult:proto]]; + }; + + [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExistenceFilter.h b/Firestore/Source/Remote/FSTExistenceFilter.h new file mode 100644 index 0000000..df95950 --- /dev/null +++ b/Firestore/Source/Remote/FSTExistenceFilter.h @@ -0,0 +1,31 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTExistenceFilter : NSObject + ++ (instancetype)filterWithCount:(int32_t)count; + +- (instancetype)init __attribute__((unavailable("Use a static constructor"))); + +@property(nonatomic, assign, readonly) int32_t count; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExistenceFilter.m b/Firestore/Source/Remote/FSTExistenceFilter.m new file mode 100644 index 0000000..7c0ded2 --- /dev/null +++ b/Firestore/Source/Remote/FSTExistenceFilter.m @@ -0,0 +1,53 @@ +/* + * 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 "FSTExistenceFilter.h" + +@interface FSTExistenceFilter () + +- (instancetype)initWithCount:(int32_t)count NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTExistenceFilter + ++ (instancetype)filterWithCount:(int32_t)count { + return [[FSTExistenceFilter alloc] initWithCount:count]; +} + +- (instancetype)initWithCount:(int32_t)count { + if (self = [super init]) { + _count = count; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTExistenceFilter class]]) { + return NO; + } + + return _count == ((FSTExistenceFilter *)other).count; +} + +- (NSUInteger)hash { + return _count; +} + +@end diff --git a/Firestore/Source/Remote/FSTExponentialBackoff.h b/Firestore/Source/Remote/FSTExponentialBackoff.h new file mode 100644 index 0000000..0bee2bd --- /dev/null +++ b/Firestore/Source/Remote/FSTExponentialBackoff.h @@ -0,0 +1,79 @@ +/* + * 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 + +@class FSTDispatchQueue; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Helper to implement exponential backoff. + * + * In general, call -reset after each successful round-trip. Call -backoffAndRunBlock before + * retrying after an error. Each backoffAndRunBlock will increase the delay between retries. + */ +@interface FSTExponentialBackoff : NSObject + +/** + * Creates and returns a helper for running delayed tasks following an exponential backoff curve + * between attempts. + * + * Each delay is made up of a "base" delay which follows the exponential backoff curve, and a + * +/- 50% "jitter" that is calculated and added to the base delay. This prevents clients from + * accidentally synchronizing their delays causing spikes of load to the backend. + * + * @param dispatchQueue The dispatch queue to run tasks on. + * @param initialDelay The initial delay (used as the base delay on the first retry attempt). + * Note that jitter will still be applied, so the actual delay could be as little as + * 0.5*initialDelay. + * @param backoffFactor The multiplier to use to determine the extended base delay after each + * attempt. + * @param maxDelay The maximum base delay after which no further backoff is performed. Note that + * jitter will still be applied, so the actual delay could be as much as 1.5*maxDelay. + */ ++ (instancetype)exponentialBackoffWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay; + +- (instancetype)init + __attribute__((unavailable("Use exponentialBackoffWithDispatchQueue constructor method."))); + +/** + * Resets the backoff delay. + * + * The very next backoffAndRunBlock: will have no delay. If it is called again (i.e. due to an + * error), initialDelay (plus jitter) will be used, and subsequent ones will increase according + * to the backoffFactor. + */ +- (void)reset; + +/** + * Resets the backoff to the maximum delay (e.g. for use after a RESOURCE_EXHAUSTED error). + */ +- (void)resetToMax; + +/** + * Waits for currentDelay seconds, increases the delay and runs the specified block. + * + * @param block The block to run. + */ +- (void)backoffAndRunBlock:(void (^)())block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExponentialBackoff.m b/Firestore/Source/Remote/FSTExponentialBackoff.m new file mode 100644 index 0000000..ec21282 --- /dev/null +++ b/Firestore/Source/Remote/FSTExponentialBackoff.m @@ -0,0 +1,97 @@ +/* + * 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 "FSTExponentialBackoff.h" + +#import "FSTDispatchQueue.h" +#import "FSTLogger.h" +#import "FSTUtil.h" + +@interface FSTExponentialBackoff () +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong) FSTDispatchQueue *dispatchQueue; +@property(nonatomic) double backoffFactor; +@property(nonatomic) NSTimeInterval initialDelay; +@property(nonatomic) NSTimeInterval maxDelay; +@property(nonatomic) NSTimeInterval currentBase; +@end + +@implementation FSTExponentialBackoff + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay { + if (self = [super init]) { + _dispatchQueue = dispatchQueue; + _initialDelay = initialDelay; + _backoffFactor = backoffFactor; + _maxDelay = maxDelay; + + [self reset]; + } + return self; +} + ++ (instancetype)exponentialBackoffWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay { + return [[FSTExponentialBackoff alloc] initWithDispatchQueue:dispatchQueue + initialDelay:initialDelay + backoffFactor:backoffFactor + maxDelay:maxDelay]; +} + +- (void)reset { + _currentBase = 0; +} + +- (void)resetToMax { + _currentBase = _maxDelay; +} + +- (void)backoffAndRunBlock:(void (^)())block { + // First schedule the block using the current base (which may be 0 and should be honored as such). + NSTimeInterval delayWithJitter = _currentBase + [self jitterDelay]; + if (_currentBase > 0) { + FSTLog(@"Backing off for %.2f seconds (base delay: %.2f seconds)", delayWithJitter, + _currentBase); + } + dispatch_time_t delay = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayWithJitter * NSEC_PER_SEC)); + dispatch_after(delay, self.dispatchQueue.queue, block); + + // Apply backoff factor to determine next delay and ensure it is within bounds. + _currentBase *= _backoffFactor; + if (_currentBase < _initialDelay) { + _currentBase = _initialDelay; + } + if (_currentBase > _maxDelay) { + _currentBase = _maxDelay; + } +} + +/** Returns a random value in the range [-currentBase/2, currentBase/2] */ +- (NSTimeInterval)jitterDelay { + return ([FSTUtil randomDouble] - 0.5) * _currentBase; +} + +@end diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h new file mode 100644 index 0000000..939a027 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteEvent.h @@ -0,0 +1,213 @@ +/* + * 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 + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" +#import "FSTTypes.h" + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTExistenceFilter; +@class FSTMaybeDocument; +@class FSTSnapshotVersion; +@class FSTWatchChange; +@class FSTQueryData; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetMapping + +/** + * TargetMapping represents a change to the documents in a query from the server. This can either + * be an incremental Update or a full Reset. + * + *

This is an empty abstract class so that all the different kinds of changes can have a common + * base class. + */ +@interface FSTTargetMapping : NSObject +@end + +#pragma mark - FSTResetMapping + +/** The new set of documents to replace the current documents for a target. */ +@interface FSTResetMapping : FSTTargetMapping + +/** + * Creates a new mapping with the keys for the given documents added. This is intended primarily + * for testing. + */ ++ (FSTResetMapping *)mappingWithDocuments:(NSArray *)documents; + +/** The new set of documents for the target. */ +@property(nonatomic, strong, readonly) FSTDocumentKeySet *documents; +@end + +#pragma mark - FSTUpdateMapping + +/** + * A target should update its set of documents with the given added/removed set of documents. + */ +@interface FSTUpdateMapping : FSTTargetMapping + +/** + * Creates a new mapping with the keys for the given documents added. This is intended primarily + * for testing. + */ ++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray *)added + removedDocuments:(NSArray *)removed; + +- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys; + +/** The documents added to the target. */ +@property(nonatomic, strong, readonly) FSTDocumentKeySet *addedDocuments; +/** The documents removed from the target. */ +@property(nonatomic, strong, readonly) FSTDocumentKeySet *removedDocuments; +@end + +#pragma mark - FSTTargetChange + +/** + * Represents an update to the current status of a target, either explicitly having no new state, or + * the new value to set. Note "current" has special meaning in the RPC protocol that implies that a + * target is both up-to-date and consistent with the rest of the watch stream. + */ +typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { + /** The current status is not affected and should not be modified */ + FSTCurrentStatusUpdateNone, + /** The target must be marked as no longer "current" */ + FSTCurrentStatusUpdateMarkNotCurrent, + /** The target must be marked as "current" */ + FSTCurrentStatusUpdateMarkCurrent, +}; + +/** + * A part of an FSTRemoteEvent specifying set of changes to a specific target. These changes track + * what documents are currently included in the target as well as the current snapshot version and + * resume token but the actual changes *to* documents are not part of the FSTTargetChange since + * documents may be part of multiple targets. + */ +@interface FSTTargetChange : NSObject + +/** + * Creates a new target change with the given documents. Instances of FSTDocument are considered + * added. Instance of FSTDeletedDocument are considered removed. This is intended primarily for + * testing. + */ ++ (instancetype)changeWithDocuments:(NSArray *)docs + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate; + +/** + * The new "current" (synced) status of this target. Set to CurrentStatusUpdateNone if the status + * should not be updated. Note "current" has special meaning for in the RPC protocol that implies + * that a target is both up-to-date and consistent with the rest of the watch stream. + */ +@property(nonatomic, assign, readonly) FSTCurrentStatusUpdate currentStatusUpdate; + +/** A set of changes to documents in this target. */ +@property(nonatomic, strong, readonly) FSTTargetMapping *mapping; + +/** + * The snapshot version representing the last state at which this target received a consistent + * snapshot from the backend. + */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** + * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting + * without retransmitting all the data that matches the query. The resume token essentially + * identifies a point in time from which the server should resume sending results. + */ +@property(nonatomic, strong, readonly) NSData *resumeToken; + +@end + +#pragma mark - FSTRemoteEvent + +/** + * An event from the RemoteStore. It is split into targetChanges (changes to the state or the set + * of documents in our watched targets) and documentUpdates (changes to the actual documents). + */ +@interface FSTRemoteEvent : NSObject + ++ (instancetype) +eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates; + +/** The snapshot version this event brings us up to. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** A map from target to changes to the target. See TargetChange. */ +@property(nonatomic, strong, readonly) + NSDictionary *targetChanges; + +/** + * A set of which documents have changed or been deleted, along with the doc's new values + * (if not deleted). + */ +@property(nonatomic, strong, readonly) + NSDictionary *documentUpdates; + +/** Adds a document update to this remote event */ +- (void)addDocumentUpdate:(FSTMaybeDocument *)document; + +/** Handles an existence filter mismatch */ +- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID; + +@end + +#pragma mark - FSTWatchChangeAggregator + +/** + * A helper class to accumulate watch changes into a FSTRemoteEvent and other target + * information. + */ +@interface FSTWatchChangeAggregator : NSObject + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + listenTargets:(NSDictionary *)listenTargets + pendingTargetResponses:(NSDictionary *)pendingTargetResponses + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The number of pending responses that are being waited on from watch */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *pendingTargetResponses; + +/** Aggregates a watch change into the current state */ +- (void)addWatchChange:(FSTWatchChange *)watchChange; + +/** Aggregates all provided watch changes to the current state in order */ +- (void)addWatchChanges:(NSArray *)watchChanges; + +/** + * Converts the current state into a remote event with the snapshot version taken from the + * initializer. + */ +- (FSTRemoteEvent *)remoteEvent; + +/** The existence filters - if any - for the given target IDs. */ +@property(nonatomic, strong, readonly) + NSDictionary *existenceFilters; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.m b/Firestore/Source/Remote/FSTRemoteEvent.m new file mode 100644 index 0000000..5c75998 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteEvent.m @@ -0,0 +1,516 @@ +/* + * 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 "FSTRemoteEvent.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTLogger.h" +#import "FSTSnapshotVersion.h" +#import "FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetMapping + +@interface FSTTargetMapping () + +/** Private mutator method to add a document key to the mapping */ +- (void)addDocumentKey:(FSTDocumentKey *)documentKey; + +/** Private mutator method to remove a document key from the mapping */ +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey; + +@end + +@implementation FSTTargetMapping + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +@end + +#pragma mark - FSTResetMapping + +@interface FSTResetMapping () +@property(nonatomic, strong) FSTDocumentKeySet *documents; +@end + +@implementation FSTResetMapping + ++ (instancetype)mappingWithDocuments:(NSArray *)documents { + FSTResetMapping *mapping = [[FSTResetMapping alloc] init]; + for (FSTDocument *doc in documents) { + mapping.documents = [mapping.documents setByAddingObject:doc.key]; + } + return mapping; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _documents = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTResetMapping class]]) { + return NO; + } + + FSTResetMapping *otherMapping = (FSTResetMapping *)other; + return [self.documents isEqual:otherMapping.documents]; +} + +- (NSUInteger)hash { + return self.documents.hash; +} + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + self.documents = [self.documents setByAddingObject:documentKey]; +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + self.documents = [self.documents setByRemovingObject:documentKey]; +} + +@end + +#pragma mark - FSTUpdateMapping + +@interface FSTUpdateMapping () +@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments; +@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments; +@end + +@implementation FSTUpdateMapping + ++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray *)added + removedDocuments:(NSArray *)removed { + FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + for (FSTDocument *doc in added) { + mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + } + for (FSTDocument *doc in removed) { + mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + } + return mapping; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _addedDocuments = [FSTDocumentKeySet keySet]; + _removedDocuments = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTUpdateMapping class]]) { + return NO; + } + + FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other; + return [self.addedDocuments isEqual:otherMapping.addedDocuments] && + [self.removedDocuments isEqual:otherMapping.removedDocuments]; +} + +- (NSUInteger)hash { + return self.addedDocuments.hash * 31 + self.removedDocuments.hash; +} + +- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys { + __block FSTDocumentKeySet *result = keys; + [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + result = [result setByAddingObject:key]; + }]; + [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + result = [result setByRemovingObject:key]; + }]; + return result; +} + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey]; + self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey]; +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey]; + self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey]; +} + +@end + +#pragma mark - FSTTargetChange + +@interface FSTTargetChange () +@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate; +@property(nonatomic, strong, nullable) FSTTargetMapping *mapping; +@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; +@property(nonatomic, strong) NSData *resumeToken; +@end + +@implementation FSTTargetChange + +- (instancetype)init { + if (self = [super init]) { + _currentStatusUpdate = FSTCurrentStatusUpdateNone; + _resumeToken = [NSData data]; + } + return self; +} + ++ (instancetype)changeWithDocuments:(NSArray *)docs + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { + FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + for (FSTMaybeDocument *doc in docs) { + if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + } else { + mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + } + } + FSTTargetChange *change = [[FSTTargetChange alloc] init]; + change.mapping = mapping; + change.currentStatusUpdate = currentStatusUpdate; + return change; +} + ++ (instancetype)changeWithMapping:(FSTTargetMapping *)mapping + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { + FSTTargetChange *change = [[FSTTargetChange alloc] init]; + change.mapping = mapping; + change.snapshotVersion = snapshotVersion; + change.currentStatusUpdate = currentStatusUpdate; + return change; +} + +- (FSTTargetMapping *)mapping { + if (!_mapping) { + // Create an FSTUpdateMapping by default, since resets are always explicit + _mapping = [[FSTUpdateMapping alloc] init]; + } + return _mapping; +} + +/** + * Sets the resume token but only when it has a new value. Empty resumeTokens are + * discarded. + */ +- (void)setResumeToken:(NSData *)resumeToken { + if (resumeToken.length > 0) { + _resumeToken = resumeToken; + } +} + +@end + +#pragma mark - FSTRemoteEvent + +@interface FSTRemoteEvent () { + NSMutableDictionary *_documentUpdates; + NSMutableDictionary *_targetChanges; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates; + +@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; + +@end + +@implementation FSTRemoteEvent + ++ (instancetype) +eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates { + return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion + targetChanges:targetChanges + documentUpdates:documentUpdates]; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary *)targetChanges + documentUpdates: + (NSMutableDictionary *)documentUpdates { + self = [super init]; + if (self) { + _snapshotVersion = snapshotVersion; + _targetChanges = targetChanges; + _documentUpdates = documentUpdates; + } + return self; +} + +/** Adds a document update to this remote event */ +- (void)addDocumentUpdate:(FSTMaybeDocument *)document { + _documentUpdates[document.key] = document; +} + +/** Handles an existence filter mismatch */ +- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID { + // An existence filter mismatch will reset the query and we need to reset the mapping to contain + // no documents and an empty resume token. + // + // Note: + // * The reset mapping is empty, specifically forcing the consumer of the change to + // forget all keys for this targetID; + // * The resume snapshot for this target must be reset + // * The target must be unacked because unwatching and rewatching introduces a race for + // changes. + // + // TODO(dimond): keep track of reset targets not to raise. + FSTTargetChange *targetChange = + [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init] + snapshotVersion:[FSTSnapshotVersion noVersion] + currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent]; + _targetChanges[targetID] = targetChange; +} + +@end + +#pragma mark - FSTWatchChangeAggregator + +@interface FSTWatchChangeAggregator () + +/** The snapshot version for every target change this creates. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** Keeps track of the current target mappings */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *targetChanges; + +/** Keeps track of document to update */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *documentUpdates; + +/** The set of open listens on the client */ +@property(nonatomic, strong, readonly) + NSDictionary *listenTargets; + +/** Whether this aggregator was frozen and can no longer be modified */ +@property(nonatomic, assign) BOOL frozen; + +@end + +@implementation FSTWatchChangeAggregator { + NSMutableDictionary *_existenceFilters; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + listenTargets:(NSDictionary *)listenTargets + pendingTargetResponses:(NSDictionary *)pendingTargetResponses { + self = [super init]; + if (self) { + _snapshotVersion = snapshotVersion; + + _frozen = NO; + _targetChanges = [NSMutableDictionary dictionary]; + _listenTargets = listenTargets; + _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses]; + + _existenceFilters = [NSMutableDictionary dictionary]; + _documentUpdates = [NSMutableDictionary dictionary]; + } + return self; +} + +- (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID { + FSTTargetChange *change = self.targetChanges[targetID]; + if (!change) { + change = [[FSTTargetChange alloc] init]; + change.snapshotVersion = self.snapshotVersion; + self.targetChanges[targetID] = change; + } + return change; +} + +- (void)addWatchChanges:(NSArray *)watchChanges { + FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); + for (FSTWatchChange *watchChange in watchChanges) { + [self addWatchChange:watchChange]; + } +} + +- (void)addWatchChange:(FSTWatchChange *)watchChange { + FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); + if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) { + [self addDocumentChange:(FSTDocumentWatchChange *)watchChange]; + } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) { + [self addTargetChange:(FSTWatchTargetChange *)watchChange]; + } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) { + [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange]; + } else { + FSTFail(@"Unknown watch change: %@", watchChange); + } +} + +- (void)addDocumentChange:(FSTDocumentWatchChange *)docChange { + BOOL relevant = NO; + + for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) { + if ([self isActiveTarget:targetID]) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + [change.mapping addDocumentKey:docChange.documentKey]; + relevant = YES; + } + } + + for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) { + if ([self isActiveTarget:targetID]) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + [change.mapping removeDocumentKey:docChange.documentKey]; + relevant = YES; + } + } + + // Only update the document if there is a new document to replace, this might be just a target + // update instead. + if (docChange.document && relevant) { + self.documentUpdates[docChange.documentKey] = docChange.document; + } +} + +- (void)addTargetChange:(FSTWatchTargetChange *)targetChange { + for (FSTBoxedTargetID *targetID in targetChange.targetIDs) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + switch (targetChange.state) { + case FSTWatchTargetChangeStateNoChange: + if ([self isActiveTarget:targetID]) { + // Creating the change above satisfies the semantics of no-change. + change.resumeToken = targetChange.resumeToken; + } + break; + case FSTWatchTargetChangeStateAdded: + [self recordResponseForTargetID:targetID]; + if (![self.pendingTargetResponses objectForKey:targetID]) { + // We have a freshly added target, so we need to reset any state that we had previously + // This can happen e.g. when remove and add back a target for existence filter + // mismatches. + change.mapping = nil; + change.currentStatusUpdate = FSTCurrentStatusUpdateNone; + [_existenceFilters removeObjectForKey:targetID]; + } + change.resumeToken = targetChange.resumeToken; + break; + case FSTWatchTargetChangeStateRemoved: + // We need to keep track of removed targets to we can post-filter and remove any target + // changes. + [self recordResponseForTargetID:targetID]; + FSTAssert(!targetChange.cause, @"WatchChangeAggregator does not handle errored targets."); + break; + case FSTWatchTargetChangeStateCurrent: + if ([self isActiveTarget:targetID]) { + change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent; + change.resumeToken = targetChange.resumeToken; + } + break; + case FSTWatchTargetChangeStateReset: + if ([self isActiveTarget:targetID]) { + // Overwrite any existing target mapping with a reset mapping. Every subsequent update + // will modify the reset mapping, not an update mapping. + change.mapping = [[FSTResetMapping alloc] init]; + change.resumeToken = targetChange.resumeToken; + } + break; + default: + FSTWarn(@"Unknown target watch change type: %ld", (long)targetChange.state); + } + } +} + +/** + * Records that we got a watch target add/remove by decrementing the number of pending target + * responses that we have. + */ +- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { + NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + int newCount = count ? [count intValue] - 1 : -1; + if (newCount == 0) { + [self.pendingTargetResponses removeObjectForKey:targetID]; + } else { + [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID]; + } +} + +/** + * Returns true if the given targetId is active. Active targets are those for which there are no + * pending requests to add a listen and are in the current list of targets the client cares about. + * + * Clients can repeatedly listen and stop listening to targets, so this check is useful in + * preventing in preventing race conditions for a target where events arrive but the server hasn't + * yet acknowledged the intended change in state. + */ +- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { + return [self.listenTargets objectForKey:targetID] && + ![self.pendingTargetResponses objectForKey:targetID]; +} + +- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange { + FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID); + if ([self isActiveTarget:targetID]) { + _existenceFilters[targetID] = existenceFilterChange.filter; + } +} + +- (FSTRemoteEvent *)remoteEvent { + NSMutableDictionary *targetChanges = self.targetChanges; + + NSMutableArray *targetsToRemove = [NSMutableArray array]; + + // Apply any inactive targets. + for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) { + if (![self isActiveTarget:targetID]) { + [targetsToRemove addObject:targetID]; + } + } + + [targetChanges removeObjectsForKeys:targetsToRemove]; + + // Mark this aggregator as frozen so no further modifications are made. + self.frozen = YES; + return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion + targetChanges:targetChanges + documentUpdates:self.documentUpdates]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h new file mode 100644 index 0000000..94208e1 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -0,0 +1,143 @@ +/* + * 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 + +#import "FSTDocumentVersionDictionary.h" +#import "FSTTypes.h" + +@class FSTDatabaseInfo; +@class FSTDatastore; +@class FSTDocumentKey; +@class FSTLocalStore; +@class FSTMutationBatch; +@class FSTMutationBatchResult; +@class FSTQuery; +@class FSTQueryData; +@class FSTRemoteEvent; +@class FSTTransaction; +@class FSTUser; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTRemoteSyncer + +/** + * A protocol that describes the actions the FSTRemoteStore needs to perform on a cooperating + * synchronization engine. + */ +@protocol FSTRemoteSyncer + +/** + * Applies one remote event to the sync engine, notifying any views of the changes, and releasing + * any pending mutation batches that would become visible because of the snapshot version the + * remote event contains. + */ +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; + +/** + * Rejects the listen for the given targetID. This can be triggered by the backend for any active + * target. + * + * @param targetID The targetID corresponding to a listen initiated via + * -listenToTargetWithQueryData: on FSTRemoteStore. + * @param error A description of the condition that has forced the rejection. Nearly always this + * will be an indication that the user is no longer authorized to see the data matching the + * target. + */ +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error; + +/** + * Applies the result of a successful write of a mutation batch to the sync engine, emitting + * snapshots in any views that the mutation applies to, and removing the batch from the mutation + * queue. + */ +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult; + +/** + * Rejects the batch, removing the batch from the mutation queue, recomputing the local view of + * any documents affected by the batch and then, emitting snapshots with the reverted value. + */ +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error; + +@end + +/** + * A protocol for the FSTRemoteStore online state delegate, called whenever the state of the + * online streams of the FSTRemoteStore changes. + * Note that this protocol only supports the watch stream for now. + */ +@protocol FSTOnlineStateDelegate + +/** Called whenever the online state of the watch stream changes */ +- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState; + +@end + +#pragma mark - FSTRemoteStore + +/** + * FSTRemoteStore handles all interaction with the backend through a simple, clean interface. This + * class is not thread safe and should be only called from the worker dispatch queue. + */ +@interface FSTRemoteStore : NSObject + ++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +@property(nonatomic, weak) id syncEngine; + +@property(nonatomic, weak) id onlineStateDelegate; + +/** Starts up the remote store, creating streams, restoring state from LocalStore, etc. */ +- (void)start; + +/** Shuts down the remote store, tearing down connections and otherwise cleaning up. */ +- (void)shutdown; + +/** + * Tells the FSTRemoteStore that the currently authenticated user has changed. + * + * In response the remote store tears down streams and clears up any tracked operations that should + * not persist across users. Restarts the streams if appropriate. + */ +- (void)userDidChange:(FSTUser *)user; + +/** Listens to the target identified by the given FSTQueryData. */ +- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData; + +/** Stops listening to the target with the given target ID. */ +- (void)stopListeningToTargetID:(FSTTargetID)targetID; + +/** + * Tells the FSTRemoteStore that there are new mutations to process in the queue. This is typically + * called by FSTSyncEngine after it has sent mutations to FSTLocalStore. + * + * In response the remote store will pull mutations from the local store until the datastore + * instance reports that it cannot accept further in-progress writes. This mechanism serves to + * maintain a pipeline of in-flight requests between the FSTDatastore and the server that + * applies them. + */ +- (void)fillWritePipeline; + +/** Returns a new transaction backed by this remote store. */ +- (FSTTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.m b/Firestore/Source/Remote/FSTRemoteStore.m new file mode 100644 index 0000000..cea2ce8 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteStore.m @@ -0,0 +1,599 @@ +/* + * 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 "FSTRemoteStore.h" + +#import "FSTAssert.h" +#import "FSTDatastore.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExistenceFilter.h" +#import "FSTLocalStore.h" +#import "FSTLogger.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTRemoteEvent.h" +#import "FSTSnapshotVersion.h" +#import "FSTTransaction.h" +#import "FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The maximum number of pending writes to allow. + * TODO(bjornick): Negotiate this value with the backend. + */ +static const NSUInteger kMaxPendingWrites = 10; + +#pragma mark - FSTRemoteStore + +@interface FSTRemoteStore () + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore NS_DESIGNATED_INITIALIZER; + +/** + * The local store, used to fill the write pipeline with outbound mutations and resolve existence + * filter mismatches. Immutable after initialization. + */ +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** The client-side proxy for interacting with the backend. Immutable after initialization. */ +@property(nonatomic, strong, readonly) FSTDatastore *datastore; + +#pragma mark Watch Stream +@property(nonatomic, strong, nullable) FSTWatchStream *watchStream; + +/** + * A mapping of watched targets that the client cares about tracking and the + * user has explicitly called a 'listen' for this target. + * + * These targets may or may not have been sent to or acknowledged by the + * server. On re-establishing the listen stream, these targets should be sent + * to the server. The targets removed with unlistens are removed eagerly + * without waiting for confirmation from the listen stream. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *listenTargets; + +/** + * A mapping of targetId to pending acks needed. + * + * If a targetId is present in this map, then we're waiting for watch to + * acknowledge a removal or addition of the target. If a target is not in this + * mapping, and it's in the listenTargets map, then we consider the target to + * be active. + * + * We increment the count here everytime we issue a request over the stream to + * watch or unwatch. We then decrement the count everytime we get a target + * added or target removed message from the server. Once the count is equal to + * 0 we know that the client and server are in the same state (once this state + * is reached the targetId is removed from the map to free the memory). + */ +@property(nonatomic, strong, readonly) + NSMutableDictionary *pendingTargetResponses; + +@property(nonatomic, strong) NSMutableArray *accumulatedChanges; +@property(nonatomic, assign) FSTBatchID lastBatchSeen; + +/** + * The online state of the watch stream. The state is set to healthy if and only if there are + * messages received by the backend. + */ +@property(nonatomic, assign) FSTOnlineState watchStreamOnlineState; + +#pragma mark Write Stream +@property(nonatomic, strong, nullable) FSTWriteStream *writeStream; + +/** + * The approximate time the StreamingWrite stream was opened. Used to estimate if stream was + * closed due to an auth expiration (a recoverable error) or some other more permanent error. + */ +@property(nonatomic, strong, nullable) NSDate *writeStreamOpenTime; + +/** + * A FIFO queue of in-flight writes. This is in-flight from the point of view of the caller of + * writeMutations, not from the point of view from the Datastore itself. In particular, these + * requests may not have been sent to the Datastore server if the write stream is not yet running. + */ +@property(nonatomic, strong, readonly) NSMutableArray *pendingWrites; +@end + +@implementation FSTRemoteStore + ++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore { + return [[FSTRemoteStore alloc] initWithLocalStore:localStore datastore:datastore]; +} + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDatastore *)datastore { + if (self = [super init]) { + _localStore = localStore; + _datastore = datastore; + _listenTargets = [NSMutableDictionary dictionary]; + _pendingTargetResponses = [NSMutableDictionary dictionary]; + _accumulatedChanges = [NSMutableArray array]; + + _lastBatchSeen = kFSTBatchIDUnknown; + _watchStreamOnlineState = FSTOnlineStateUnknown; + _pendingWrites = [NSMutableArray array]; + } + return self; +} + +- (void)start { + [self setupStreams]; + + // Resume any writes + [self fillWritePipeline]; +} + +- (void)updateAndNotifyAboutOnlineState:(FSTOnlineState)watchStreamOnlineState { + BOOL didChange = (watchStreamOnlineState != self.watchStreamOnlineState); + self.watchStreamOnlineState = watchStreamOnlineState; + if (didChange) { + [self.onlineStateDelegate watchStreamDidChangeOnlineState:watchStreamOnlineState]; + } +} + +- (void)setupStreams { + self.watchStream = [self.datastore createWatchStreamWithDelegate:self]; + self.writeStream = [self.datastore createWriteStreamWithDelegate:self]; + + // Load any saved stream token from persistent storage + self.writeStream.lastStreamToken = [self.localStore lastStreamToken]; +} + +#pragma mark Shutdown + +- (void)shutdown { + FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); + + self.watchStreamOnlineState = FSTOnlineStateUnknown; + [self cleanupWatchStreamState]; + [self.watchStream stop]; + [self.writeStream stop]; +} + +- (void)userDidChange:(FSTUser *)user { + FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user); + + // Clear pending writes because those are per-user. Watched targets persist across users so + // don't clear those. + _lastBatchSeen = kFSTBatchIDUnknown; + [self.pendingWrites removeAllObjects]; + + // Stop the streams. They promise not to call us back. + [self.watchStream stop]; + [self.writeStream stop]; + + [self cleanupWatchStreamState]; + + // Create new streams (but note they're not started yet). + [self setupStreams]; + + // If there are any watchedTargets properly handle the stream restart now that FSTRemoteStore + // is ready to handle them. + if ([self shouldStartWatchStream]) { + [self.watchStream start]; + } + + // Resume any writes + [self fillWritePipeline]; + + // User change moves us back to the unknown state because we might not + // want to re-open the stream + [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; +} + +#pragma mark Watch Stream + +- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { + NSNumber *targetKey = @(queryData.targetID); + FSTAssert(!self.listenTargets[targetKey], @"listenToQuery called with duplicate target id: %@", + targetKey); + + self.listenTargets[targetKey] = queryData; + if ([self.watchStream isOpen]) { + [self sendWatchRequestWithQueryData:queryData]; + } else if (![self.watchStream isStarted]) { + [self.watchStream start]; + } +} + +- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { + [self recordPendingRequestForTargetID:@(queryData.targetID)]; + [self.watchStream watchQuery:queryData]; +} + +- (void)stopListeningToTargetID:(FSTTargetID)targetID { + FSTBoxedTargetID *targetKey = @(targetID); + FSTQueryData *queryData = self.listenTargets[targetKey]; + FSTAssert(queryData, @"unlistenToTarget: target not currently watched: %@", targetKey); + + [self.listenTargets removeObjectForKey:targetKey]; + if ([self.watchStream isOpen]) { + [self sendUnwatchRequestForTargetID:targetKey]; + } +} + +- (void)sendUnwatchRequestForTargetID:(FSTBoxedTargetID *)targetID { + [self recordPendingRequestForTargetID:targetID]; + [self.watchStream unwatchTargetID:[targetID intValue]]; +} + +- (void)recordPendingRequestForTargetID:(FSTBoxedTargetID *)targetID { + NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + count = @([count intValue] + 1); + [self.pendingTargetResponses setObject:count forKey:targetID]; +} + +/** + * Returns whether the watch stream should be started because there are active targets trying to + * be listened to. + */ +- (BOOL)shouldStartWatchStream { + return self.listenTargets.count > 0; +} + +- (void)cleanupWatchStreamState { + // If the connection is closed then we'll never get a snapshot version for the accumulated + // changes and so we'll never be able to complete the batch. When we start up again the server + // is going to resend these changes anyway, so just toss the accumulated state. + [self.accumulatedChanges removeAllObjects]; + [self.pendingTargetResponses removeAllObjects]; +} + +- (void)watchStreamDidOpen { + // Restore any existing watches. + for (FSTQueryData *queryData in [self.listenTargets objectEnumerator]) { + [self sendWatchRequestWithQueryData:queryData]; + } +} + +- (void)watchStreamDidChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + // Mark the connection as healthy because we got a message from the server. + [self updateAndNotifyAboutOnlineState:FSTOnlineStateHealthy]; + + FSTWatchTargetChange *watchTargetChange = + [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; + + if (watchTargetChange && watchTargetChange.state == FSTWatchTargetChangeStateRemoved && + watchTargetChange.cause) { + // There was an error on a target, don't wait for a consistent snapshot to raise events + [self processTargetErrorForWatchChange:(FSTWatchTargetChange *)change]; + } else { + // Accumulate watch changes but don't process them if there's no snapshotVersion or it's + // older than a previous snapshot we've processed (can happen after we resume a target + // using a resume token). + [self.accumulatedChanges addObject:change]; + FSTAssert(snapshotVersion, @"snapshotVersion must not be nil."); + if ([snapshotVersion isEqual:[FSTSnapshotVersion noVersion]] || + [snapshotVersion compare:[self.localStore lastRemoteSnapshotVersion]] == + NSOrderedAscending) { + return; + } + + // Create a batch, giving it the accumulatedChanges array. + NSArray *changes = self.accumulatedChanges; + self.accumulatedChanges = [NSMutableArray array]; + + [self processBatchedWatchChanges:changes snapshotVersion:snapshotVersion]; + } +} + +- (void)watchStreamDidClose:(NSError *_Nullable)error { + [self cleanupWatchStreamState]; + + // If there was an error, retry the connection. + if ([self shouldStartWatchStream]) { + // If the connection fails before the stream has become healthy, consider the online state + // failed. Otherwise consider the online state unknown and the next connection attempt will + // resolve the online state. For example, if a healthy stream is closed due to an expired token + // we want to have one more try at reconnecting before we consider the connection unhealthy. + if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { + [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; + } else { + [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed]; + } + [self.watchStream start]; + } else { + // No need to restart watch stream because there are no active targets. The online state is set + // to unknown because there is no active attempt at establishing a connection. + [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; + } +} + +/** + * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that + * on to the SyncEngine. + */ +- (void)processBatchedWatchChanges:(NSArray *)changes + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion + listenTargets:self.listenTargets + pendingTargetResponses:self.pendingTargetResponses]; + [aggregator addWatchChanges:changes]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + [self.pendingTargetResponses removeAllObjects]; + [self.pendingTargetResponses setDictionary:aggregator.pendingTargetResponses]; + + // Handle existence filters and existence filter mismatches + [aggregator.existenceFilters enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *target, + FSTExistenceFilter *filter, + BOOL *stop) { + FSTTargetID targetID = target.intValue; + + FSTQueryData *queryData = self.listenTargets[target]; + FSTQuery *query = queryData.query; + if (!queryData) { + // A watched target might have been removed already. + return; + + } else if ([query isDocumentQuery]) { + if (filter.count == 0) { + // The existence filter told us the document does not exist. + // We need to deduce that this document does not exist and apply a deleted document to our + // updates. Without applying a deleted document there might be another query that will + // raise this document as part of a snapshot until it is resolved, essentially exposing + // inconsistency between queries + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:query.path]; + FSTDeletedDocument *deletedDoc = + [FSTDeletedDocument documentWithKey:key version:snapshotVersion]; + [remoteEvent addDocumentUpdate:deletedDoc]; + } else { + FSTAssert(filter.count == 1, @"Single document existence filter with count: %" PRId32, + filter.count); + } + + } else { + // Not a document query. + FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; + FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping; + if (mapping) { + if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { + FSTUpdateMapping *update = (FSTUpdateMapping *)mapping; + trackedRemote = [update applyTo:trackedRemote]; + } else { + FSTAssert([mapping isKindOfClass:[FSTResetMapping class]], + @"Expected either reset or update mapping but got something else %@", mapping); + trackedRemote = ((FSTResetMapping *)mapping).documents; + } + } + + if (trackedRemote.count != (NSUInteger)filter.count) { + FSTLog(@"Existence filter mismatch, resetting mapping"); + + // Make sure the mismatch is exposed in the remote event + [remoteEvent handleExistenceFilterMismatchForTargetID:target]; + + // Clear the resume token for the query, since we're in a known mismatch state. + queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:targetID purpose:queryData.purpose]; + self.listenTargets[target] = queryData; + + // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't + // send a resume token so that we get a full update. + [self sendUnwatchRequestForTargetID:@(targetID)]; + + // Mark the query we send as being on behalf of an existence filter mismatch, but don't + // actually retain that in listenTargets. This ensures that we flag the first re-listen + // this way without impacting future listens of this target (that might happen e.g. on + // reconnect). + FSTQueryData *requestQueryData = + [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeExistenceFilterMismatch]; + [self sendWatchRequestWithQueryData:requestQueryData]; + } + } + }]; + + // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when + // applying the completed FSTRemoteEvent. + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + FSTBoxedTargetID *target, FSTTargetChange *change, BOOL *stop) { + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + FSTQueryData *queryData = _listenTargets[target]; + // A watched target might have been removed already. + if (queryData) { + _listenTargets[target] = + [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken]; + } + } + }]; + + // Finally handle remote event + [self.syncEngine applyRemoteEvent:remoteEvent]; +} + +/** Process a target error and passes the error along to SyncEngine. */ +- (void)processTargetErrorForWatchChange:(FSTWatchTargetChange *)change { + FSTAssert(change.cause, @"Handling target error without a cause"); + // Ignore targets that have been removed already. + for (FSTBoxedTargetID *targetID in change.targetIDs) { + if (self.listenTargets[targetID]) { + [self.listenTargets removeObjectForKey:targetID]; + [self.syncEngine rejectListenWithTargetID:targetID error:change.cause]; + } + } +} + +#pragma mark Write Stream + +- (void)fillWritePipeline { + while ([self canWriteMutations]) { + FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:self.lastBatchSeen]; + if (!batch) { + break; + } + [self commitBatch:batch]; + } +} + +/** + * Returns YES if the backend can accept additional write requests. + * + * When sending mutations to the write stream (e.g. in -fillWritePipeline), call this method first + * to check if more mutations can be sent. + * + * Currently the only thing that can prevent the backend from accepting write requests is if + * there are too many requests already outstanding. As writes complete the backend will be able + * to accept more. + */ +- (BOOL)canWriteMutations { + return self.pendingWrites.count < kMaxPendingWrites; +} + +/** Given mutations to commit, actually commits them to the backend. */ +- (void)commitBatch:(FSTMutationBatch *)batch { + FSTAssert([self canWriteMutations], @"commitBatch called when mutations can't be written"); + self.lastBatchSeen = batch.batchID; + + if (!self.writeStream.isStarted) { + [self.writeStream start]; + } + + [self.pendingWrites addObject:batch]; + + if (self.writeStream.handshakeComplete) { + [self.writeStream writeMutations:batch.mutations]; + } +} + +- (void)writeStreamDidOpen { + self.writeStreamOpenTime = [NSDate date]; + + [self.writeStream writeHandshake]; +} + +/** + * Handles a successful handshake response from the server, which is our cue to send any pending + * writes. + */ +- (void)writeStreamDidCompleteHandshake { + // Record the stream token. + [self.localStore setLastStreamToken:self.writeStream.lastStreamToken]; + + // Drain any pending writes. + // + // Note that at this point pendingWrites contains mutations that have already been accepted by + // fillWritePipeline/commitBatch. If the pipeline is full, canWriteMutations will be NO, despite + // the fact that we actually need to send mutations over. + // + // This also means that this method indirectly respects the limits imposed by canWriteMutations + // since writes can't be added to the pendingWrites array when canWriteMutations is NO. If the + // limits imposed by canWriteMutations actually protect us from DOSing ourselves then those limits + // won't be exceeded here and we'll continue to make progress. + for (FSTMutationBatch *write in self.pendingWrites) { + [self.writeStream writeMutations:write.mutations]; + } +} + +/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ +- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray *)results { + // This is a response to a write containing mutations and should be correlated to the first + // pending write. + NSMutableArray *pendingWrites = self.pendingWrites; + FSTMutationBatch *batch = pendingWrites[0]; + [pendingWrites removeObjectAtIndex:0]; + + FSTMutationBatchResult *batchResult = + [FSTMutationBatchResult resultWithBatch:batch + commitVersion:commitVersion + mutationResults:results + streamToken:self.writeStream.lastStreamToken]; + [self.syncEngine applySuccessfulWriteWithResult:batchResult]; + + // It's possible that with the completion of this mutation another slot has freed up. + [self fillWritePipeline]; +} + +/** + * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC + * has been terminated by the client or the server. + */ +- (void)writeStreamDidClose:(NSError *_Nullable)error { + NSMutableArray *pendingWrites = self.pendingWrites; + // Ignore close if there are no pending writes. + if (pendingWrites.count == 0) { + return; + } + + FSTAssert(error, @"There are pending writes, but the write stream closed without an error."); + if ([FSTDatastore isPermanentWriteError:error]) { + if (self.writeStream.handshakeComplete) { + // This error affects the actual writes. + [self handleWriteError:error]; + } else { + // If there was an error before the handshake finished, it's possible that the server is + // unable to process the stream token we're sending. (Perhaps it's too old?) + [self handleHandshakeError:error]; + } + } + + // The write stream might have been started by refilling the write pipeline for failed writes + if (pendingWrites.count > 0 && !self.writeStream.isStarted) { + [self.writeStream start]; + } +} + +- (void)handleHandshakeError:(NSError *)error { + // Reset the token if it's a permanent error or the error code is ABORTED, signaling the write + // stream is no longer valid. + if ([FSTDatastore isPermanentWriteError:error] || [FSTDatastore isAbortedError:error]) { + NSString *token = [self.writeStream.lastStreamToken base64EncodedStringWithOptions:0]; + FSTLog(@"FSTRemoteStore %p error before completed handshake; resetting stream token %@: %@", + (__bridge void *)self, token, error); + self.writeStream.lastStreamToken = nil; + [self.localStore setLastStreamToken:nil]; + } +} + +- (void)handleWriteError:(NSError *)error { + // Only handle permanent error. If it's transient, just let the retry logic kick in. + if (![FSTDatastore isPermanentWriteError:error]) { + return; + } + + // If this was a permanent error, the request itself was the problem so it's not going to + // succeed if we resend it. + FSTMutationBatch *batch = self.pendingWrites[0]; + [self.pendingWrites removeObjectAtIndex:0]; + + // In this case it's also unlikely that the server itself is melting down--this was just a + // bad request so inhibit backoff on the next restart. + [self.writeStream inhibitBackoff]; + + [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:error]; + + // It's possible that with the completion of this mutation another slot has freed up. + [self fillWritePipeline]; +} + +- (FSTTransaction *)transaction { + return [FSTTransaction transactionWithDatastore:self.datastore]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.h b/Firestore/Source/Remote/FSTSerializerBeta.h new file mode 100644 index 0000000..973f866 --- /dev/null +++ b/Firestore/Source/Remote/FSTSerializerBeta.h @@ -0,0 +1,110 @@ +/* + * 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 + +@class FSTDatabaseID; +@class FSTDocumentKey; +@class FSTFieldValue; +@class FSTMaybeDocument; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTMutationResult; +@class FSTObjectValue; +@class FSTQuery; +@class FSTQueryData; +@class FSTSnapshotVersion; +@class FSTTimestamp; +@class FSTWatchChange; + +@class GCFSBatchGetDocumentsResponse; +@class GCFSDocument; +@class GCFSDocumentMask; +@class GCFSListenResponse; +@class GCFSTarget; +@class GCFSTarget_DocumentsTarget; +@class GCFSTarget_QueryTarget; +@class GCFSValue; +@class GCFSWrite; +@class GCFSWriteResult; + +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Converts internal model objects to their equivalent protocol buffer form. Methods starting with + * "encoded" convert to a protocol buffer and methods starting with "decoded" convert from a + * protocol buffer. + * + * Throws an exception if a protocol buffer is missing a critical field or has a value we can't + * interpret. + */ +@interface FSTSerializerBeta : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER; + +- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp; +- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp; + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; + +/** Returns the database ID, such as `projects/{project id}/databases/{database_id}`. */ +- (NSString *)encodedDatabaseID; + +- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key; +- (FSTDocumentKey *)decodedDocumentKey:(NSString *)key; + +- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue; +- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto; + +- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation; +- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation; + +- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation; + +- (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: + (FSTQueryData *)queryData; + +- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData; + +- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query; +- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target; + +- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query; +- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target; + +- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange; +- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange; + +- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue key:(FSTDocumentKey *)key; + +/** + * Encodes an FSTObjectValue into a dictionary. + * @return a new dictionary that can be assigned to a field in another proto. + */ +- (NSMutableDictionary *)encodedFields:(FSTObjectValue *)value; + +- (FSTObjectValue *)decodedFields:(NSDictionary *)fields; + +- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.m b/Firestore/Source/Remote/FSTSerializerBeta.m new file mode 100644 index 0000000..418dabd --- /dev/null +++ b/Firestore/Source/Remote/FSTSerializerBeta.m @@ -0,0 +1,1084 @@ +/* + * 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 "FSTSerializerBeta.h" + +#import + +#import "Common.pbobjc.h" +#import "Document.pbobjc.h" +#import "Firestore.pbobjc.h" +#import "Latlng.pbobjc.h" +#import "Query.pbobjc.h" +#import "Status.pbobjc.h" +#import "Write.pbobjc.h" + +#import "FIRFirestoreErrors.h" +#import "FIRGeoPoint.h" +#import "FSTAssert.h" +#import "FSTDatabaseID.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExistenceFilter.h" +#import "FSTFieldValue.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTSnapshotVersion.h" +#import "FSTTimestamp.h" +#import "FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta () +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@end + +@implementation FSTSerializerBeta + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _databaseID = databaseID; + } + return self; +} + +#pragma mark - FSTSnapshotVersion <=> GPBTimestamp + +- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp { + GPBTimestamp *result = [GPBTimestamp message]; + result.seconds = timestamp.seconds; + result.nanos = timestamp.nanos; + return result; +} + +- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp { + return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos]; +} + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { + return [self encodedTimestamp:version.timestamp]; +} + +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { + return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]]; +} + +#pragma mark - FIRGeoPoint <=> GTPLatLng + +- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint { + GTPLatLng *latLng = [GTPLatLng message]; + latLng.latitude = geoPoint.latitude; + latLng.longitude = geoPoint.longitude; + return latLng; +} + +- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng { + return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude]; +} + +#pragma mark - FSTDocumentKey <=> Key proto + +- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key { + return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path]; +} + +- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name { + FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name]; + FSTAssert([[path segmentAtIndex:1] isEqualToString:self.databaseID.projectID], + @"Tried to deserialize key from different project."); + FSTAssert([[path segmentAtIndex:3] isEqualToString:self.databaseID.databaseID], + @"Tried to deserialize key from different datbase."); + return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; +} + +- (NSString *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID + path:(FSTResourcePath *)path { + return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"] + pathByAppendingPath:path] canonicalString]; +} + +- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name { + FSTResourcePath *path = [FSTResourcePath pathWithString:name]; + FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path); + return path; +} + +- (NSString *)encodedQueryPath:(FSTResourcePath *)path { + if (path.length == 0) { + // If the path is empty, the backend requires we leave off the /documents at the end. + return [self encodedDatabaseID]; + } + return [self encodedResourcePathForDatabaseID:self.databaseID path:path]; +} + +- (FSTResourcePath *)decodedQueryPath:(NSString *)name { + FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name]; + if (resource.length == 4) { + return [FSTResourcePath pathWithSegments:@[]]; + } else { + return [self localResourcePathForQualifiedResourcePath:resource]; + } +} + +- (FSTResourcePath *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID { + return [FSTResourcePath + pathWithSegments:@[ @"projects", databaseID.projectID, @"databases", databaseID.databaseID ]]; +} + +- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName { + FSTAssert( + resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"], + @"Tried to deserialize invalid key %@", resourceName); + return [resourceName pathByRemovingFirstSegments:5]; +} + +- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path { + return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] && + [[path segmentAtIndex:2] isEqualToString:@"databases"]; +} + +- (NSString *)encodedDatabaseID { + return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString]; +} + +#pragma mark - FSTFieldValue <=> Value proto + +- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue { + Class class = [fieldValue class]; + if (class == [FSTNullValue class]) { + return [self encodedNull]; + + } else if (class == [FSTBooleanValue class]) { + return [self encodedBool:[[fieldValue value] boolValue]]; + + } else if (class == [FSTIntegerValue class]) { + return [self encodedInteger:[[fieldValue value] longLongValue]]; + + } else if (class == [FSTDoubleValue class]) { + return [self encodedDouble:[[fieldValue value] doubleValue]]; + + } else if (class == [FSTStringValue class]) { + return [self encodedString:[fieldValue value]]; + + } else if (class == [FSTTimestampValue class]) { + return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue]; + + } else if (class == [FSTGeoPointValue class]) { + return [self encodedGeoPointValue:[fieldValue value]]; + + } else if (class == [FSTBlobValue class]) { + return [self encodedBlobValue:[fieldValue value]]; + + } else if (class == [FSTReferenceValue class]) { + FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue; + return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]]; + + } else if (class == [FSTObjectValue class]) { + GCFSValue *result = [GCFSValue message]; + result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue]; + return result; + + } else if (class == [FSTArrayValue class]) { + GCFSValue *result = [GCFSValue message]; + result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue]; + return result; + + } else { + FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue); + } +} + +- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto { + switch (valueProto.valueTypeOneOfCase) { + case GCFSValue_ValueType_OneOfCase_NullValue: + return [FSTNullValue nullValue]; + + case GCFSValue_ValueType_OneOfCase_BooleanValue: + return [FSTBooleanValue booleanValue:valueProto.booleanValue]; + + case GCFSValue_ValueType_OneOfCase_IntegerValue: + return [FSTIntegerValue integerValue:valueProto.integerValue]; + + case GCFSValue_ValueType_OneOfCase_DoubleValue: + return [FSTDoubleValue doubleValue:valueProto.doubleValue]; + + case GCFSValue_ValueType_OneOfCase_StringValue: + return [FSTStringValue stringValue:valueProto.stringValue]; + + case GCFSValue_ValueType_OneOfCase_TimestampValue: + return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]]; + + case GCFSValue_ValueType_OneOfCase_GeoPointValue: + return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]]; + + case GCFSValue_ValueType_OneOfCase_BytesValue: + return [FSTBlobValue blobValue:valueProto.bytesValue]; + + case GCFSValue_ValueType_OneOfCase_ReferenceValue: + return [self decodedReferenceValue:valueProto.referenceValue]; + + case GCFSValue_ValueType_OneOfCase_ArrayValue: + return [self decodedArrayValue:valueProto.arrayValue]; + + case GCFSValue_ValueType_OneOfCase_MapValue: + return [self decodedMapValue:valueProto.mapValue]; + + default: + FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto); + } +} + +- (GCFSValue *)encodedNull { + GCFSValue *result = [GCFSValue message]; + result.nullValue = GPBNullValue_NullValue; + return result; +} + +- (GCFSValue *)encodedBool:(BOOL)value { + GCFSValue *result = [GCFSValue message]; + result.booleanValue = value; + return result; +} + +- (GCFSValue *)encodedDouble:(double)value { + GCFSValue *result = [GCFSValue message]; + result.doubleValue = value; + return result; +} + +- (GCFSValue *)encodedInteger:(int64_t)value { + GCFSValue *result = [GCFSValue message]; + result.integerValue = value; + return result; +} + +- (GCFSValue *)encodedString:(NSString *)value { + GCFSValue *result = [GCFSValue message]; + result.stringValue = value; + return result; +} + +- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value { + GCFSValue *result = [GCFSValue message]; + result.timestampValue = [self encodedTimestamp:value]; + return result; +} + +- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value { + GCFSValue *result = [GCFSValue message]; + result.geoPointValue = [self encodedGeoPoint:value]; + return result; +} + +- (GCFSValue *)encodedBlobValue:(NSData *)value { + GCFSValue *result = [GCFSValue message]; + result.bytesValue = value; + return result; +} + +- (GCFSValue *)encodedReferenceValueForDatabaseID:(FSTDatabaseID *)databaseID + key:(FSTDocumentKey *)key { + GCFSValue *result = [GCFSValue message]; + result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path]; + return result; +} + +- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { + FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName]; + NSString *project = [path segmentAtIndex:1]; + NSString *database = [path segmentAtIndex:3]; + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:project database:database]; + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; + return [FSTReferenceValue referenceValue:key databaseID:databaseID]; +} + +- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { + GCFSArrayValue *proto = [GCFSArrayValue message]; + NSMutableArray *protoContents = [proto valuesArray]; + + [[arrayValue internalValue] + enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) { + GCFSValue *converted = [self encodedFieldValue:value]; + [protoContents addObject:converted]; + }]; + return proto; +} + +- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue { + NSMutableArray *contents = + [NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count]; + + [arrayValue.valuesArray + enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) { + [contents addObject:[self decodedFieldValue:value]]; + }]; + return [[FSTArrayValue alloc] initWithValueNoCopy:contents]; +} + +- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value { + GCFSMapValue *result = [GCFSMapValue message]; + result.fields = [self encodedFields:value]; + return result; +} + +- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map { + return [self decodedFields:map.fields]; +} + +/** + * Encodes an FSTObjectValue into a dictionary. + * @return a new dictionary that can be assigned to a field in another proto. + */ +- (NSMutableDictionary *)encodedFields:(FSTObjectValue *)value { + FSTImmutableSortedDictionary *fields = value.internalValue; + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { + GCFSValue *converted = [self encodedFieldValue:obj]; + result[key] = converted; + }]; + return result; +} + +- (FSTObjectValue *)decodedFields:(NSDictionary *)fields { + __block FSTObjectValue *result = [FSTObjectValue objectValue]; + [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj, + BOOL *_Nonnull stop) { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]]; + FSTFieldValue *value = [self decodedFieldValue:obj]; + result = [result objectBySettingValue:value forPath:path]; + }]; + return result; +} + +#pragma mark - FSTObjectValue <=> Document proto + +- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue + key:(FSTDocumentKey *)key { + GCFSDocument *proto = [GCFSDocument message]; + proto.name = [self encodedDocumentKey:key]; + proto.fields = [self encodedFields:objectValue]; + return proto; +} + +#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto + +- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response { + switch (response.resultOneOfCase) { + case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found: + return [self decodedFoundDocument:response]; + case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing: + return [self decodedDeletedDocument:response]; + default: + FSTFail(@"Unknown document type: %@", response); + } +} + +- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response { + FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document."); + FSTDocumentKey *key = [self decodedDocumentKey:response.found.name]; + FSTObjectValue *value = [self decodedFields:response.found.fields]; + FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a document response with no snapshot version"); + + return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; +} + +- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { + FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document."); + FSTDocumentKey *key = [self decodedDocumentKey:response.missing]; + FSTSnapshotVersion *version = [self decodedVersion:response.readTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a no document response with no snapshot version"); + return [FSTDeletedDocument documentWithKey:key version:version]; +} + +#pragma mark - FSTMutation => GCFSWrite proto + +- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation { + GCFSWrite *proto = [GCFSWrite message]; + + Class mutationClass = [mutation class]; + if (mutationClass == [FSTSetMutation class]) { + FSTSetMutation *set = (FSTSetMutation *)mutation; + proto.update = [self encodedDocumentWithFields:set.value key:set.key]; + + } else if (mutationClass == [FSTPatchMutation class]) { + FSTPatchMutation *patch = (FSTPatchMutation *)mutation; + proto.update = [self encodedDocumentWithFields:patch.value key:patch.key]; + proto.updateMask = [self encodedFieldMask:patch.fieldMask]; + + } else if (mutationClass == [FSTTransformMutation class]) { + FSTTransformMutation *transform = (FSTTransformMutation *)mutation; + + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self encodedDocumentKey:transform.key]; + proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms]; + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + proto.currentDocument.exists = YES; + + } else if (mutationClass == [FSTDeleteMutation class]) { + FSTDeleteMutation *delete = (FSTDeleteMutation *)mutation; + proto.delete_p = [self encodedDocumentKey:delete.key]; + + } else { + FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass)); + } + + if (!mutation.precondition.isNone) { + proto.currentDocument = [self encodedPrecondition:mutation.precondition]; + } + + return proto; +} + +- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation { + FSTPrecondition *precondition = [mutation hasCurrentDocument] + ? [self decodedPrecondition:mutation.currentDocument] + : [FSTPrecondition none]; + + switch (mutation.operationOneOfCase) { + case GCFSWrite_Operation_OneOfCase_Update: + if (mutation.hasUpdateMask) { + return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] + fieldMask:[self decodedFieldMask:mutation.updateMask] + value:[self decodedFields:mutation.update.fields] + precondition:precondition]; + } else { + return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] + value:[self decodedFields:mutation.update.fields] + precondition:precondition]; + } + + case GCFSWrite_Operation_OneOfCase_Delete_p: + return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p] + precondition:precondition]; + + case GCFSWrite_Operation_OneOfCase_Transform: { + FSTPreconditionExists exists = precondition.exists; + FSTAssert(exists == FSTPreconditionExistsYes, + @"Transforms must have precondition \"exists == true\""); + + return [[FSTTransformMutation alloc] + initWithKey:[self decodedDocumentKey:mutation.transform.document] + fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]]; + } + + default: + // Note that insert is intentionally unhandled, since we don't ever deal in them. + FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase); + } +} + +- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition { + FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition"); + GCFSPrecondition *message = [GCFSPrecondition message]; + if (precondition.updateTime) { + message.updateTime = [self encodedVersion:precondition.updateTime]; + } else if (precondition.exists != FSTPreconditionExistsNotSet) { + message.exists = precondition.exists == FSTPreconditionExistsYes; + } else { + FSTFail(@"Unknown precondition: %@", precondition); + } + return message; +} + +- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition { + switch (precondition.conditionTypeOneOfCase) { + case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase: + return [FSTPrecondition none]; + + case GCFSPrecondition_ConditionType_OneOfCase_Exists: + return [FSTPrecondition preconditionWithExists:precondition.exists]; + + case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime: + return [FSTPrecondition + preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]]; + + default: + FSTFail(@"Unrecognized Precondition one-of case %@", precondition); + } +} + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask { + GCFSDocumentMask *mask = [GCFSDocumentMask message]; + for (FSTFieldPath *field in fieldMask.fields) { + [mask.fieldPathsArray addObject:field.canonicalString]; + } + return mask; +} + +- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask { + NSMutableArray *fields = + [NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count]; + for (NSString *path in fieldMask.fieldPathsArray) { + [fields addObject:[FSTFieldPath pathWithServerFormat:path]]; + } + return [[FSTFieldMask alloc] initWithFields:fields]; +} + +- (NSMutableArray *)encodedFieldTransforms: + (NSArray *)fieldTransforms { + NSMutableArray *protos = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in fieldTransforms) { + FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]], + @"Unknown transform: %@", fieldTransform.transform); + GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message]; + proto.fieldPath = fieldTransform.path.canonicalString; + proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime; + [protos addObject:proto]; + } + return protos; +} + +- (NSArray *)decodedFieldTransforms: + (NSArray *)protos { + NSMutableArray *fieldTransforms = [NSMutableArray array]; + for (GCFSDocumentTransform_FieldTransform *proto in protos) { + FSTAssert( + proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime, + @"Unknown transform setToServerValue: %d", proto.setToServerValue); + [fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath] + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + } + return fieldTransforms; +} + +#pragma mark - FSTMutationResult <= GCFSWriteResult proto + +- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { + // NOTE: Deletes don't have an updateTime. + FSTSnapshotVersion *_Nullable version = + mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil; + NSMutableArray *_Nullable transformResults = nil; + if (mutation.transformResultsArray.count > 0) { + transformResults = [NSMutableArray array]; + for (GCFSValue *result in mutation.transformResultsArray) { + [transformResults addObject:[self decodedFieldValue:result]]; + } + } + return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults]; +} + +#pragma mark - FSTQueryData => GCFSTarget proto + +- (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: + (FSTQueryData *)queryData { + NSString *value = [self encodedLabelForPurpose:queryData.purpose]; + if (!value) { + return nil; + } + + NSMutableDictionary *result = + [NSMutableDictionary dictionaryWithCapacity:1]; + [result setObject:value forKey:@"goog-listen-tags"]; + return result; +} + +- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose { + switch (purpose) { + case FSTQueryPurposeListen: + return nil; + case FSTQueryPurposeExistenceFilterMismatch: + return @"existence-filter-mismatch"; + case FSTQueryPurposeLimboResolution: + return @"limbo-document"; + default: + FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose); + } +} + +- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData { + GCFSTarget *result = [GCFSTarget message]; + FSTQuery *query = queryData.query; + + if ([query isDocumentQuery]) { + result.documents = [self encodedDocumentsTarget:query]; + } else { + result.query = [self encodedQueryTarget:query]; + } + + result.targetId = queryData.targetID; + if (queryData.resumeToken.length > 0) { + result.resumeToken = queryData.resumeToken; + } + + return result; +} + +- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query { + GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message]; + NSMutableArray *docs = result.documentsArray; + [docs addObject:[self encodedQueryPath:query.path]]; + return result; +} + +- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target { + NSArray *documents = target.documentsArray; + FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu", + (unsigned long)documents.count); + + NSString *name = documents[0]; + return [FSTQuery queryWithPath:[self decodedQueryPath:name]]; +} + +- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query { + // Dissect the path into parent, collectionId, and optional key filter. + GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message]; + if (query.path.length == 0) { + queryTarget.parent = [self encodedQueryPath:query.path]; + } else { + FSTResourcePath *path = query.path; + FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported."); + queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]]; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = path.lastSegment; + [queryTarget.structuredQuery.fromArray addObject:from]; + } + + // Encode the filters. + GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters]; + if (where) { + queryTarget.structuredQuery.where = where; + } + + NSArray *orders = [self encodedSortOrders:query.sortOrders]; + if (orders.count) { + [queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders]; + } + + if (query.limit != NSNotFound) { + queryTarget.structuredQuery.limit.value = (int32_t)query.limit; + } + + if (query.startAt) { + queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt]; + } + + if (query.endAt) { + queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt]; + } + + return queryTarget; +} + +- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target { + FSTResourcePath *path = [self decodedQueryPath:target.parent]; + + GCFSStructuredQuery *query = target.structuredQuery; + NSUInteger fromCount = query.fromArray_Count; + if (fromCount > 0) { + FSTAssert(fromCount == 1, + @"StructuredQuery.from with more than one collection is not supported."); + + GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0]; + path = [path pathByAppendingSegment:from.collectionId]; + } + + NSArray> *filterBy; + if (query.hasWhere) { + filterBy = [self decodedFilters:query.where]; + } else { + filterBy = @[]; + } + + NSArray *orderBy; + if (query.orderByArray_Count > 0) { + orderBy = [self decodedSortOrders:query.orderByArray]; + } else { + orderBy = @[]; + } + + NSInteger limit = NSNotFound; + if (query.hasLimit) { + limit = query.limit.value; + } + + FSTBound *_Nullable startAt; + if (query.hasStartAt) { + startAt = [self decodedBound:query.startAt]; + } + + FSTBound *_Nullable endAt; + if (query.hasEndAt) { + endAt = [self decodedBound:query.endAt]; + } + + return [[FSTQuery alloc] initWithPath:path + filterBy:filterBy + orderBy:orderBy + limit:limit + startAt:startAt + endAt:endAt]; +} + +#pragma mark Filters + +- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray> *)filters { + if (filters.count == 0) { + return nil; + } + NSMutableArray *protos = [NSMutableArray array]; + for (id filter in filters) { + if ([filter isKindOfClass:[FSTRelationFilter class]]) { + [protos addObject:[self encodedRelationFilter:filter]]; + } else { + [protos addObject:[self encodedUnaryFilter:filter]]; + } + } + if (protos.count == 1) { + // Special case: no existing filters and we only need to add one filter. This can be made the + // single root filter without a composite filter. + return protos[0]; + } + GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message]; + composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + composite.compositeFilter.filtersArray = protos; + return composite; +} + +- (NSArray> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto { + NSMutableArray> *result = [NSMutableArray array]; + + NSArray *filters; + if (proto.filterTypeOneOfCase == + GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) { + FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And, + @"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op); + filters = proto.compositeFilter.filtersArray; + } else { + filters = @[ proto ]; + } + + for (GCFSStructuredQuery_Filter *filter in filters) { + switch (filter.filterTypeOneOfCase) { + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter: + FSTFail(@"Nested composite filters are not supported"); + + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter: + [result addObject:[self decodedRelationFilter:filter.fieldFilter]]; + break; + + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter: + [result addObject:[self decodedUnaryFilter:filter.unaryFilter]]; + break; + + default: + FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase); + } + } + return result; +} + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter { + GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter; + fieldFilter.field = [self encodedFieldPath:filter.field]; + fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator]; + fieldFilter.value = [self encodedFieldValue:filter.value]; + return proto; +} + +- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto { + FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op]; + FSTFieldValue *value = [self decodedFieldValue:proto.value]; + return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value]; +} + +- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id)filter { + GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; + proto.unaryFilter.field = [self encodedFieldPath:filter.field]; + if ([filter isKindOfClass:[FSTNanFilter class]]) { + proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan; + } else if ([filter isKindOfClass:[FSTNullFilter class]]) { + proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull; + } else { + FSTFail(@"Unrecognized filter: %@", filter); + } + return proto; +} + +- (id)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto { + FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + switch (proto.op) { + case GCFSStructuredQuery_UnaryFilter_Operator_IsNan: + return [[FSTNanFilter alloc] initWithField:field]; + + case GCFSStructuredQuery_UnaryFilter_Operator_IsNull: + return [[FSTNullFilter alloc] initWithField:field]; + + default: + FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op); + } +} + +- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath { + GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message]; + ref.fieldPath = fieldPath.canonicalString; + return ref; +} + +- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator: + (FSTRelationFilterOperator)filterOperator { + switch (filterOperator) { + case FSTRelationFilterOperatorLessThan: + return GCFSStructuredQuery_FieldFilter_Operator_LessThan; + case FSTRelationFilterOperatorLessThanOrEqual: + return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual; + case FSTRelationFilterOperatorEqual: + return GCFSStructuredQuery_FieldFilter_Operator_Equal; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + case FSTRelationFilterOperatorGreaterThan: + return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan; + default: + FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator); + } +} + +- (FSTRelationFilterOperator)decodedRelationFilterOperator: + (GCFSStructuredQuery_FieldFilter_Operator)filterOperator { + switch (filterOperator) { + case GCFSStructuredQuery_FieldFilter_Operator_LessThan: + return FSTRelationFilterOperatorLessThan; + case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual: + return FSTRelationFilterOperatorLessThanOrEqual; + case GCFSStructuredQuery_FieldFilter_Operator_Equal: + return FSTRelationFilterOperatorEqual; + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual: + return FSTRelationFilterOperatorGreaterThanOrEqual; + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan: + return FSTRelationFilterOperatorGreaterThan; + default: + FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator); + } +} + +#pragma mark Property Orders + +- (NSArray *)encodedSortOrders:(NSArray *)orders { + NSMutableArray *protos = [NSMutableArray array]; + for (FSTSortOrder *order in orders) { + [protos addObject:[self encodedSortOrder:order]]; + } + return protos; +} + +- (NSArray *)decodedSortOrders:(NSArray *)protos { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:protos.count]; + for (GCFSStructuredQuery_Order *orderProto in protos) { + [result addObject:[self decodedSortOrder:orderProto]]; + } + return result; +} + +- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder { + GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message]; + proto.field = [self encodedFieldPath:sortOrder.field]; + if (sortOrder.ascending) { + proto.direction = GCFSStructuredQuery_Direction_Ascending; + } else { + proto.direction = GCFSStructuredQuery_Direction_Descending; + } + return proto; +} + +- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto { + FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + BOOL ascending; + switch (proto.direction) { + case GCFSStructuredQuery_Direction_Ascending: + ascending = YES; + break; + case GCFSStructuredQuery_Direction_Descending: + ascending = NO; + break; + default: + FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction); + } + return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending]; +} + +#pragma mark - Bounds/Cursors + +- (GCFSCursor *)encodedBound:(FSTBound *)bound { + GCFSCursor *proto = [GCFSCursor message]; + proto.before = bound.isBefore; + for (FSTFieldValue *fieldValue in bound.position) { + GCFSValue *value = [self encodedFieldValue:fieldValue]; + [proto.valuesArray addObject:value]; + } + return proto; +} + +- (FSTBound *)decodedBound:(GCFSCursor *)proto { + NSMutableArray *indexComponents = [NSMutableArray array]; + + for (GCFSValue *valueProto in proto.valuesArray) { + FSTFieldValue *value = [self decodedFieldValue:valueProto]; + [indexComponents addObject:value]; + } + + return [FSTBound boundWithPosition:indexComponents isBefore:proto.before]; +} + +#pragma mark - FSTWatchChange <= GCFSListenResponse proto + +- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange { + switch (watchChange.responseTypeOneOfCase) { + case GCFSListenResponse_ResponseType_OneOfCase_TargetChange: + return [self decodedTargetChangeFromWatchChange:watchChange.targetChange]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange: + return [self decodedDocumentChange:watchChange.documentChange]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete: + return [self decodedDocumentDelete:watchChange.documentDelete]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove: + return [self decodedDocumentRemove:watchChange.documentRemove]; + + case GCFSListenResponse_ResponseType_OneOfCase_Filter: + return [self decodedExistenceFilterWatchChange:watchChange.filter]; + + default: + FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase); + } +} + +- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange { + // We have only reached a consistent snapshot for the entire stream if there is a read_time set + // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to + // send such responses. + if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) { + return [FSTSnapshotVersion noVersion]; + } + if (watchChange.targetChange.targetIdsArray.count != 0) { + return [FSTSnapshotVersion noVersion]; + } + return [self decodedVersion:watchChange.targetChange.readTime]; +} + +- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change { + FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType]; + NSMutableArray *targetIDs = + [NSMutableArray arrayWithCapacity:change.targetIdsArray_Count]; + + [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [targetIDs addObject:@(value)]; + }]; + + NSError *cause = nil; + if (change.hasCause) { + cause = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:change.cause.code + userInfo:@{NSLocalizedDescriptionKey : change.cause.message}]; + } + + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:change.resumeToken + cause:cause]; +} + +- (FSTWatchTargetChangeState)decodedWatchTargetChangeState: + (GCFSTargetChange_TargetChangeType)state { + switch (state) { + case GCFSTargetChange_TargetChangeType_NoChange: + return FSTWatchTargetChangeStateNoChange; + case GCFSTargetChange_TargetChangeType_Add: + return FSTWatchTargetChangeStateAdded; + case GCFSTargetChange_TargetChangeType_Remove: + return FSTWatchTargetChangeStateRemoved; + case GCFSTargetChange_TargetChangeType_Current: + return FSTWatchTargetChangeStateCurrent; + case GCFSTargetChange_TargetChangeType_Reset: + return FSTWatchTargetChangeStateReset; + default: + FSTFail(@"Unexpected TargetChange.state: %" PRId32, state); + } +} + +- (NSArray *)decodedIntegerArray:(GPBInt32Array *)values { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:values.count]; + [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [result addObject:@(value)]; + }]; + return result; +} + +- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change { + FSTObjectValue *value = [self decodedFields:change.document.fields]; + FSTDocumentKey *key = [self decodedDocumentKey:change.document.name]; + FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a document change with no snapshot version"); + FSTMaybeDocument *document = + [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + + NSArray *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray]; + NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds + removedTargetIDs:removedTargetIds + documentKey:document.key + document:document]; +} + +- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { + FSTDocumentKey *key = [self decodedDocumentKey:change.document]; + // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion] + FSTSnapshotVersion *version = [self decodedVersion:change.readTime]; + FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; + + NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:removedTargetIds + documentKey:document.key + document:document]; +} + +- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change { + FSTDocumentKey *key = [self decodedDocumentKey:change.document]; + NSArray *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:removedTargetIds + documentKey:key + document:nil]; +} + +- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter { + // TODO(dimond): implement existence filter parsing + FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count]; + FSTTargetID targetID = filter.targetId; + return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTWatchChange.h b/Firestore/Source/Remote/FSTWatchChange.h new file mode 100644 index 0000000..6b65279 --- /dev/null +++ b/Firestore/Source/Remote/FSTWatchChange.h @@ -0,0 +1,118 @@ +/* + * 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 + +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTExistenceFilter; +@class FSTMaybeDocument; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTWatchChange is the internal representation of the watcher API protocol buffers. + * This is an empty abstract class so that all the different kinds of changes can have a common + * base class. + */ +@interface FSTWatchChange : NSObject +@end + +/** + * FSTDocumentWatchChange represents a changed document and a list of target ids to which this + * change applies. If the document has been deleted, the deleted document will be provided. + */ +@interface FSTDocumentWatchChange : FSTWatchChange + +- (instancetype)initWithUpdatedTargetIDs:(NSArray *)updatedTargetIDs + removedTargetIDs:(NSArray *)removedTargetIDs + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTMaybeDocument *)document + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The new document applies to all of these targets. */ +@property(nonatomic, strong, readonly) NSArray *updatedTargetIDs; + +/** The new document is removed from all of these targets. */ +@property(nonatomic, strong, readonly) NSArray *removedTargetIDs; + +/** The key of the document for this change. */ +@property(nonatomic, strong, readonly) FSTDocumentKey *documentKey; + +/** + * The new document or DeletedDocument if it was deleted. Is null if the document went out of + * view without the server sending a new document. + */ +@property(nonatomic, strong, readonly, nullable) FSTMaybeDocument *document; + +@end + +/** + * An ExistenceFilterWatchChange applies to the targets and is required to verify the current client + * state against expected state sent from the server. + */ +@interface FSTExistenceFilterWatchChange : FSTWatchChange + ++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID; + +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, strong, readonly) FSTExistenceFilter *filter; +@property(nonatomic, assign, readonly) FSTTargetID targetID; +@end + +/** FSTWatchTargetChangeState is the kind of change that happened to the watch target. */ +typedef NS_ENUM(NSInteger, FSTWatchTargetChangeState) { + FSTWatchTargetChangeStateNoChange, + FSTWatchTargetChangeStateAdded, + FSTWatchTargetChangeStateRemoved, + FSTWatchTargetChangeStateCurrent, + FSTWatchTargetChangeStateReset, +}; + +/** FSTWatchTargetChange is a change to a watch target. */ +@interface FSTWatchTargetChange : FSTWatchChange + +- (instancetype)initWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + resumeToken:(NSData *)resumeToken + cause:(nullable NSError *)cause NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** What kind of change occurred to the watch target. */ +@property(nonatomic, assign, readonly) FSTWatchTargetChangeState state; + +/** The target IDs that were added/removed/set. */ +@property(nonatomic, strong, readonly) NSArray *targetIDs; + +/** + * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting + * without retransmitting all the data that matches the query. The resume token essentially + * identifies a point in time from which the server should resume sending results. + */ +@property(nonatomic, strong, readonly) NSData *resumeToken; + +/** An RPC error indicating why the watch failed. */ +@property(nonatomic, strong, readonly, nullable) NSError *cause; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTWatchChange.m b/Firestore/Source/Remote/FSTWatchChange.m new file mode 100644 index 0000000..1ace26e --- /dev/null +++ b/Firestore/Source/Remote/FSTWatchChange.m @@ -0,0 +1,150 @@ +/* + * 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 "FSTWatchChange.h" + +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExistenceFilter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchChange +@end + +@implementation FSTDocumentWatchChange + +- (instancetype)initWithUpdatedTargetIDs:(NSArray *)updatedTargetIDs + removedTargetIDs:(NSArray *)removedTargetIDs + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTMaybeDocument *)document { + self = [super init]; + if (self) { + _updatedTargetIDs = updatedTargetIDs; + _removedTargetIDs = removedTargetIDs; + _documentKey = documentKey; + _document = document; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTDocumentWatchChange class]]) { + return NO; + } + + FSTDocumentWatchChange *otherChange = (FSTDocumentWatchChange *)other; + return [_updatedTargetIDs isEqual:otherChange.updatedTargetIDs] && + [_removedTargetIDs isEqual:otherChange.removedTargetIDs] && + [_documentKey isEqual:otherChange.documentKey] && + (_document == otherChange.document || [_document isEqual:otherChange.document]); +} + +- (NSUInteger)hash { + NSUInteger hash = self.updatedTargetIDs.hash; + hash = hash * 31 + self.removedTargetIDs.hash; + hash = hash * 31 + self.documentKey.hash; + hash = hash * 31 + self.document.hash; + return hash; +} + +@end + +@interface FSTExistenceFilterWatchChange () + +- (instancetype)initWithFilter:(FSTExistenceFilter *)filter + targetID:(FSTTargetID)targetID NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTExistenceFilterWatchChange + ++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { + return [[FSTExistenceFilterWatchChange alloc] initWithFilter:filter targetID:targetID]; +} + +- (instancetype)initWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { + self = [super init]; + if (self) { + _filter = filter; + _targetID = targetID; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTExistenceFilterWatchChange class]]) { + return NO; + } + + FSTExistenceFilterWatchChange *otherChange = (FSTExistenceFilterWatchChange *)other; + return [_filter isEqual:otherChange->_filter] && _targetID == otherChange->_targetID; +} + +- (NSUInteger)hash { + return self.filter.hash; +} + +@end + +@implementation FSTWatchTargetChange + +- (instancetype)initWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray *)targetIDs + resumeToken:(NSData *)resumeToken + cause:(nullable NSError *)cause { + self = [super init]; + if (self) { + _state = state; + _targetIDs = targetIDs; + _resumeToken = resumeToken; + _cause = cause; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTWatchTargetChange class]]) { + return NO; + } + + FSTWatchTargetChange *otherChange = (FSTWatchTargetChange *)other; + return _state == otherChange->_state && [_targetIDs isEqual:otherChange->_targetIDs] && + [_resumeToken isEqual:otherChange->_resumeToken] && + (_cause == otherChange->_cause || [_cause isEqual:otherChange->_cause]); +} + +- (NSUInteger)hash { + NSUInteger hash = (NSUInteger)self.state; + + hash = hash * 31 + self.targetIDs.hash; + hash = hash * 31 + self.resumeToken.hash; + hash = hash * 31 + self.cause.hash; + return hash; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAssert.h b/Firestore/Source/Util/FSTAssert.h new file mode 100644 index 0000000..77bbb1d --- /dev/null +++ b/Firestore/Source/Util/FSTAssert.h @@ -0,0 +1,77 @@ +/* + * 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. + */ + +#include + +NS_ASSUME_NONNULL_BEGIN + +// Fails the current Objective-C method if the given condition is false. +// +// Unlike NSAssert, this macro is never compiled out if assertions are disabled. +#define FSTAssert(condition, format, ...) \ + do { \ + if (!(condition)) { \ + FSTFail((format), ##__VA_ARGS__); \ + } \ + } while (0) + +// Fails the current C function if the given condition is false. +// +// Unlike NSCAssert, this macro is never compiled out if assertions are disabled. +#define FSTCAssert(condition, format, ...) \ + do { \ + if (!(condition)) { \ + FSTCFail((format), ##__VA_ARGS__); \ + } \ + } while (0) + +// Unconditionally fails the current Objective-C method. +// +// This macro fails by calling [[NSAssertionHandler currentHandler] handleFailureInMethod]. It +// also calls abort(3) in order to make this macro appear to never return, even though the call +// to handleFailureInMethod itself never returns. +#define FSTFail(format, ...) \ + do { \ + NSString *_file = [NSString stringWithUTF8String:__FILE__]; \ + NSString *_description = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + [[NSAssertionHandler currentHandler] \ + handleFailureInMethod:_cmd \ + object:self \ + file:_file \ + lineNumber:__LINE__ \ + description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", _description]; \ + abort(); \ + } while (0) + +// Unconditionally fails the current C function. +// +// This macro fails by calling [[NSAssertionHandler currentHandler] handleFailureInFunction]. It +// also calls abort(3) in order to make this macro appear to never return, even though the call +// to handleFailureInFunction itself never returns. +#define FSTCFail(format, ...) \ + do { \ + NSString *_file = [NSString stringWithUTF8String:__FILE__]; \ + NSString *_function = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \ + NSString *_description = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + [[NSAssertionHandler currentHandler] \ + handleFailureInFunction:_function \ + file:_file \ + lineNumber:__LINE__ \ + description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", _description]; \ + abort(); \ + } while (0) + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.h b/Firestore/Source/Util/FSTAsyncQueryListener.h new file mode 100644 index 0000000..0ff1551 --- /dev/null +++ b/Firestore/Source/Util/FSTAsyncQueryListener.h @@ -0,0 +1,48 @@ +/* + * 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 + +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDispatchQueue; +@class FSTQueryListener; + +/** + * A wrapper class around FSTQueryListener that dispatches events asynchronously. + */ +@interface FSTAsyncQueryListener : NSObject + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Synchronously mutes the listener and raise no further events. This method is thread safe can be + * called from any queue. + */ +- (void)mute; + +/** Creates an asynchronous version of the provided snapshot handler. */ +- (FSTViewSnapshotHandler)asyncSnapshotHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.m b/Firestore/Source/Util/FSTAsyncQueryListener.m new file mode 100644 index 0000000..31951e1 --- /dev/null +++ b/Firestore/Source/Util/FSTAsyncQueryListener.m @@ -0,0 +1,50 @@ +/* + * 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 "FSTAsyncQueryListener.h" + +#import "FSTDispatchQueue.h" + +@implementation FSTAsyncQueryListener { + volatile BOOL _muted; + FSTViewSnapshotHandler _snapshotHandler; + FSTDispatchQueue *_dispatchQueue; +} + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { + if (self = [super init]) { + _dispatchQueue = dispatchQueue; + _snapshotHandler = snapshotHandler; + } + return self; +} + +- (FSTViewSnapshotHandler)asyncSnapshotHandler { + return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + [_dispatchQueue dispatchAsync:^{ + if (!_muted) { + _snapshotHandler(snapshot, error); + } + }]; + }; +} + +- (void)mute { + _muted = true; +} + +@end diff --git a/Firestore/Source/Util/FSTClasses.h b/Firestore/Source/Util/FSTClasses.h new file mode 100644 index 0000000..77dca12 --- /dev/null +++ b/Firestore/Source/Util/FSTClasses.h @@ -0,0 +1,40 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +// A convenience macro for unimplemented methods. Use as follows: +// +// @throw FSTAbstractMethodException(); // NOLINT +#define FSTAbstractMethodException() \ + [NSException exceptionWithName:NSInternalInconsistencyException \ + reason:[NSString stringWithFormat:@"You must override %s in a subclass", \ + __func__] \ + userInfo:nil]; + +// Declare a weak pointer to the given variable +#define FSTWeakify(var) __weak typeof(var) fstWeakPointerTo##var = var; + +// Declare a strong pointer to a variable that's been FSTWeakified. This creates a shadow of the +// original. +#define FSTStrongify(var) \ + _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong typeof(var) var = fstWeakPointerTo##var; \ + _Pragma("clang diagnostic pop") + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTComparison.h b/Firestore/Source/Util/FSTComparison.h new file mode 100644 index 0000000..e6e57e6 --- /dev/null +++ b/Firestore/Source/Util/FSTComparison.h @@ -0,0 +1,66 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** Compares two NSStrings. */ +NSComparisonResult FSTCompareStrings(NSString *left, NSString *right); + +/** Compares two BOOLs. */ +NSComparisonResult FSTCompareBools(BOOL left, BOOL right); + +/** Compares two integers. */ +NSComparisonResult FSTCompareInts(int left, int right); + +/** Compares two int32_t. */ +NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right); + +/** Compares two int64_t. */ +NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right); + +/** Compares two NSUIntegers. */ +NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right); + +/** Compares two doubles (using Firestore semantics for NaN). */ +NSComparisonResult FSTCompareDoubles(double left, double right); + +/** Compares a double and an int64_t. */ +NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue); + +/** Compare two NSData byte sequences. */ +NSComparisonResult FSTCompareBytes(NSData *left, NSData *right); + +/** A simple NSComparator for comparing NSNumber instances. */ +extern const NSComparator FSTNumberComparator; + +/** A simple NSComparator for comparing NSString instances. */ +extern const NSComparator FSTStringComparator; + +/** + * Compares the bitwise representation of two doubles, but normalizes NaN values. This is + * similar to what the backend and android clients do, including comparing -0.0 as not equal to 0.0. + */ +BOOL FSTDoubleBitwiseEquals(double left, double right); + +/** + * Computes a bitwise hash of a double, but normalizes NaN values, suitable for use when using + * FSTDoublesAreBitwiseEqual for equality. + */ +NSUInteger FSTDoubleBitwiseHash(double d); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTComparison.m b/Firestore/Source/Util/FSTComparison.m new file mode 100644 index 0000000..e4f4ccb --- /dev/null +++ b/Firestore/Source/Util/FSTComparison.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 "FSTComparison.h" + +NS_ASSUME_NONNULL_BEGIN + +union DoubleBits { + double d; + uint64_t bits; +}; + +const NSComparator FSTNumberComparator = ^NSComparisonResult(NSNumber *left, NSNumber *right) { + return [left compare:right]; +}; + +const NSComparator FSTStringComparator = ^NSComparisonResult(NSString *left, NSString *right) { + return FSTCompareStrings(left, right); +}; + +NSComparisonResult FSTCompareStrings(NSString *left, NSString *right) { + // NOTE: NSLiteralSearch is necessary to compare the raw character codes. By default, + // precomposed characters are considered equivalent to their decomposed equivalents. + return [left compare:right options:NSLiteralSearch]; +} + +NSComparisonResult FSTCompareBools(BOOL left, BOOL right) { + if (!left) { + return right ? NSOrderedAscending : NSOrderedSame; + } else { + return right ? NSOrderedSame : NSOrderedDescending; + } +} + +NSComparisonResult FSTCompareInts(int left, int right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareDoubles(double left, double right) { + // NaN sorts equal to itself and before any other number. + if (left < right) { + return NSOrderedAscending; + } else if (left > right) { + return NSOrderedDescending; + } else if (left == right) { + return NSOrderedSame; + } else { + // One or both left and right is NaN. + if (isnan(left)) { + return isnan(right) ? NSOrderedSame : NSOrderedAscending; + } else { + return NSOrderedDescending; + } + } +} + +static const double LONG_MIN_VALUE_AS_DOUBLE = (double)LLONG_MIN; +static const double LONG_MAX_VALUE_AS_DOUBLE = (double)LLONG_MAX; + +NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue) { + // LLONG_MIN has an exact representation as double, so to check for a value outside the range + // representable by long, we have to check for strictly less than LLONG_MIN. Note that this also + // handles negative infinity. + if (doubleValue < LONG_MIN_VALUE_AS_DOUBLE) { + return NSOrderedAscending; + } + + // LLONG_MAX has no exact representation as double (casting as we've done makes 2^63, which is + // larger than LLONG_MAX), so consider any value greater than or equal to the threshold to be out + // of range. This also handles positive infinity. + if (doubleValue >= LONG_MAX_VALUE_AS_DOUBLE) { + return NSOrderedDescending; + } + + // In Firestore NaN is defined to compare before all other numbers. + if (isnan(doubleValue)) { + return NSOrderedAscending; + } + + int64_t doubleAsLong = (int64_t)doubleValue; + NSComparisonResult cmp = FSTCompareInt64s(doubleAsLong, longValue); + if (cmp != NSOrderedSame) { + return cmp; + } + + // At this point the long representations are equal but this could be due to rounding. + double longAsDouble = (double)longValue; + return FSTCompareDoubles(doubleValue, longAsDouble); +} + +NSComparisonResult FSTCompareBytes(NSData *left, NSData *right) { + NSUInteger minLength = MIN(left.length, right.length); + int result = memcmp(left.bytes, right.bytes, minLength); + if (result < 0) { + return NSOrderedAscending; + } else if (result > 0) { + return NSOrderedDescending; + } else if (left.length < right.length) { + return NSOrderedAscending; + } else if (left.length > right.length) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } +} + +/** Helper to normalize a double and then return the raw bits as a uint64_t. */ +uint64_t FSTDoubleBits(double d) { + if (isnan(d)) { + d = NAN; + } + union DoubleBits converter = {.d = d}; + return converter.bits; +} + +BOOL FSTDoubleBitwiseEquals(double left, double right) { + return FSTDoubleBits(left) == FSTDoubleBits(right); +} + +NSUInteger FSTDoubleBitwiseHash(double d) { + uint64_t bits = FSTDoubleBits(d); + // Note that x ^ (x >> 32) works fine for both 32 and 64 bit definitions of NSUInteger + return (((NSUInteger)bits) ^ (NSUInteger)(bits >> 32)); +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTDispatchQueue.h b/Firestore/Source/Util/FSTDispatchQueue.h new file mode 100644 index 0000000..da6b3fe --- /dev/null +++ b/Firestore/Source/Util/FSTDispatchQueue.h @@ -0,0 +1,58 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDispatchQueue : NSObject + +/** Creates and returns an FSTDispatchQueue wrapping the specified dispatch_queue_t. */ ++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +/** + * Asserts that we are already running on this queue (actually, we can only verify that the + * queue's label is the same, but hopefully that's good enough.) + */ +- (void)verifyIsCurrentQueue; + +/** + * Same as dispatch_async() except it asserts that we're not already on the queue, since this + * generally indicates a bug (and can lead to re-ordering of operations, etc). + * + * @param block The block to run. + */ +- (void)dispatchAsync:(void (^)())block; + +/** + * Unlike dispatchAsync: this method does not require you to dispatch to a different queue than + * the current one (thus it is equivalent to a raw dispatch_async()). + * + * This is useful, e.g. for dispatching to the user's queue directly from user API call (in which + * case we don't know if we're already on the user's queue or not). + * + * @param block The block to run. + */ +- (void)dispatchAsyncAllowingSameQueue:(void (^)())block; + +/** The underlying wrapped dispatch_queue_t */ +@property(nonatomic, strong, readonly) dispatch_queue_t queue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTDispatchQueue.m b/Firestore/Source/Util/FSTDispatchQueue.m new file mode 100644 index 0000000..8d55d28 --- /dev/null +++ b/Firestore/Source/Util/FSTDispatchQueue.m @@ -0,0 +1,75 @@ +/* + * 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 + +#import "FSTAssert.h" +#import "FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDispatchQueue () +- (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTDispatchQueue + ++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { + return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue]; +} + +- (instancetype)initWithQueue:(dispatch_queue_t)queue { + if (self = [super init]) { + _queue = queue; + } + return self; +} + +- (void)verifyIsCurrentQueue { + FSTAssert([self onTargetQueue], + @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'", + [self targetQueueLabel], [self currentQueueLabel]); +} + +- (void)dispatchAsync:(void (^)())block { + FSTAssert(![self onTargetQueue], + @"dispatchAsync called when we are already running on target dispatch queue '%@'", + [self targetQueueLabel]); + + dispatch_async(self.queue, block); +} + +- (void)dispatchAsyncAllowingSameQueue:(void (^)())block { + dispatch_async(self.queue, block); +} + +#pragma mark - Private Methods + +- (NSString *)currentQueueLabel { + return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)]; +} + +- (NSString *)targetQueueLabel { + return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)]; +} + +- (BOOL)onTargetQueue { + return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.h b/Firestore/Source/Util/FSTLogger.h new file mode 100644 index 0000000..699570a --- /dev/null +++ b/Firestore/Source/Util/FSTLogger.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 + +NS_ASSUME_NONNULL_BEGIN + +#ifdef __cplusplus +extern "C" { +#endif + +/** Logs to NSLog if [FIRFirestore isLoggingEnabled] is YES. */ +void FSTLog(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); + +void FSTWarn(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); + +#ifdef __cplusplus +} // extern "C" +#endif + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.m b/Firestore/Source/Util/FSTLogger.m new file mode 100644 index 0000000..396c788 --- /dev/null +++ b/Firestore/Source/Util/FSTLogger.m @@ -0,0 +1,40 @@ +/* + * 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 "FSTLogger.h" + +#import "FIRFirestore+Internal.h" +#import "FIRLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +void FSTLog(NSString *format, ...) { + if ([FIRFirestore isLoggingEnabled]) { + va_list args; + va_start(args, format); + FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerFirestore, @"I-FST000001", format, args); + va_end(args); + } +} + +void FSTWarn(NSString *format, ...) { + va_list args; + va_start(args, format); + FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerFirestore, @"I-FST000001", format, args); + va_end(args); +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.h b/Firestore/Source/Util/FSTUsageValidation.h new file mode 100644 index 0000000..a80dafa --- /dev/null +++ b/Firestore/Source/Util/FSTUsageValidation.h @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#include + +NS_ASSUME_NONNULL_BEGIN + +/** Helper for creating a general exception for invalid usage of an API. */ +NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...); + +/** + * Macro to throw exceptions in response to API usage errors. Avoids the lint warning you usually + * get when using @throw and (unlike a function) doesn't trigger warnings about not all codepaths + * returning a value. + * + * Exceptions should only be used for programmer errors made by consumers of the SDK, e.g. + * invalid method arguments. + * + * For recoverable runtime errors, use NSError**. + * For internal programming errors, use FSTFail(). + */ +#define FSTThrowInvalidUsage(exceptionName, format, ...) \ + do { \ + @throw FSTInvalidUsage(exceptionName, format, ##__VA_ARGS__); \ + } while (0) + +#define FSTThrowInvalidArgument(format, ...) \ + do { \ + @throw FSTInvalidUsage(@"FIRInvalidArgumentException", format, ##__VA_ARGS__); \ + } while (0) + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.m b/Firestore/Source/Util/FSTUsageValidation.m new file mode 100644 index 0000000..82128f4 --- /dev/null +++ b/Firestore/Source/Util/FSTUsageValidation.m @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#include + +NS_ASSUME_NONNULL_BEGIN + +NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...) { + va_list arg_list; + va_start(arg_list, format); + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + va_end(arg_list); + + return [[NSException alloc] initWithName:exceptionName reason:formattedString userInfo:nil]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUtil.h b/Firestore/Source/Util/FSTUtil.h new file mode 100644 index 0000000..3985d10 --- /dev/null +++ b/Firestore/Source/Util/FSTUtil.h @@ -0,0 +1,31 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTUtil : NSObject + +/** Generates a random double between 0 and 1. */ ++ (double)randomDouble; + +/** Generates a random ID suitable for use as a document ID. */ ++ (NSString *)autoID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUtil.m b/Firestore/Source/Util/FSTUtil.m new file mode 100644 index 0000000..d14c429 --- /dev/null +++ b/Firestore/Source/Util/FSTUtil.m @@ -0,0 +1,44 @@ +/* + * 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 "FSTUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +static const double kArc4RandomMax = 0x100000000; + +static const int kAutoIDLength = 20; +static NSString *const kAutoIDAlphabet = + @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +@implementation FSTUtil + ++ (double)randomDouble { + return ((double)arc4random() / kArc4RandomMax); +} + ++ (NSString *)autoID { + unichar autoID[kAutoIDLength]; + for (int i = 0; i < kAutoIDLength; i++) { + uint32_t randIndex = arc4random_uniform((uint32_t)kAutoIDAlphabet.length); + autoID[i] = [kAutoIDAlphabet characterAtIndex:randIndex]; + } + return [NSString stringWithCharacters:autoID length:kAutoIDLength]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/test.sh b/Firestore/test.sh new file mode 100755 index 0000000..7e26e3f --- /dev/null +++ b/Firestore/test.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# 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. + +set -euo pipefail + +FIRESTORE_DIR=$(dirname "${BASH_SOURCE[0]}") + +test_iOS() { + xcodebuild \ + -workspace "$FIRESTORE_DIR/Example/Firestore.xcworkspace" \ + -scheme Firestore_Tests \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 7' \ + build \ + test \ + ONLY_ACTIVE_ARCH=YES \ + CODE_SIGNING_REQUIRED=NO \ + | xcpretty + + xcodebuild \ + -workspace "$FIRESTORE_DIR/Example/Firestore.xcworkspace" \ + -scheme SwiftBuildTest \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 7' \ + build \ + ONLY_ACTIVE_ARCH=YES \ + CODE_SIGNING_REQUIRED=NO \ + | xcpretty +} + +test_iOS; RESULT=$? +if [[ $RESULT == 65 ]]; then + echo "xcodebuild exited with 65, retrying" + sleep 5 + + test_iOS; RESULT=$? +fi + +exit $RESULT diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionary.h b/Firestore/third_party/Immutable/FSTArraySortedDictionary.h new file mode 100644 index 0000000..4b78360 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTArraySortedDictionary.h @@ -0,0 +1,35 @@ +#import + +#import "FSTImmutableSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTArraySortedDictionary is an array backed implementation of FSTImmutableSortedDictionary. + * + * You should not use this class directly. You should use FSTImmutableSortedDictionary. + * + * FSTArraySortedDictionary uses arrays and linear lookups to achieve good memory efficiency while + * maintaining good performance for small collections. It also uses fewer allocations than a + * comparable red black tree. To avoid degrading performance with increasing collection size it + * will automatically convert to a FSTTreeSortedDictionary after an insert call above a certain + * threshold. + */ +@interface FSTArraySortedDictionary : + FSTImmutableSortedDictionary + ++ (FSTArraySortedDictionary *) + dictionaryWithDictionary:(NSDictionary *)dictionary + comparator:(NSComparator)comparator; + +- (id)init __attribute__((unavailable("Use initWithComparator:keys:values: instead."))); + +- (instancetype)initWithComparator:(NSComparator)comparator; + +- (instancetype)initWithComparator:(NSComparator)comparator + keys:(NSArray *)keys + values:(NSArray *)values NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionary.m b/Firestore/third_party/Immutable/FSTArraySortedDictionary.m new file mode 100644 index 0000000..fd3bfd7 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTArraySortedDictionary.m @@ -0,0 +1,242 @@ +#import "FSTArraySortedDictionary.h" + +#import "FSTArraySortedDictionaryEnumerator.h" +#import "FSTAssert.h" +#import "FSTTreeSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTArraySortedDictionary () +@property(nonatomic, copy, readwrite) NSComparator comparator; +@property(nonatomic, strong) NSArray *keys; +@property(nonatomic, strong) NSArray *values; +@end + +@implementation FSTArraySortedDictionary + ++ (FSTArraySortedDictionary *)dictionaryWithDictionary:(NSDictionary *)dictionary + comparator:(NSComparator)comparator { + NSMutableArray *keys = [NSMutableArray arrayWithCapacity:dictionary.count]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [keys addObject:key]; + }]; + [keys sortUsingComparator:comparator]; + + [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + if (idx > 0) { + if (comparator(keys[idx - 1], obj) != NSOrderedAscending) { + [NSException raise:NSInvalidArgumentException + format: + @"Can't create FSTImmutableSortedDictionary with keys " + @"with same ordering!"]; + } + } + }]; + + NSMutableArray *values = [NSMutableArray arrayWithCapacity:keys.count]; + NSInteger pos = 0; + for (id key in keys) { + values[pos++] = dictionary[key]; + } + FSTAssert(values.count == keys.count, @"We added as many keys as values"); + return [[FSTArraySortedDictionary alloc] initWithComparator:comparator keys:keys values:values]; +} + +- (id)initWithComparator:(NSComparator)comparator { + return [self initWithComparator:comparator keys:[NSArray array] values:[NSArray array]]; +} + +// Designated initializer. +- (id)initWithComparator:(NSComparator)comparator keys:(NSArray *)keys values:(NSArray *)values { + self = [super init]; + if (self != nil) { + FSTAssert(keys.count == values.count, @"keys and values must have the same count"); + _comparator = comparator; + _keys = keys; + _values = values; + } + return self; +} + +/** Returns the index of the first position where array[position] >= key. */ +- (int)findInsertPositionForKey:(id)key { + int newPos = 0; + while (newPos < self.keys.count && self.comparator(self.keys[newPos], key) < NSOrderedSame) { + newPos++; + } + return newPos; +} + +- (NSInteger)findKey:(id)key { + if (key == nil) { + return NSNotFound; + } + for (NSInteger pos = 0; pos < self.keys.count; pos++) { + NSComparisonResult result = self.comparator(key, self.keys[pos]); + if (result == NSOrderedSame) { + return pos; + } else if (result == NSOrderedAscending) { + return NSNotFound; + } + } + return NSNotFound; +} + +- (FSTImmutableSortedDictionary *)dictionaryBySettingObject:(id)value forKey:(id)key { + NSInteger pos = [self findKey:key]; + + if (pos == NSNotFound) { + /* + * If we're above the threshold we want to convert it to a tree backed implementation to not + * have degrading performance + */ + if (self.count >= kSortedDictionaryArrayToRBTreeSizeThreshold) { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:self.count]; + for (NSInteger i = 0; i < self.keys.count; i++) { + dict[self.keys[i]] = self.values[i]; + } + dict[key] = value; + return [FSTTreeSortedDictionary dictionaryWithDictionary:dict comparator:self.comparator]; + } else { + NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys]; + NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values]; + NSInteger newPos = [self findInsertPositionForKey:key]; + [newKeys insertObject:key atIndex:newPos]; + [newValues insertObject:value atIndex:newPos]; + return [[FSTArraySortedDictionary alloc] initWithComparator:self.comparator + keys:newKeys + values:newValues]; + } + } else { + NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys]; + NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values]; + newKeys[pos] = key; + newValues[pos] = value; + return [[FSTArraySortedDictionary alloc] initWithComparator:self.comparator + keys:newKeys + values:newValues]; + } +} + +- (FSTImmutableSortedDictionary *)dictionaryByRemovingObjectForKey:(id)key { + NSInteger pos = [self findKey:key]; + if (pos == NSNotFound) { + return self; + } else { + NSMutableArray *newKeys = [NSMutableArray arrayWithArray:self.keys]; + NSMutableArray *newValues = [NSMutableArray arrayWithArray:self.values]; + [newKeys removeObjectAtIndex:pos]; + [newValues removeObjectAtIndex:pos]; + return [[FSTArraySortedDictionary alloc] initWithComparator:self.comparator + keys:newKeys + values:newValues]; + } +} + +- (nullable id)objectForKey:(id)key { + NSInteger pos = [self findKey:key]; + if (pos == NSNotFound) { + return nil; + } else { + return self.values[pos]; + } +} + +- (nullable id)predecessorKey:(id)key { + NSInteger pos = [self findKey:key]; + if (pos == NSNotFound) { + [NSException raise:NSInternalInconsistencyException + format:@"Can't get predecessor key for non-existent key"]; + return nil; + } else if (pos == 0) { + return nil; + } else { + return self.keys[pos - 1]; + } +} + +- (NSUInteger)indexOfKey:(id)key { + return [self findKey:key]; +} + +- (BOOL)isEmpty { + return self.keys.count == 0; +} + +- (NSUInteger)count { + return self.keys.count; +} + +- (id)minKey { + return [self.keys firstObject]; +} + +- (id)maxKey { + return [self.keys lastObject]; +} + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block { + [self enumerateKeysAndObjectsReverse:NO usingBlock:block]; +} + +- (void)enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block { + if (reverse) { + BOOL stop = NO; + for (NSInteger i = self.keys.count - 1; i >= 0; i--) { + block(self.keys[i], self.values[i], &stop); + if (stop) return; + } + } else { + BOOL stop = NO; + for (NSInteger i = 0; i < self.keys.count; i++) { + block(self.keys[i], self.values[i], &stop); + if (stop) return; + } + } +} + +- (BOOL)containsKey:(id)key { + return [self findKey:key] != NSNotFound; +} + +- (NSEnumerator *)keyEnumerator { + return [self.keys objectEnumerator]; +} + +- (NSEnumerator *)keyEnumeratorFrom:(id)startKey { + return [self keyEnumeratorFrom:startKey to:nil]; +} + +- (NSEnumerator *)keyEnumeratorFrom:(id)startKey to:(nullable id)endKey { + int start = [self findInsertPositionForKey:startKey]; + int end = (int)self.count; + if (endKey) { + end = [self findInsertPositionForKey:endKey]; + } + return [[FSTArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys + startPos:start + endPos:end + isReverse:NO]; +} + +- (NSEnumerator *)reverseKeyEnumerator { + return [self.keys reverseObjectEnumerator]; +} + +- (NSEnumerator *)reverseKeyEnumeratorFrom:(id)startKey { + int startPos = [self findInsertPositionForKey:startKey]; + // if there's no exact match, findKeyOrInsertPosition will return the index *after* the closest + // match, but since this is a reverse iterator, we want to start just *before* the closest match. + if (startPos >= self.keys.count || + self.comparator(self.keys[startPos], startKey) != NSOrderedSame) { + startPos -= 1; + } + return [[FSTArraySortedDictionaryEnumerator alloc] initWithKeys:self.keys + startPos:startPos + endPos:-1 + isReverse:YES]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h new file mode 100644 index 0000000..36c4f32 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.h @@ -0,0 +1,26 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTArraySortedDictionaryEnumerator : NSEnumerator + +- (id)init __attribute__((unavailable("Use initWithKeys:startPos:endPos:isReverse: instead."))); + +/** + * An enumerator for use with a dictionary. + * + * @param keys The keys to enumerator within. + * @param start The index of the initial key to return. + * @param end If end is after (or equal to) start (or before, if reverse), then the enumerator will + * stop and not return the value once it reaches end. + */ +- (instancetype)initWithKeys:(NSArray *)keys + startPos:(int)start + endPos:(int)end + isReverse:(BOOL)reverse NS_DESIGNATED_INITIALIZER; + +- (_Nullable ValueType)nextObject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m new file mode 100644 index 0000000..e8fdfcc --- /dev/null +++ b/Firestore/third_party/Immutable/FSTArraySortedDictionaryEnumerator.m @@ -0,0 +1,54 @@ +#import "FSTArraySortedDictionaryEnumerator.h" + +NS_ASSUME_NONNULL_BEGIN + +// clang-format off +// For some reason, clang-format messes this line up... +@interface FSTArraySortedDictionaryEnumerator () +@property(nonatomic, assign) int pos; +@property(nonatomic, assign) int start; +@property(nonatomic, assign) int end; +@property(nonatomic, assign) BOOL reverse; +@property(nonatomic, strong) NSArray *keys; +@end +// clang-format on + +@implementation FSTArraySortedDictionaryEnumerator + +- (id)initWithKeys:(NSArray *)keys startPos:(int)start endPos:(int)end isReverse:(BOOL)reverse { + self = [super init]; + if (self != nil) { + _keys = keys; + _start = start; + _end = end; + _pos = start; + _reverse = reverse; + } + return self; +} + +- (nullable id)nextObject { + if (self.pos < 0 || self.pos >= self.keys.count) { + return nil; + } + if (self.reverse) { + if (self.pos <= self.end) { + return nil; + } + } else { + if (self.pos >= self.end) { + return nil; + } + } + int pos = self.pos; + if (self.reverse) { + self.pos--; + } else { + self.pos++; + } + return self.keys[pos]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h new file mode 100644 index 0000000..a2264ec --- /dev/null +++ b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h @@ -0,0 +1,120 @@ +/** + * Implementation of an immutable SortedMap using a Left-leaning Red-Black Tree, adapted from the + * implementation in Mugs (http://mads379.github.com/mugs/) by Mads Hartmann Jensen + * (mads379@gmail.com). + * + * Original paper on Left-leaning Red-Black Trees: + * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf + * + * Invariant 1: No red node has a red child + * Invariant 2: Every leaf path has the same number of black nodes + * Invariant 3: Only the left child can be red (left leaning) + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The size threshold where we use a tree backed sorted map instead of an array backed sorted map. + * This is a more or less arbitrary chosen value, that was chosen to be large enough to fit most of + * object kind of Firebase data, but small enough to not notice degradation in performance for + * inserting and lookups. Feel free to empirically determine this constant, but don't expect much + * gain in real world performance. + */ +extern const int kSortedDictionaryArrayToRBTreeSizeThreshold; + +/** + * FSTImmutableSortedDictionary is a dictionary. It is immutable, but has methods to create new + * dictionaries that are mutations of it, in an efficient way. + */ +@interface FSTImmutableSortedDictionary : NSObject + ++ (FSTImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator; ++ (FSTImmutableSortedDictionary *)dictionaryWithDictionary: + (NSDictionary *)dictionary + comparator:(NSComparator)comparator; + +/** + * Creates a new dictionary identical to this one, but with a key-value pair added or updated. + * + * @param aValue The value to associate with the key. + * @param aKey The key to insert/update. + * @return A new dictionary with the added/updated value. + */ +- (FSTImmutableSortedDictionary *)dictionaryBySettingObject:(ValueType)aValue + forKey:(KeyType)aKey; + +/** + * Creates a new dictionary identical to this one, but with a key removed from it. + * + * @param aKey The key to remove. + * @return A new dictionary without that value. + */ +- (FSTImmutableSortedDictionary *)dictionaryByRemovingObjectForKey: + (KeyType)aKey; + +/** + * Looks up a value in the dictionary. + * + * @param key The key to look up. + * @return The value for the key, if present. + */ +- (nullable ValueType)objectForKey:(KeyType)key; + +/** + * Looks up a value in the dictionary. + * + * @param key The key to look up. + * @return The value for the key, if present. + */ +- (ValueType)objectForKeyedSubscript:(KeyType)key; + +/** + * Gets the key before the given key in sorted order. + * + * @param key The key to look before. + * @return The key before the given one. + */ +- (nullable KeyType)predecessorKey:(KeyType)key; + +/** + * Returns the index of the key or NSNotFound if the key is not found. + * + * @param key The key to return the index for. + * @return The index of the key, or NSNotFound if key not found. + */ +- (NSUInteger)indexOfKey:(KeyType)key; + +/** Returns true if the dictionary contains no elements. */ +- (BOOL)isEmpty; + +/** Returns the number of items in this dictionary. */ +- (NSUInteger)count; + +/** Returns the smallest key in this dictionary. */ +- (KeyType)minKey; + +/** Returns the largest key in this dictionary. */ +- (KeyType)maxKey; + +/** Calls the given block with each of the items in this dictionary, in order. */ +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ValueType value, BOOL *stop))block; + +/** Calls the given block with each of the items in this dictionary, in reverse order. */ +- (void)enumerateKeysAndObjectsReverse:(BOOL)reverse + usingBlock:(void (^)(KeyType key, ValueType value, BOOL *stop))block; + +/** Returns true if the dictionary contains the given key. */ +- (BOOL)containsKey:(KeyType)key; + +- (NSEnumerator *)keyEnumerator; +- (NSEnumerator *)keyEnumeratorFrom:(KeyType)startKey; +/** Enumerator for the range [startKey, endKey). */ +- (NSEnumerator *)keyEnumeratorFrom:(KeyType)startKey to:(nullable KeyType)endKey; +- (NSEnumerator *)reverseKeyEnumerator; +- (NSEnumerator *)reverseKeyEnumeratorFrom:(KeyType)startKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m new file mode 100644 index 0000000..a858529 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTImmutableSortedDictionary.m @@ -0,0 +1,143 @@ +#import "FSTImmutableSortedDictionary.h" + +#import "FSTArraySortedDictionary.h" +#import "FSTClasses.h" +#import "FSTTreeSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +const int kSortedDictionaryArrayToRBTreeSizeThreshold = 25; + +@implementation FSTImmutableSortedDictionary + ++ (FSTImmutableSortedDictionary *)dictionaryWithComparator:(NSComparator)comparator { + return [[FSTArraySortedDictionary alloc] initWithComparator:comparator]; +} + ++ (FSTImmutableSortedDictionary *)dictionaryWithDictionary:(NSDictionary *)dictionary + comparator:(NSComparator)comparator { + if (dictionary.count <= kSortedDictionaryArrayToRBTreeSizeThreshold) { + return [FSTArraySortedDictionary dictionaryWithDictionary:dictionary comparator:comparator]; + } else { + return [FSTTreeSortedDictionary dictionaryWithDictionary:dictionary comparator:comparator]; + } +} + +- (FSTImmutableSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (FSTImmutableSortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[FSTImmutableSortedDictionary class]]) { + return NO; + } + + // TODO(klimt): We could make this more efficient if we put the comparison inside the + // implementations and short-circuit if they share the same tree node, for instance. + FSTImmutableSortedDictionary *other = (FSTImmutableSortedDictionary *)object; + if (self.count != other.count) { + return NO; + } + __block BOOL isEqual = YES; + [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + id otherValue = [other objectForKey:key]; + isEqual = isEqual && (value == otherValue || [value isEqual:otherValue]); + *stop = !isEqual; + }]; + return isEqual; +} + +- (NSUInteger)hash { + __block NSUInteger hash = 0; + [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + hash = (hash * 31 + [key hash]) * 17 + [value hash]; + }]; + return hash; +} + +- (NSString *)description { + NSMutableString *str = [[NSMutableString alloc] init]; + __block BOOL first = YES; + [str appendString:@"{ "]; + [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + if (!first) { + [str appendString:@", "]; + } + first = NO; + [str appendString:[NSString stringWithFormat:@"%@: %@", key, value]]; + }]; + [str appendString:@" }"]; + return str; +} + +- (nullable id)objectForKey:(id)key { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (id)objectForKeyedSubscript:(id)key { + return [self objectForKey:key]; +} + +- (nullable id)predecessorKey:(id)key { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSUInteger)indexOfKey:(id)key { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (BOOL)isEmpty { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSUInteger)count { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (id)minKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (id)maxKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (BOOL)containsKey:(id)key { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSEnumerator *)keyEnumerator { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSEnumerator *)keyEnumeratorFrom:(id)startKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSEnumerator *)keyEnumeratorFrom:(id)startKey to:(nullable id)endKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSEnumerator *)reverseKeyEnumerator { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSEnumerator *)reverseKeyEnumeratorFrom:(id)startKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedSet.h b/Firestore/third_party/Immutable/FSTImmutableSortedSet.h new file mode 100644 index 0000000..d0f9906 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTImmutableSortedSet.h @@ -0,0 +1,47 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTImmutableSortedSet is a set. It is immutable, but has methods to create new sets that are + * mutations of it, in an efficient way. + */ +@interface FSTImmutableSortedSet : NSObject + ++ (FSTImmutableSortedSet *)setWithComparator:(NSComparator)comparator; + ++ (FSTImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)array + comparator:(NSComparator)comparator; + +- (BOOL)containsObject:(KeyType)object; + +- (FSTImmutableSortedSet *)setByAddingObject:(KeyType)object; +- (FSTImmutableSortedSet *)setByRemovingObject:(KeyType)object; + +- (KeyType)firstObject; +- (KeyType)lastObject; +- (NSUInteger)count; +- (BOOL)isEmpty; + +- (KeyType)predecessorObject:(KeyType)entry; + +/** + * Returns the index of the object or NSNotFound if the object is not found. + * + * @param object The object to return the index for. + * @return The index of the object, or NSNotFound if not found. + */ +- (NSUInteger)indexOfObject:(KeyType)object; + +- (void)enumerateObjectsUsingBlock:(void (^)(KeyType obj, BOOL *stop))block; +- (void)enumerateObjectsFrom:(KeyType)start + to:(_Nullable KeyType)end + usingBlock:(void (^)(KeyType obj, BOOL *stop))block; +- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(KeyType obj, BOOL *stop))block; + +- (NSEnumerator *)objectEnumerator; +- (NSEnumerator *)objectEnumeratorFrom:(KeyType)startKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTImmutableSortedSet.m b/Firestore/third_party/Immutable/FSTImmutableSortedSet.m new file mode 100644 index 0000000..a13a79e --- /dev/null +++ b/Firestore/third_party/Immutable/FSTImmutableSortedSet.m @@ -0,0 +1,144 @@ +#import "FSTImmutableSortedSet.h" + +#import "FSTImmutableSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTImmutableSortedSet () +@property(nonatomic, strong) FSTImmutableSortedDictionary *dictionary; +@end + +@implementation FSTImmutableSortedSet + ++ (FSTImmutableSortedSet *)setWithComparator:(NSComparator)comparator { + return [FSTImmutableSortedSet setWithKeysFromDictionary:@{} comparator:comparator]; +} + ++ (FSTImmutableSortedSet *)setWithKeysFromDictionary:(NSDictionary *)dictionary + comparator:(NSComparator)comparator { + FSTImmutableSortedDictionary *setDict = + [FSTImmutableSortedDictionary dictionaryWithDictionary:dictionary comparator:comparator]; + return [[FSTImmutableSortedSet alloc] initWithDictionary:setDict]; +} + +// Designated initializer. +- (id)initWithDictionary:(FSTImmutableSortedDictionary *)dictionary { + self = [super init]; + if (self != nil) { + _dictionary = dictionary; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[FSTImmutableSortedSet class]]) { + return NO; + } + + FSTImmutableSortedSet *other = (FSTImmutableSortedSet *)object; + + return [self.dictionary isEqual:other.dictionary]; +} + +- (NSUInteger)hash { + return [self.dictionary hash]; +} + +- (BOOL)containsObject:(id)object { + return [self.dictionary containsKey:object]; +} + +- (FSTImmutableSortedSet *)setByAddingObject:(id)object { + FSTImmutableSortedDictionary *newDictionary = + [self.dictionary dictionaryBySettingObject:[NSNull null] forKey:object]; + if (newDictionary != self.dictionary) { + return [[FSTImmutableSortedSet alloc] initWithDictionary:newDictionary]; + } else { + return self; + } +} + +- (FSTImmutableSortedSet *)setByRemovingObject:(id)object { + FSTImmutableSortedDictionary *newDictionary = + [self.dictionary dictionaryByRemovingObjectForKey:object]; + if (newDictionary != self.dictionary) { + return [[FSTImmutableSortedSet alloc] initWithDictionary:newDictionary]; + } else { + return self; + } +} + +- (id)firstObject { + return [self.dictionary minKey]; +} + +- (id)lastObject { + return [self.dictionary maxKey]; +} + +- (id)predecessorObject:(id)entry { + return [self.dictionary predecessorKey:entry]; +} + +- (NSUInteger)indexOfObject:(id)object { + return [self.dictionary indexOfKey:object]; +} + +- (NSUInteger)count { + return [self.dictionary count]; +} + +- (BOOL)isEmpty { + return [self.dictionary isEmpty]; +} + +- (void)enumerateObjectsUsingBlock:(void (^)(id, BOOL *))block { + [self enumerateObjectsReverse:NO usingBlock:block]; +} + +- (void)enumerateObjectsFrom:(id)start to:(_Nullable id)end usingBlock:(void (^)(id, BOOL *))block { + NSEnumerator *enumerator = [self.dictionary keyEnumeratorFrom:start to:end]; + id item = [enumerator nextObject]; + while (item) { + BOOL stop = NO; + block(item, &stop); + if (stop) { + return; + } + item = [enumerator nextObject]; + } +} + +- (void)enumerateObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, BOOL *))block { + [self.dictionary enumerateKeysAndObjectsReverse:reverse + usingBlock:^(id key, id value, BOOL *stop) { + block(key, stop); + }]; +} + +- (NSEnumerator *)objectEnumerator { + return [self.dictionary keyEnumerator]; +} + +- (NSEnumerator *)objectEnumeratorFrom:(id)startKey { + return [self.dictionary keyEnumeratorFrom:startKey]; +} + +- (NSString *)description { + NSMutableString *str = [[NSMutableString alloc] init]; + __block BOOL first = YES; + [str appendString:@"FSTImmutableSortedSet ( "]; + [self enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { + if (!first) { + [str appendString:@", "]; + } + first = NO; + [str appendString:[NSString stringWithFormat:@"%@", obj]]; + }]; + [str appendString:@" )"]; + return str; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTLLRBEmptyNode.h b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.h new file mode 100644 index 0000000..4c3c2af --- /dev/null +++ b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.h @@ -0,0 +1,11 @@ +#import + +#import "FSTLLRBNode.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLLRBEmptyNode : NSObject ++ (instancetype)emptyNode; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTLLRBEmptyNode.m b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.m new file mode 100644 index 0000000..ec49c2c --- /dev/null +++ b/Firestore/third_party/Immutable/FSTLLRBEmptyNode.m @@ -0,0 +1,102 @@ +#import "FSTLLRBEmptyNode.h" + +#import "FSTLLRBValueNode.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTLLRBEmptyNode + +- (NSString *)description { + return @"[empty node]"; +} + ++ (instancetype)emptyNode { + static dispatch_once_t pred = 0; + __strong static id _sharedObject = nil; + dispatch_once(&pred, ^{ + _sharedObject = [[self alloc] init]; // or some other init method + }); + return _sharedObject; +} + +- (nullable id)key { + return nil; +} + +- (nullable id)value { + return nil; +} + +- (FSTLLRBColor)color { + return FSTLLRBColorUnspecified; +} + +- (nullable id)left { + return nil; +} + +- (nullable id)right { + return nil; +} + +- (instancetype)copyWith:(id _Nullable)aKey + withValue:(id _Nullable)aValue + withColor:(FSTLLRBColor)aColor + withLeft:(id _Nullable)aLeft + withRight:(id _Nullable)aRight { + // This class is a singleton anyway, so this is more efficient than calling the constructor again. + return self; +} + +- (id)insertKey:(id)aKey forValue:(id)aValue withComparator:(NSComparator)aComparator { + FSTLLRBValueNode *result = [[FSTLLRBValueNode alloc] initWithKey:aKey + withValue:aValue + withColor:FSTLLRBColorUnspecified + withLeft:nil + withRight:nil]; + return result; +} + +- (id)remove:(id)key withComparator:(NSComparator)aComparator { + return self; +} + +- (NSUInteger)count { + return 0; +} + +- (BOOL)isEmpty { + return YES; +} + +- (BOOL)inorderTraversal:(BOOL (^)(id key, id value))action { + return NO; +} + +- (BOOL)reverseTraversal:(BOOL (^)(id key, id value))action { + return NO; +} + +- (id)min { + return self; +} + +- (nullable id)minKey { + return nil; +} + +- (nullable id)maxKey { + return nil; +} + +- (BOOL)isRed { + return NO; +} + +- (int)check { + return 0; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTLLRBNode.h b/Firestore/third_party/Immutable/FSTLLRBNode.h new file mode 100644 index 0000000..082b875 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTLLRBNode.h @@ -0,0 +1,68 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A FSTLLRBColor is the color of a tree node. It can be RED, BLACK, or unset. + */ +typedef NS_ENUM(NSInteger, FSTLLRBColor) { + FSTLLRBColorUnspecified = 0, + FSTLLRBColorRed = 1, + FSTLLRBColorBlack = 2, +}; + +/** + * FSTLLRBNode is the interface for a node in a FSTTreeSortedDictionary. + */ +@protocol FSTLLRBNode + +/** + * Creates a copy of the given node, changing any values that were specified. + * For any parameter that is left as nil, this instance's value will be used. + */ +- (instancetype)copyWith:(nullable id)aKey + withValue:(nullable id)aValue + withColor:(FSTLLRBColor)aColor + withLeft:(nullable id)aLeft + withRight:(nullable id)aRight; + +/** Returns a tree node with the given key-value pair set/updated. */ +- (id)insertKey:(id)aKey forValue:(id)aValue withComparator:(NSComparator)aComparator; + +/** Returns a tree node with the given key removed. */ +- (id)remove:(id)key withComparator:(NSComparator)aComparator; + +/** Returns the number of elements at this node or beneath it in the tree. */ +- (NSUInteger)count; + +/** Returns true if this is an FSTLLRBEmptyNode -- a leaf node in the tree. */ +- (BOOL)isEmpty; + +- (BOOL)inorderTraversal:(BOOL (^)(id key, id value))action; +- (BOOL)reverseTraversal:(BOOL (^)(id key, id value))action; + +/** Returns the left-most node under (or including) this node. */ +- (id)min; + +/** Returns the key of the left-most node under (or including) this node. */ +- (nullable id)minKey; + +/** Returns the key of the right-most node under (or including) this node. */ +- (nullable id)maxKey; + +/** Returns true if this node is red (as opposed to black). */ +- (BOOL)isRed; + +/** Checks that this node and below it hold the red-black invariants. Throws otherwise. */ +- (int)check; + +// Accessors for properties. +- (nullable id)key; +- (nullable id)value; +- (FSTLLRBColor)color; +- (nullable id)left; +- (nullable id)right; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTLLRBValueNode.h b/Firestore/third_party/Immutable/FSTLLRBValueNode.h new file mode 100644 index 0000000..4a0873c --- /dev/null +++ b/Firestore/third_party/Immutable/FSTLLRBValueNode.h @@ -0,0 +1,29 @@ +#import + +#import "FSTLLRBNode.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLLRBValueNode : NSObject + +- (id)init __attribute__(( + unavailable("Use initWithKey:withValue:withColor:withLeft:withRight: instead."))); + +- (instancetype)initWithKey:(nullable id)key + withValue:(nullable id)value + withColor:(FSTLLRBColor)color + withLeft:(nullable id)left + withRight:(nullable id)right NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, assign, readonly) FSTLLRBColor color; +@property(nonatomic, strong, readonly, nullable) id key; +@property(nonatomic, strong, readonly, nullable) id value; +@property(nonatomic, strong, readonly, nullable) id right; + +// This property cannot be readonly, because it is set when building the tree. +// TODO(klimt): Find a way to build the tree without mutating this. +@property(nonatomic, strong, nullable) id left; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTLLRBValueNode.m b/Firestore/third_party/Immutable/FSTLLRBValueNode.m new file mode 100644 index 0000000..d048012 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTLLRBValueNode.m @@ -0,0 +1,308 @@ +#import "FSTLLRBValueNode.h" + +#import "FSTAssert.h" +#import "FSTLLRBEmptyNode.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLLRBValueNode () +@property(nonatomic, assign) FSTLLRBColor color; +@property(nonatomic, assign) NSUInteger count; +@property(nonatomic, strong) id key; +@property(nonatomic, strong) id value; +@property(nonatomic, strong) id right; +@end + +@implementation FSTLLRBValueNode + +- (NSString *)colorDescription { + NSString *color = @"unspecified"; + if (self.color == FSTLLRBColorRed) { + color = @"red"; + } else if (self.color == FSTLLRBColorBlack) { + color = @"black"; + } + return color; +} + +- (NSString *)description { + NSString *color = self.colorDescription; + return [NSString stringWithFormat:@"[key=%@ val=%@ color=%@]", self.key, self.value, color]; +} + +// Designated initializer. +- (instancetype)initWithKey:(id _Nullable)aKey + withValue:(id _Nullable)aValue + withColor:(FSTLLRBColor)aColor + withLeft:(id _Nullable)aLeft + withRight:(id _Nullable)aRight { + self = [super init]; + if (self) { + _key = aKey; + _value = aValue; + _color = aColor != FSTLLRBColorUnspecified ? aColor : FSTLLRBColorRed; + _left = aLeft != nil ? aLeft : [FSTLLRBEmptyNode emptyNode]; + _right = aRight != nil ? aRight : [FSTLLRBEmptyNode emptyNode]; + _count = NSNotFound; + } + return self; +} + +- (instancetype)copyWith:(id _Nullable)aKey + withValue:(id _Nullable)aValue + withColor:(FSTLLRBColor)aColor + withLeft:(id _Nullable)aLeft + withRight:(id _Nullable)aRight { + return [[FSTLLRBValueNode alloc] + initWithKey:(aKey != nil) ? aKey : self.key + withValue:(aValue != nil) ? aValue : self.value + withColor:(aColor != FSTLLRBColorUnspecified) ? aColor : self.color + withLeft:(aLeft != nil) ? aLeft : self.left + withRight:(aRight != nil) ? aRight : self.right]; +} + +- (void)setLeft:(nullable id)left { + // Setting the left node should be only done by the builder, so doing it after someone has + // memoized count is an error. + FSTAssert(_count == NSNotFound, @"Can't update left node after using count"); + _left = left; +} + +- (NSUInteger)count { + if (_count == NSNotFound) { + _count = _left.count + 1 + _right.count; + } + return _count; +} + +- (BOOL)isEmpty { + return NO; +} + +/** + * Early terminates if action returns YES. + * + * @return The first truthy value returned by action, or the last falsey value returned by action. + */ +- (BOOL)inorderTraversal:(BOOL (^)(id key, id value))action { + return [self.left inorderTraversal:action] || action(self.key, self.value) || + [self.right inorderTraversal:action]; +} + +- (BOOL)reverseTraversal:(BOOL (^)(id key, id value))action { + return [self.right reverseTraversal:action] || action(self.key, self.value) || + [self.left reverseTraversal:action]; +} + +- (id)min { + if ([self.left isEmpty]) { + return self; + } else { + return [self.left min]; + } +} + +- (nullable id)minKey { + return [[self min] key]; +} + +- (nullable id)maxKey { + if ([self.right isEmpty]) { + return self.key; + } else { + return [self.right maxKey]; + } +} + +- (id)insertKey:(id)aKey forValue:(id)aValue withComparator:(NSComparator)aComparator { + NSComparisonResult cmp = aComparator(aKey, self.key); + FSTLLRBValueNode *n = self; + + if (cmp == NSOrderedAscending) { + n = [n copyWith:nil + withValue:nil + withColor:FSTLLRBColorUnspecified + withLeft:[n.left insertKey:aKey forValue:aValue withComparator:aComparator] + withRight:nil]; + } else if (cmp == NSOrderedSame) { + n = [n copyWith:nil + withValue:aValue + withColor:FSTLLRBColorUnspecified + withLeft:nil + withRight:nil]; + } else { + n = [n copyWith:nil + withValue:nil + withColor:FSTLLRBColorUnspecified + withLeft:nil + withRight:[n.right insertKey:aKey forValue:aValue withComparator:aComparator]]; + } + + return [n fixUp]; +} + +- (id)removeMin { + if ([self.left isEmpty]) { + return [FSTLLRBEmptyNode emptyNode]; + } + + FSTLLRBValueNode *n = self; + if (![n.left isRed] && ![n.left.left isRed]) { + n = [n moveRedLeft]; + } + + n = [n copyWith:nil + withValue:nil + withColor:FSTLLRBColorUnspecified + withLeft:[(FSTLLRBValueNode *)n.left removeMin] + withRight:nil]; + return [n fixUp]; +} + +- (id)fixUp { + FSTLLRBValueNode *n = self; + if ([n.right isRed] && ![n.left isRed]) n = [n rotateLeft]; + if ([n.left isRed] && [n.left.left isRed]) n = [n rotateRight]; + if ([n.left isRed] && [n.right isRed]) n = [n colorFlip]; + return n; +} + +- (FSTLLRBValueNode *)moveRedLeft { + FSTLLRBValueNode *n = [self colorFlip]; + if ([n.right.left isRed]) { + n = [n copyWith:nil + withValue:nil + withColor:FSTLLRBColorUnspecified + withLeft:nil + withRight:[(FSTLLRBValueNode *)n.right rotateRight]]; + n = [n rotateLeft]; + n = [n colorFlip]; + } + return n; +} + +- (FSTLLRBValueNode *)moveRedRight { + FSTLLRBValueNode *n = [self colorFlip]; + if ([n.left.left isRed]) { + n = [n rotateRight]; + n = [n colorFlip]; + } + return n; +} + +- (id)rotateLeft { + id nl = [self copyWith:nil + withValue:nil + withColor:FSTLLRBColorRed + withLeft:nil + withRight:self.right.left]; + return [self.right copyWith:nil withValue:nil withColor:self.color withLeft:nl withRight:nil]; +} + +- (id)rotateRight { + id nr = [self copyWith:nil + withValue:nil + withColor:FSTLLRBColorRed + withLeft:self.left.right + withRight:nil]; + return [self.left copyWith:nil withValue:nil withColor:self.color withLeft:nil withRight:nr]; +} + +- (id)colorFlip { + FSTLLRBColor color = self.color == FSTLLRBColorBlack ? FSTLLRBColorRed : FSTLLRBColorBlack; + FSTLLRBColor leftColor = + self.left.color == FSTLLRBColorBlack ? FSTLLRBColorRed : FSTLLRBColorBlack; + FSTLLRBColor rightColor = + self.right.color == FSTLLRBColorBlack ? FSTLLRBColorRed : FSTLLRBColorBlack; + + id nleft = + [self.left copyWith:nil withValue:nil withColor:leftColor withLeft:nil withRight:nil]; + id nright = + [self.right copyWith:nil withValue:nil withColor:rightColor withLeft:nil withRight:nil]; + + return [self copyWith:nil withValue:nil withColor:color withLeft:nleft withRight:nright]; +} + +- (id)remove:(id)aKey withComparator:(NSComparator)comparator { + id smallest; + FSTLLRBValueNode *n = self; + + if (comparator(aKey, n.key) == NSOrderedAscending) { + if (![n.left isEmpty] && ![n.left isRed] && ![n.left.left isRed]) { + n = [n moveRedLeft]; + } + n = [n copyWith:nil + withValue:nil + withColor:FSTLLRBColorUnspecified + withLeft:[n.left remove:aKey withComparator:comparator] + withRight:nil]; + } else { + if ([n.left isRed]) { + n = [n rotateRight]; + } + + if (![n.right isEmpty] && ![n.right isRed] && ![n.right.left isRed]) { + n = [n moveRedRight]; + } + + if (comparator(aKey, n.key) == NSOrderedSame) { + if ([n.right isEmpty]) { + return [FSTLLRBEmptyNode emptyNode]; + } else { + smallest = [n.right min]; + n = [n copyWith:smallest.key + withValue:smallest.value + withColor:FSTLLRBColorUnspecified + withLeft:nil + withRight:[(FSTLLRBValueNode *)n.right removeMin]]; + } + } + n = [n copyWith:nil + withValue:nil + withColor:FSTLLRBColorUnspecified + withLeft:nil + withRight:[n.right remove:aKey withComparator:comparator]]; + } + return [n fixUp]; +} + +- (BOOL)isRed { + return self.color == FSTLLRBColorRed; +} + +- (BOOL)checkMaxDepth { + int blackDepth = [self check]; + if (pow(2.0, blackDepth) <= ([self count] + 1)) { + return YES; + } else { + return NO; + } +} + +- (int)check { + int blackDepth = 0; + + if ([self isRed] && [self.left isRed]) { + @throw + [[NSException alloc] initWithName:@"check" reason:@"Red node has a red child" userInfo:nil]; + } + + if ([self.right isRed]) { + @throw [[NSException alloc] initWithName:@"check" reason:@"Right child is red" userInfo:nil]; + } + + blackDepth = [self.left check]; + if (blackDepth != [self.right check]) { + NSString *err = + [NSString stringWithFormat:@"(%@ -> %@)blackDepth: %d ; self.right check: %d", self.value, + self.colorDescription, blackDepth, [self.right check]]; + @throw [[NSException alloc] initWithName:@"check" reason:err userInfo:nil]; + } else { + int ret = blackDepth + ([self isRed] ? 0 : 1); + return ret; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionary.h b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.h new file mode 100644 index 0000000..6c26e5f --- /dev/null +++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.h @@ -0,0 +1,41 @@ +/** + * Implementation of an immutable SortedMap using a Left-leaning + * Red-Black Tree, adapted from the implementation in Mugs + * (http://mads379.github.com/mugs/) by Mads Hartmann Jensen + * (mads379@gmail.com). + * + * Original paper on Left-leaning Red-Black Trees: + * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf + * + * Invariant 1: No red node has a red child + * Invariant 2: Every leaf path has the same number of black nodes + * Invariant 3: Only the left child can be red (left leaning) + */ + +#import + +#import "FSTImmutableSortedDictionary.h" +#import "FSTLLRBNode.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTTreeSortedDictionary is a tree-based implementation of FSTImmutableSortedDictionary. + * You should not use this class directly. You should use FSTImmutableSortedDictionary. + */ +@interface FSTTreeSortedDictionary : + FSTImmutableSortedDictionary + +@property(nonatomic, copy, readonly) NSComparator comparator; +@property(nonatomic, strong, readonly) id root; + +- (id)init __attribute__((unavailable("Use initWithComparator:withRoot: instead."))); + +- (instancetype)initWithComparator:(NSComparator)aComparator; + +- (instancetype)initWithComparator:(NSComparator)aComparator + withRoot:(id)aRoot NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionary.m b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.m new file mode 100644 index 0000000..4059951 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionary.m @@ -0,0 +1,382 @@ +#import "FSTTreeSortedDictionary.h" + +#import "FSTLLRBEmptyNode.h" +#import "FSTLLRBValueNode.h" +#import "FSTTreeSortedDictionaryEnumerator.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTreeSortedDictionary () + +- (FSTTreeSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey; + +@property(nonatomic, strong) id root; +@property(nonatomic, copy, readwrite) NSComparator comparator; +@end + +@implementation FSTTreeSortedDictionary + ++ (FSTTreeSortedDictionary *)dictionaryWithDictionary:(NSDictionary *)dictionary + comparator:(NSComparator)comparator { + __block FSTTreeSortedDictionary *dict = + [[FSTTreeSortedDictionary alloc] initWithComparator:comparator]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + dict = [dict dictionaryBySettingObject:obj forKey:key]; + }]; + return dict; +} + +- (id)initWithComparator:(NSComparator)aComparator { + return [self initWithComparator:aComparator withRoot:[FSTLLRBEmptyNode emptyNode]]; +} + +// Designated initializer. +- (id)initWithComparator:(NSComparator)aComparator withRoot:(id)aRoot { + self = [super init]; + if (self) { + self.root = aRoot; + self.comparator = aComparator; + } + return self; +} + +/** + * Returns a copy of the map, with the specified key/value added or replaced. + */ +- (FSTTreeSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey { + return [[FSTTreeSortedDictionary alloc] + initWithComparator:self.comparator + withRoot:[[self.root insertKey:aKey forValue:aValue withComparator:self.comparator] + copyWith:nil + withValue:nil + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil]]; +} + +- (FSTTreeSortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey { + // Remove is somewhat expensive even if the key doesn't exist (the tree does rebalancing and + // stuff). So avoid it. + if (![self containsKey:aKey]) { + return self; + } else { + return [[FSTTreeSortedDictionary alloc] + initWithComparator:self.comparator + withRoot:[[self.root remove:aKey withComparator:self.comparator] + copyWith:nil + withValue:nil + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil]]; + } +} + +- (nullable id)objectForKey:(id)key { + NSComparisonResult cmp; + id node = self.root; + while (![node isEmpty]) { + cmp = self.comparator(key, node.key); + if (cmp == NSOrderedSame) { + return node.value; + } else if (cmp == NSOrderedAscending) { + node = node.left; + } else { + node = node.right; + } + } + return nil; +} + +- (nullable id)predecessorKey:(id)key { + NSComparisonResult cmp; + id node = self.root; + id rightParent = nil; + while (![node isEmpty]) { + cmp = self.comparator(key, node.key); + if (cmp == NSOrderedSame) { + if (![node.left isEmpty]) { + node = node.left; + while (![node.right isEmpty]) { + node = node.right; + } + return node.key; + } else if (rightParent != nil) { + return rightParent.key; + } else { + return nil; + } + } else if (cmp == NSOrderedAscending) { + node = node.left; + } else if (cmp == NSOrderedDescending) { + rightParent = node; + node = node.right; + } + } + @throw [NSException exceptionWithName:@"NonexistentKey" + reason:@"getPredecessorKey called with nonexistent key." + userInfo:@{@"key" : [key description]}]; +} + +- (NSUInteger)indexOfKey:(id)key { + NSUInteger prunedNodes = 0; + id node = self.root; + while (![node isEmpty]) { + NSComparisonResult cmp = self.comparator(key, node.key); + if (cmp == NSOrderedSame) { + return prunedNodes + node.left.count; + } else if (cmp == NSOrderedAscending) { + node = node.left; + } else if (cmp == NSOrderedDescending) { + prunedNodes += node.left.count + 1; + node = node.right; + } + } + return NSNotFound; +} + +- (BOOL)isEmpty { + return [self.root isEmpty]; +} + +- (NSUInteger)count { + return [self.root count]; +} + +- (id)minKey { + return [self.root minKey]; +} + +- (id)maxKey { + return [self.root maxKey]; +} + +- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id, id, BOOL *))block { + [self enumerateKeysAndObjectsReverse:NO usingBlock:block]; +} + +- (void)enumerateKeysAndObjectsReverse:(BOOL)reverse usingBlock:(void (^)(id, id, BOOL *))block { + if (reverse) { + __block BOOL stop = NO; + [self.root reverseTraversal:^BOOL(id key, id value) { + block(key, value, &stop); + return stop; + }]; + } else { + __block BOOL stop = NO; + [self.root inorderTraversal:^BOOL(id key, id value) { + block(key, value, &stop); + return stop; + }]; + } +} + +- (BOOL)containsKey:(id)key { + return ([self objectForKey:key] != nil); +} + +- (NSEnumerator *)keyEnumerator { + return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self + startKey:nil + endKey:nil + isReverse:NO]; +} + +- (NSEnumerator *)keyEnumeratorFrom:(id)startKey { + return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self + startKey:startKey + endKey:nil + isReverse:NO]; +} + +- (NSEnumerator *)keyEnumeratorFrom:(id)startKey to:(nullable id)endKey { + return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self + startKey:startKey + endKey:endKey + isReverse:NO]; +} + +- (NSEnumerator *)reverseKeyEnumerator { + return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self + startKey:nil + endKey:nil + isReverse:YES]; +} + +- (NSEnumerator *)reverseKeyEnumeratorFrom:(id)startKey { + return [[FSTTreeSortedDictionaryEnumerator alloc] initWithImmutableSortedDictionary:self + startKey:startKey + endKey:nil + isReverse:YES]; +} + +#pragma mark - +#pragma mark Tree Builder + +// Code to efficiently build a red black tree. + +typedef struct { + unsigned int bits; + unsigned short count; + unsigned short current; +} Base12List; + +unsigned int LogBase2(unsigned int num) { + return (unsigned int)(log(num) / log(2)); +} + +/** + * Works like an iterator, so it moves to the next bit. Do not call more than list->count times. + * @return whether or not the next bit is a 1 in base {1,2}. + */ +BOOL Base12ListNext(Base12List *list) { + BOOL result = !(list->bits & (0x1 << list->current)); + list->current--; + return result; +} + +static inline unsigned BitMask(int x) { + return (x >= sizeof(unsigned) * CHAR_BIT) ? (unsigned)-1 : (1U << x) - 1; +} + +/** + * We represent the base{1,2} number as the combination of a binary number and a number of bits that + * we care about. We iterate backwards, from most significant bit to least, to build up the llrb + * nodes. 0 base 2 => 1 base {1,2}, 1 base 2 => 2 base {1,2} + */ +Base12List *NewBase12List(unsigned int length) { + size_t sz = sizeof(Base12List); + Base12List *list = calloc(1, sz); + // Calculate the number of bits that we care about + list->count = (unsigned short)LogBase2(length + 1); + unsigned int mask = BitMask(list->count); + list->bits = (length + 1) & mask; + list->current = list->count - 1; + return list; +} + +void FreeBase12List(Base12List *list) { + free(list); +} + ++ (id)buildBalancedTree:(NSArray *)keys + dictionary:(NSDictionary *)dictionary + subArrayStartIndex:(NSUInteger)startIndex + length:(NSUInteger)length { + length = MIN(keys.count - startIndex, length); // Bound length by the actual length of the array + if (length == 0) { + return nil; + } else if (length == 1) { + id key = keys[startIndex]; + return [[FSTLLRBValueNode alloc] initWithKey:key + withValue:dictionary[key] + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil]; + } else { + NSUInteger middle = length / 2; + id left = [FSTTreeSortedDictionary buildBalancedTree:keys + dictionary:dictionary + subArrayStartIndex:startIndex + length:middle]; + id right = [FSTTreeSortedDictionary buildBalancedTree:keys + dictionary:dictionary + subArrayStartIndex:(startIndex + middle + 1) + length:middle]; + id key = keys[startIndex + middle]; + return [[FSTLLRBValueNode alloc] initWithKey:key + withValue:dictionary[key] + withColor:FSTLLRBColorBlack + withLeft:left + withRight:right]; + } +} + ++ (nullable id)rootFrom12List:(Base12List *)base12List + keyList:(NSArray *)keyList + dictionary:(NSDictionary *)dictionary { + __block FSTLLRBValueNode *root = nil; + __block FSTLLRBValueNode *node = nil; + __block NSUInteger index = keyList.count; + + void (^buildPennant)(FSTLLRBColor, NSUInteger) = ^(FSTLLRBColor color, NSUInteger chunkSize) { + NSUInteger startIndex = index - chunkSize + 1; + index -= chunkSize; + id key = keyList[index]; + FSTLLRBValueNode *childTree = [self buildBalancedTree:keyList + dictionary:dictionary + subArrayStartIndex:startIndex + length:(chunkSize - 1)]; + FSTLLRBValueNode *pennant = [[FSTLLRBValueNode alloc] initWithKey:key + withValue:dictionary[key] + withColor:color + withLeft:nil + withRight:childTree]; + if (node) { + // This is the only place this property is set. + node.left = pennant; + node = pennant; + } else { + root = pennant; + node = pennant; + } + }; + + for (int i = 0; i < base12List->count; ++i) { + BOOL isOne = Base12ListNext(base12List); + NSUInteger chunkSize = (NSUInteger)pow(2.0, base12List->count - (i + 1)); + if (isOne) { + buildPennant(FSTLLRBColorBlack, chunkSize); + } else { + buildPennant(FSTLLRBColorBlack, chunkSize); + buildPennant(FSTLLRBColorRed, chunkSize); + } + } + return root; +} + +/** + * Uses the algorithm linked here: + * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.46.1458 + */ ++ (FSTImmutableSortedDictionary *)fromDictionary:(NSDictionary *)dictionary + withComparator:(NSComparator)comparator { + // Steps: + // 0. Sort the array + // 1. Calculate the 1-2 number + // 2. Build From 1-2 number + // 0. for each digit in 1-2 number + // 0. calculate chunk size + // 1. build 1 or 2 pennants of that size + // 2. attach pennants and update node pointer + // 1. return root + NSMutableArray *sortedKeyList = [NSMutableArray arrayWithCapacity:dictionary.count]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + [sortedKeyList addObject:key]; + }]; + [sortedKeyList sortUsingComparator:comparator]; + + [sortedKeyList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + if (idx > 0) { + if (comparator(sortedKeyList[idx - 1], obj) != NSOrderedAscending) { + [NSException raise:NSInvalidArgumentException + format: + @"Can't create FSTImmutableSortedDictionary " + @"with keys with same ordering!"]; + } + } + }]; + + Base12List *list = NewBase12List((unsigned int)sortedKeyList.count); + id root = [self rootFrom12List:list keyList:sortedKeyList dictionary:dictionary]; + FreeBase12List(list); + + if (root != nil) { + return [[FSTTreeSortedDictionary alloc] initWithComparator:comparator withRoot:root]; + } else { + return [[FSTTreeSortedDictionary alloc] initWithComparator:comparator]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h new file mode 100644 index 0000000..ab82f00 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.h @@ -0,0 +1,21 @@ +#import + +#import "FSTTreeSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTTreeSortedDictionaryEnumerator : NSEnumerator + +- (id)init __attribute__(( + unavailable("Use initWithImmutableSortedDictionary:startKey:isReverse: instead."))); + +- (instancetype)initWithImmutableSortedDictionary: + (FSTTreeSortedDictionary *)aDict + startKey:(KeyType _Nullable)startKey + endKey:(KeyType _Nullable)endKey + isReverse:(BOOL)reverse NS_DESIGNATED_INITIALIZER; +- (nullable ValueType)nextObject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m new file mode 100644 index 0000000..bfdfba5 --- /dev/null +++ b/Firestore/third_party/Immutable/FSTTreeSortedDictionaryEnumerator.m @@ -0,0 +1,114 @@ +#import "FSTTreeSortedDictionaryEnumerator.h" + +NS_ASSUME_NONNULL_BEGIN + +// clang-format off +// For some reason, clang-format messes this line up... +@interface FSTTreeSortedDictionaryEnumerator () +/** The dictionary being enumerated. */ +@property(nonatomic, strong) FSTTreeSortedDictionary *immutableSortedDictionary; +/** The stack of tree nodes above the current node that will need to be revisited later. */ +@property(nonatomic, strong) NSMutableArray> *stack; +/** The direction of the traversal. YES=Descending. NO=Ascending. */ +@property(nonatomic, assign) BOOL isReverse; +/** If set, the enumerator should stop at this key and not return it. */ +@property(nonatomic, strong, nullable) id endKey; +@end +// clang-format on + +@implementation FSTTreeSortedDictionaryEnumerator + +- (instancetype)initWithImmutableSortedDictionary:(FSTTreeSortedDictionary *)aDict + startKey:(id _Nullable)startKey + endKey:(id _Nullable)endKey + isReverse:(BOOL)reverse { + self = [super init]; + if (self) { + _immutableSortedDictionary = aDict; + _stack = [[NSMutableArray alloc] init]; + _isReverse = reverse; + _endKey = endKey; + + NSComparator comparator = aDict.comparator; + id node = aDict.root; + + NSComparisonResult comparedToStart; + NSComparisonResult comparedToEnd; + while (![node isEmpty]) { + comparedToStart = NSOrderedDescending; + if (startKey) { + comparedToStart = comparator(node.key, startKey); + if (reverse) { + comparedToStart *= -1; + } + } + comparedToEnd = NSOrderedAscending; + if (endKey) { + comparedToEnd = comparator(node.key, endKey); + if (reverse) { + comparedToEnd *= -1; + } + } + + if (comparedToStart == NSOrderedAscending) { + // This node is less than our start key. Ignore it. + if (reverse) { + node = node.left; + } else { + node = node.right; + } + } else if (comparedToStart == NSOrderedSame) { + // This node is exactly equal to our start key. If it's less than the end key, push it on + // the stack, but stop iterating. + if (comparedToEnd == NSOrderedAscending) { + [_stack addObject:node]; + } + break; + } else { + // This node is greater than our start key. If it's less than our end key, add it to the + // stack and move on to the next one. + if (comparedToEnd == NSOrderedAscending) { + [_stack addObject:node]; + } + if (reverse) { + node = node.right; + } else { + node = node.left; + } + } + } + } + return self; +} + +- (nullable id)nextObject { + if ([self.stack count] == 0) { + return nil; + } + + id node = [self.stack lastObject]; + [self.stack removeLastObject]; + id result = node.key; + NSComparator comparator = self.immutableSortedDictionary.comparator; + + node = self.isReverse ? node.left : node.right; + while (![node isEmpty]) { + NSComparisonResult comparedToEnd = NSOrderedAscending; + if (self.endKey) { + comparedToEnd = comparator(node.key, self.endKey); + if (self.isReverse) { + comparedToEnd *= -1; + } + } + if (comparedToEnd == NSOrderedAscending) { + [self.stack addObject:node]; + } + node = self.isReverse ? node.right : node.left; + } + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/LICENSE b/Firestore/third_party/Immutable/LICENSE new file mode 100644 index 0000000..b437003 --- /dev/null +++ b/Firestore/third_party/Immutable/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2012 Mads Hartmann Jensen + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m b/Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m new file mode 100644 index 0000000..68f2fc3 --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m @@ -0,0 +1,467 @@ +#import "Immutable/FSTArraySortedDictionary.h" + +#import + +#import "Util/FSTAssert.h" + +@interface FSTArraySortedDictionary (Test) +// Override methods to return subtype. +- (FSTArraySortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey; +- (FSTArraySortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey; +@end + +@interface FSTArraySortedDictionaryTests : XCTestCase +@end + +@implementation FSTArraySortedDictionaryTests + +- (NSComparator)defaultComparator { + return ^(id obj1, id obj2) { + FSTAssert([obj1 respondsToSelector:@selector(compare:)] && + [obj2 respondsToSelector:@selector(compare:)], + @"Objects must support compare: %@ %@", obj1, obj2); + return [obj1 compare:obj2]; + }; +} + +- (void)testSearchForSpecificKey { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + + XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object"); + XCTAssertEqualObjects(map[@1], @1, @"Found first object"); + XCTAssertEqualObjects(map[@2], @2, @"Found second object"); + XCTAssertNil([map objectForKey:@3], @"Properly not found object"); +} + +- (void)testRemoveKeyValuePair { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + + FSTImmutableSortedDictionary *newMap = [map dictionaryByRemovingObjectForKey:@1]; + XCTAssertEqualObjects([newMap objectForKey:@2], @2, @"Found second object"); + XCTAssertNil([newMap objectForKey:@1], @"Properly not found object"); + + // Make sure the original one is not mutated + XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object"); +} + +- (void)testMoreRemovals { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + map = [map dictionaryBySettingObject:@1 forKey:@20]; + map = [map dictionaryBySettingObject:@18 forKey:@18]; + map = [map dictionaryBySettingObject:@3 forKey:@2]; + map = [map dictionaryBySettingObject:@4 forKey:@71]; + map = [map dictionaryBySettingObject:@7 forKey:@42]; + map = [map dictionaryBySettingObject:@9 forKey:@88]; + + XCTAssertNotNil([map objectForKey:@7], @"Found object"); + XCTAssertNotNil([map objectForKey:@3], @"Found object"); + XCTAssertNotNil([map objectForKey:@1], @"Found object"); + + FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@7]; + FSTImmutableSortedDictionary *m2 = [map dictionaryByRemovingObjectForKey:@3]; + FSTImmutableSortedDictionary *m3 = [map dictionaryByRemovingObjectForKey:@1]; + + XCTAssertNil([m1 objectForKey:@7], @"Removed object"); + XCTAssertNotNil([m1 objectForKey:@3], @"Found object"); + XCTAssertNotNil([m1 objectForKey:@1], @"Found object"); + + XCTAssertNil([m2 objectForKey:@3], @"Removed object"); + XCTAssertNotNil([m2 objectForKey:@7], @"Found object"); + XCTAssertNotNil([m2 objectForKey:@1], @"Found object"); + + XCTAssertNil([m3 objectForKey:@1], @"Removed object"); + XCTAssertNotNil([m3 objectForKey:@7], @"Found object"); + XCTAssertNotNil([m3 objectForKey:@3], @"Found object"); +} + +- (void)testRemovalBug { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + + XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found object"); + XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found object"); + XCTAssertEqualObjects([map objectForKey:@3], @3, @"Found object"); + + FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@2]; + XCTAssertEqualObjects([m1 objectForKey:@1], @1, @"Found object"); + XCTAssertEqualObjects([m1 objectForKey:@3], @3, @"Found object"); + XCTAssertNil([m1 objectForKey:@2], @"Removed object"); +} + +- (void)testIncreasing { + int total = 100; + + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + for (int i = 0; i < total; i++) { + NSNumber *item = @(i); + map = [map dictionaryBySettingObject:item forKey:item]; + } + + XCTAssertTrue(map.count == 100, @"Check if all 100 objects are in the map"); + + for (int i = 0; i < total; i++) { + NSNumber *item = @(i); + map = [map dictionaryByRemovingObjectForKey:item]; + } + + XCTAssertTrue(map.count == 0, @"Check if all 100 objects were removed"); +} + +- (void)testOverride { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@10 forKey:@10]; + map = [map dictionaryBySettingObject:@8 forKey:@10]; + + XCTAssertEqualObjects([map objectForKey:@10], @8, @"Found first object"); +} + +- (void)testEmpty { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@10 forKey:@10]; + map = [map dictionaryByRemovingObjectForKey:@10]; + + XCTAssertTrue([map isEmpty], @"Properly empty"); +} + +- (void)testEmptyGet { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertNil([map objectForKey:@"something"], @"Properly nil"); +} + +- (void)testEmptyCount { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertTrue([map count] == 0, @"Properly zero count"); +} + +- (void)testEmptyRemoval { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryByRemovingObjectForKey:@"something"]; + XCTAssertTrue(map.count == 0, @"Properly zero count"); +} + +- (void)testReverseTraversal { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@5 forKey:@5]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + + __block int next = 5; + [map enumerateKeysAndObjectsReverse:YES + usingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, @(next), @"Properly equal"); + next = next - 1; + }]; +} + +- (void)testInsertionAndRemovalOfAHundredItems { + NSUInteger n = 100; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n]; + + for (NSUInteger i = 0; i < n; i++) { + [toInsert addObject:@(i)]; + [toRemove addObject:@(i)]; + } + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, @(next), @"Correct key"); + XCTAssertEqualObjects(value, @(next), @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual(next, n, @"Check we traversed all of the items"); + + // remove them + + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryByRemovingObjectForKey:toRemove[i]]; + } + + XCTAssertEqual(map.count, 0, @"Check we removed all of the items"); +} + +- (void)shuffleArray:(NSMutableArray *)array { + NSUInteger count = array.count; + for (NSUInteger i = 0; i < count; i++) { + NSUInteger nElements = count - i; + NSUInteger n = (arc4random() % nElements) + i; + [array exchangeObjectAtIndex:i withObjectAtIndex:n]; + } +} + +- (void)testBalanceProblem { + NSArray *toInsert = @[ @1, @7, @8, @5, @2, @6, @4, @0, @3 ]; + + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for (NSUInteger i = 0; i < toInsert.count; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == toInsert.count, @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, @(next), @"Correct key"); + XCTAssertEqualObjects(value, @(next), @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual((int)next, (int)toInsert.count, @"Check we traversed all of the items"); +} + +- (void)testPredecessorKey { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + + XCTAssertNil([map predecessorKey:@1], @"First object doesn't have a predecessor"); + XCTAssertEqualObjects([map predecessorKey:@3], @1, @"@1"); + XCTAssertEqualObjects([map predecessorKey:@4], @3, @"@3"); + XCTAssertEqualObjects([map predecessorKey:@7], @4, @"@4"); + XCTAssertEqualObjects([map predecessorKey:@9], @7, @"@7"); + XCTAssertEqualObjects([map predecessorKey:@50], @9, @"@9"); + XCTAssertThrows([map predecessorKey:@777], @"Expect exception about nonexistent key"); +} + +// This is a macro instead of a method so that the failures show on the proper lines. +#define ASSERT_ENUMERATOR(enumerator, start, end, step) \ + do { \ + NSEnumerator *e = (enumerator); \ + id next = nil; \ + for (NSUInteger i = (start); i != (end); i += (step)) { \ + next = [e nextObject]; \ + XCTAssertNotNil(next, @"expected %lu. got nil.", (unsigned long)i); \ + XCTAssertEqualObjects(next, @(i), "expected %lu. got %@.", (unsigned long)i, next); \ + } \ + next = [e nextObject]; \ + XCTAssertNil(next, @"expected nil. got %@.", next); \ + } while (0) + +- (void)testEnumerator { + NSUInteger n = 100; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i)]; + [toRemove addObject:@(i)]; + } + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:self.defaultComparator]; + + // add them to the dictionary + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + + ASSERT_ENUMERATOR([map keyEnumerator], 0, 100, 1); +} + +- (void)testReverseEnumerator { + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i)]; + } + + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // Add them to the dictionary. + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class], + @"Make sure we still have a array backed dictionary"); + + ASSERT_ENUMERATOR([map reverseKeyEnumerator], n - 1, -1, -1); +} + +- (void)testEnumeratorFrom { + // Create a dictionary with the even numbers in [2, 42). + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i * 2 + 2)]; + } + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // Add them to the dictionary. + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class], + @"Make sure we still have a array backed dictionary"); + + // Test from before keys. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0], 2, n * 2 + 2, 2); + + // Test from after keys. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100], 0, 0, 2); + + // Test from key in map. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@10], 10, n * 2 + 2, 2); + + // Test from in between keys. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@11], 12, n * 2 + 2, 2); +} + +- (void)testEnumeratorFromTo { + // Create a dictionary with the even numbers in [2, 42). + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i * 2 + 2)]; + } + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // Add them to the dictionary. + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class], + @"Make sure we still have an array backed dictionary"); + + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@1], 2, 2, 2); // before to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@100], 2, n * 2 + 2, 2); // before to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@6], 2, 6, 2); // before to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@7], 2, 8, 2); // before to in between keys + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@0], 2, 2, 2); // after to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@110], 2, 2, 2); // after to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@6], 2, 2, 2); // after to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@7], 2, 2, 2); // after to in between + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@0], 6, 6, 2); // key in map to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@100], 6, n * 2 + 2, 2); // key in map to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@10], 6, 10, 2); // key in map to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@11], 6, 12, 2); // key in map to in between + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@0], 8, 8, 2); // in between to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@100], 8, n * 2 + 2, 2); // in between to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@10], 8, 10, 2); // in between to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@13], 8, 14, 2); // in between to in between +} + +- (void)testReverseEnumeratorFrom { + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i * 2 + 2)]; + } + + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTArraySortedDictionary.class], + @"Make sure we still have a array backed dictionary"); + + // Test from before keys. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@0], 0, 0, -2); + + // Test from after keys. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@100], n * 2, 0, -2); + + // Test from key in map. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@10], 10, 0, -2); + + // Test from in between keys. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@11], 10, 0, -2); +} + +#undef ASSERT_ENUMERATOR + +- (void)testIndexOf { + FSTArraySortedDictionary *map = + [[FSTArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + + XCTAssertEqual([map indexOfKey:@0], NSNotFound); + XCTAssertEqual([map indexOfKey:@1], 0); + XCTAssertEqual([map indexOfKey:@2], NSNotFound); + XCTAssertEqual([map indexOfKey:@3], 1); + XCTAssertEqual([map indexOfKey:@4], 2); + XCTAssertEqual([map indexOfKey:@5], NSNotFound); + XCTAssertEqual([map indexOfKey:@6], NSNotFound); + XCTAssertEqual([map indexOfKey:@7], 3); + XCTAssertEqual([map indexOfKey:@8], NSNotFound); + XCTAssertEqual([map indexOfKey:@9], 4); + XCTAssertEqual([map indexOfKey:@50], 5); +} + +@end diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h new file mode 100644 index 0000000..7496173 --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h @@ -0,0 +1,17 @@ +#import + +#import "Immutable/FSTImmutableSortedDictionary.h" + +NS_ASSUME_NONNULL_BEGIN + +// clang-format doesn't yet deal with generic parameters and categories :-( +// clang-format off +@interface FSTImmutableSortedDictionary (Testing) + +/** Converts the values of the dictionary to an array preserving order. */ +- (NSArray *)values; + +@end +// clang-format on + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m new file mode 100644 index 0000000..d402916 --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m @@ -0,0 +1,17 @@ +#import "FSTImmutableSortedDictionary+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (Testing) + +- (NSArray *)values { + NSMutableArray *result = [NSMutableArray array]; + [self enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + [result addObject:value]; + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h new file mode 100644 index 0000000..a0e25d7 --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h @@ -0,0 +1,20 @@ +#import "Immutable/FSTImmutableSortedSet.h" + +NS_ASSUME_NONNULL_BEGIN + +// clang-format doesn't yet deal with generic parameters and categories :-( +// clang-format off +@interface FSTImmutableSortedSet (Testing) + +/** + * An array containing the set’s members, or an empty array if the set has no members. + * + * Implemented here for compatibility with NSSet in testing though we'd never want to do this + * in production code. + */ +- (NSArray *)allObjects; + +@end +// clang-format on + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m new file mode 100644 index 0000000..d4d81e0 --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m @@ -0,0 +1,17 @@ +#import "FSTImmutableSortedSet+Testing.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedSet (Testing) + +- (NSArray *)allObjects { + NSMutableArray *result = [NSMutableArray array]; + [self enumerateObjectsUsingBlock:^(id object, BOOL *stop) { + [result addObject:object]; + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h b/Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h new file mode 100644 index 0000000..3b05647 --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTLLRBValueNode+Test.h @@ -0,0 +1,10 @@ +#import "Immutable/FSTLLRBValueNode.h" + +#import + +/** Extra methods exposed only for testing. */ +@interface FSTLLRBValueNode (Test) +- (id)rotateLeft; +- (id)rotateRight; +- (BOOL)checkMaxDepth; +@end diff --git a/Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m b/Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m new file mode 100644 index 0000000..352ac4e --- /dev/null +++ b/Firestore/third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m @@ -0,0 +1,655 @@ +#import + +#import "Immutable/FSTLLRBEmptyNode.h" +#import "Immutable/FSTLLRBNode.h" +#import "Immutable/FSTLLRBValueNode.h" +#import "Immutable/FSTTreeSortedDictionary.h" +#import "Util/FSTAssert.h" + +#import "FSTLLRBValueNode+Test.h" + +@interface FSTTreeSortedDictionary (Test) +// Override methods to return subtype. +- (FSTTreeSortedDictionary *)dictionaryBySettingObject:(id)aValue forKey:(id)aKey; +- (FSTTreeSortedDictionary *)dictionaryByRemovingObjectForKey:(id)aKey; +@end + +@interface FSTTreeSortedDictionaryTests : XCTestCase +@end + +@implementation FSTTreeSortedDictionaryTests + +- (NSComparator)defaultComparator { + return ^(id obj1, id obj2) { + FSTAssert([obj1 respondsToSelector:@selector(compare:)] && + [obj2 respondsToSelector:@selector(compare:)], + @"Objects must support compare: %@ %@", obj1, obj2); + return [obj1 compare:obj2]; + }; +} + +- (void)testCreateNode { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@"value" forKey:@"key"]; + XCTAssertTrue([map.root.left isEmpty], @"Left child is properly empty"); + XCTAssertTrue([map.root.right isEmpty], @"Right child is properly empty"); +} + +- (void)testSearchForSpecificKey { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + + XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object"); + XCTAssertEqualObjects(map[@1], @1, @"Found first object"); + XCTAssertEqualObjects(map[@2], @2, @"Found second object"); + XCTAssertNil([map objectForKey:@3], @"Properly not found object"); +} + +- (void)testInsertNewKeyValuePair { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + + XCTAssertEqualObjects(map.root.key, @2, @"Check the root key"); + XCTAssertEqualObjects(map.root.left.key, @1, @"Check the root.left key"); +} + +- (void)testRemoveKeyValuePair { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + + FSTImmutableSortedDictionary *newMap = [map dictionaryByRemovingObjectForKey:@1]; + XCTAssertEqualObjects([newMap objectForKey:@2], @2, @"Found second object"); + XCTAssertNil([newMap objectForKey:@1], @"Properly not found object"); + + // Make sure the original one is not mutated + XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found second object"); +} + +- (void)testMoreRemovals { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + map = [map dictionaryBySettingObject:@1 forKey:@20]; + map = [map dictionaryBySettingObject:@18 forKey:@18]; + map = [map dictionaryBySettingObject:@3 forKey:@2]; + map = [map dictionaryBySettingObject:@4 forKey:@71]; + map = [map dictionaryBySettingObject:@7 forKey:@42]; + map = [map dictionaryBySettingObject:@9 forKey:@88]; + + XCTAssertNotNil([map objectForKey:@7], @"Found object"); + XCTAssertNotNil([map objectForKey:@3], @"Found object"); + XCTAssertNotNil([map objectForKey:@1], @"Found object"); + + FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@7]; + FSTImmutableSortedDictionary *m2 = [map dictionaryByRemovingObjectForKey:@3]; + FSTImmutableSortedDictionary *m3 = [map dictionaryByRemovingObjectForKey:@1]; + + XCTAssertNil([m1 objectForKey:@7], @"Removed object"); + XCTAssertNotNil([m1 objectForKey:@3], @"Found object"); + XCTAssertNotNil([m1 objectForKey:@1], @"Found object"); + + XCTAssertNil([m2 objectForKey:@3], @"Removed object"); + XCTAssertNotNil([m2 objectForKey:@7], @"Found object"); + XCTAssertNotNil([m2 objectForKey:@1], @"Found object"); + + XCTAssertNil([m3 objectForKey:@1], @"Removed object"); + XCTAssertNotNil([m3 objectForKey:@7], @"Found object"); + XCTAssertNotNil([m3 objectForKey:@3], @"Found object"); +} + +- (void)testRemovalBug { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + + XCTAssertEqualObjects([map objectForKey:@1], @1, @"Found object"); + XCTAssertEqualObjects([map objectForKey:@2], @2, @"Found object"); + XCTAssertEqualObjects([map objectForKey:@3], @3, @"Found object"); + + FSTImmutableSortedDictionary *m1 = [map dictionaryByRemovingObjectForKey:@2]; + XCTAssertEqualObjects([m1 objectForKey:@1], @1, @"Found object"); + XCTAssertEqualObjects([m1 objectForKey:@3], @3, @"Found object"); + XCTAssertNil([m1 objectForKey:@2], @"Removed object"); +} + +- (void)testIncreasing { + int total = 100; + + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + for (int i = 0; i < total; i++) { + NSNumber *item = @(i); + map = [map dictionaryBySettingObject:item forKey:item]; + } + + XCTAssertTrue(map.count == 100, @"Check if all 100 objects are in the map"); + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + + for (int i = 0; i < total; i++) { + NSNumber *item = @(i); + map = [map dictionaryByRemovingObjectForKey:item]; + } + + XCTAssertTrue(map.count == 0, @"Check if all 100 objects were removed"); + // We can't check the depth here because the map no longer contains values, so we check that it + // doesn't respond to this check + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBEmptyNode.class], @"Root is an empty node"); + XCTAssertFalse([map respondsToSelector:@selector(checkMaxDepth)], + @"The empty node doesn't respond to this selector."); +} + +- (void)testStructureShouldBeValidAfterInsertionA { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + + XCTAssertEqualObjects(map.root.key, @2, @"Check root key"); + XCTAssertEqualObjects(map.root.left.key, @1, @"Check the left key is correct"); + XCTAssertEqualObjects(map.root.right.key, @3, @"Check the right key is correct"); +} + +- (void)testStructureShouldBeValidAfterInsertionB { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + map = [map dictionaryBySettingObject:@1 forKey:@20]; + map = [map dictionaryBySettingObject:@18 forKey:@18]; + map = [map dictionaryBySettingObject:@3 forKey:@2]; + map = [map dictionaryBySettingObject:@4 forKey:@71]; + map = [map dictionaryBySettingObject:@7 forKey:@42]; + map = [map dictionaryBySettingObject:@9 forKey:@88]; + + XCTAssertTrue(map.count == 12, @"Check if all 12 objects are in the map"); + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); +} + +- (void)testRotateLeftLeavesTreeInAValidState { + FSTLLRBValueNode *node = [[FSTLLRBValueNode alloc] + initWithKey:@4 + withValue:@4 + withColor:FSTLLRBColorBlack + withLeft:[[FSTLLRBValueNode alloc] initWithKey:@2 + withValue:@2 + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil] + withRight:[[FSTLLRBValueNode alloc] + initWithKey:@7 + withValue:@7 + withColor:FSTLLRBColorRed + withLeft:[[FSTLLRBValueNode alloc] initWithKey:@5 + withValue:@5 + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil] + withRight:[[FSTLLRBValueNode alloc] initWithKey:@8 + withValue:@8 + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil]]]; + + FSTLLRBValueNode *node2 = [node rotateLeft]; + + XCTAssertTrue(node2.count == 5, @"Make sure the count is correct"); + XCTAssertTrue([node2 checkMaxDepth], @"Check proper structure"); +} + +- (void)testRotateRightLeavesTreeInAValidState { + FSTLLRBValueNode *node = [[FSTLLRBValueNode alloc] + initWithKey:@7 + withValue:@7 + withColor:FSTLLRBColorBlack + withLeft:[[FSTLLRBValueNode alloc] + initWithKey:@4 + withValue:@4 + withColor:FSTLLRBColorRed + withLeft:[[FSTLLRBValueNode alloc] initWithKey:@2 + withValue:@2 + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil] + withRight:[[FSTLLRBValueNode alloc] initWithKey:@5 + withValue:@5 + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil]] + withRight:[[FSTLLRBValueNode alloc] initWithKey:@8 + withValue:@8 + withColor:FSTLLRBColorBlack + withLeft:nil + withRight:nil]]; + + FSTLLRBValueNode *node2 = [node rotateRight]; + XCTAssertTrue(node2.count == 5, @"Make sure the count is correct"); + XCTAssertEqualObjects(node2.key, @4, @"Check roots key"); + XCTAssertEqualObjects(node2.left.key, @2, @"Check first left child key"); + XCTAssertEqualObjects(node2.right.key, @7, @"Check first right child key"); + XCTAssertEqualObjects(node2.right.left.key, @5, @"Check second right left key"); + XCTAssertEqualObjects(node2.right.right.key, @8, @"Check second right left key"); +} + +- (void)testStructureShouldBeValidAfterInsertionC { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + + XCTAssertTrue(map.count == 6, @"Check if all 6 objects are in the map"); + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + + FSTTreeSortedDictionary *m2 = map; + m2 = [m2 dictionaryBySettingObject:@20 forKey:@20]; + m2 = [m2 dictionaryBySettingObject:@18 forKey:@18]; + m2 = [m2 dictionaryBySettingObject:@2 forKey:@2]; + + XCTAssertTrue(m2.count == 9, @"Check if all 9 objects are in the map"); + XCTAssertTrue([m2.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)m2.root checkMaxDepth], + @"Checking valid depth and tree structure"); + + FSTTreeSortedDictionary *m3 = m2; + m3 = [m3 dictionaryBySettingObject:@71 forKey:@71]; + m3 = [m3 dictionaryBySettingObject:@42 forKey:@42]; + m3 = [m3 dictionaryBySettingObject:@88 forKey:@88]; + m3 = [m3 dictionaryBySettingObject:@20 forKey:@20]; // Add a dupe to see if the size is correct + + XCTAssertTrue(m3.count == 12, @"Check if all 12 (minus dupe @20) objects are in the map"); + XCTAssertTrue([m3.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)m3.root checkMaxDepth], + @"Checking valid depth and tree structure"); +} + +- (void)testOverride { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@10 forKey:@10]; + map = [map dictionaryBySettingObject:@8 forKey:@10]; + + XCTAssertEqualObjects([map objectForKey:@10], @8, @"Found first object"); +} +- (void)testEmpty { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@10 forKey:@10]; + map = [map dictionaryByRemovingObjectForKey:@10]; + + XCTAssertTrue([map isEmpty], @"Properly empty"); +} + +- (void)testEmptyGet { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertNil([map objectForKey:@"something"], @"Properly nil"); +} + +- (void)testEmptyCount { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertTrue([map count] == 0, @"Properly zero count"); +} + +- (void)testEmptyRemoval { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryByRemovingObjectForKey:@"something"]; + XCTAssertTrue(map.count == 0, @"Properly zero count"); +} + +- (void)testReverseTraversal { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@5 forKey:@5]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@2 forKey:@2]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + + __block int next = 5; + [map enumerateKeysAndObjectsReverse:YES + usingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, @(next), @"Properly equal"); + next = next - 1; + }]; +} + +- (void)testInsertionAndRemovalOfAHundredItems { + NSUInteger n = 100; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i)]; + [toRemove addObject:@(i)]; + } + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, @(next), @"Correct key"); + XCTAssertEqualObjects(value, @(next), @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual(next, n, @"Check we traversed all of the items"); + + // remove them + + for (NSUInteger i = 0; i < n; i++) { + if ([map.root isMemberOfClass:FSTLLRBValueNode.class]) { + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + } + map = [map dictionaryByRemovingObjectForKey:toRemove[i]]; + } + + XCTAssertEqual(map.count, 0, @"Check we removed all of the items"); +} + +- (void)shuffleArray:(NSMutableArray *)array { + NSUInteger count = array.count; + for (NSUInteger i = 0; i < count; i++) { + NSUInteger nElements = count - i; + NSUInteger n = (arc4random() % nElements) + i; + [array exchangeObjectAtIndex:i withObjectAtIndex:n]; + } +} + +- (void)testBalanceProblem { + NSArray *toInsert = @[ @1, @7, @8, @5, @2, @6, @4, @0, @3 ]; + + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for (NSUInteger i = 0; i < toInsert.count; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + } + XCTAssertTrue(map.count == toInsert.count, @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, @(next), @"Correct key"); + XCTAssertEqualObjects(value, @(next), @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual((int)next, (int)toInsert.count, @"Check we traversed all of the items"); + + // removing one triggers the balance problem + map = [map dictionaryByRemovingObjectForKey:@5]; + + if ([map.root isMemberOfClass:FSTLLRBValueNode.class]) { + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + } +} + +- (void)testPredecessorKey { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + + XCTAssertNil([map predecessorKey:@1], @"First object doesn't have a predecessor"); + XCTAssertEqualObjects([map predecessorKey:@3], @1, @"@1"); + XCTAssertEqualObjects([map predecessorKey:@4], @3, @"@3"); + XCTAssertEqualObjects([map predecessorKey:@7], @4, @"@4"); + XCTAssertEqualObjects([map predecessorKey:@9], @7, @"@7"); + XCTAssertEqualObjects([map predecessorKey:@50], @9, @"@9"); + XCTAssertThrows([map predecessorKey:@777], @"Expect exception about nonexistent key"); +} + +// This is a macro instead of a method so that the failures show on the proper lines. +#define ASSERT_ENUMERATOR(enumerator, start, end, step) \ + do { \ + NSEnumerator *e = (enumerator); \ + id next = nil; \ + for (NSUInteger i = (start); i != (end); i += (step)) { \ + next = [e nextObject]; \ + XCTAssertNotNil(next, @"expected %lu. got nil.", (unsigned long)i); \ + XCTAssertEqualObjects(next, @(i), "expected %lu. got %@.", (unsigned long)i, next); \ + } \ + next = [e nextObject]; \ + XCTAssertNil(next, @"expected nil. got %@.", next); \ + } while (0) + +- (void)testEnumerator { + NSUInteger n = 100; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i)]; + [toRemove addObject:@(i)]; + } + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:self.defaultComparator]; + + // add them to the dictionary + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + XCTAssertTrue([map.root isMemberOfClass:FSTLLRBValueNode.class], @"Root is a value node"); + XCTAssertTrue([(FSTLLRBValueNode *)map.root checkMaxDepth], + @"Checking valid depth and tree structure"); + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + + ASSERT_ENUMERATOR([map keyEnumerator], 0, 100, 1); +} + +- (void)testReverseEnumerator { + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i)]; + } + + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // Add them to the dictionary. + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class], + @"Make sure we still have a tree backed dictionary"); + + ASSERT_ENUMERATOR([map reverseKeyEnumerator], n - 1, -1, -1); +} + +- (void)testEnumeratorFrom { + // Create a dictionary with the even numbers in [2, 42). + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i * 2 + 2)]; + } + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // Add them to the dictionary. + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class], + @"Make sure we still have a tree backed dictionary"); + + // Test from before keys. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0], 2, n * 2 + 2, 2); + + // Test from after keys. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100], 0, 0, 2); + + // Test from key in map. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@10], 10, n * 2 + 2, 2); + + // Test from in between keys. + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@11], 12, n * 2 + 2, 2); +} + +- (void)testEnumeratorFromTo { + // Create a dictionary with the even numbers in [2, 42). + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i * 2 + 2)]; + } + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // Add them to the dictionary. + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class], + @"Make sure we still have a tree backed dictionary"); + + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@1], 2, 2, 2); // before to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@100], 2, n * 2 + 2, 2); // before to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@6], 2, 6, 2); // before to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@0 to:@7], 2, 8, 2); // before to in between keys + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@0], 2, 2, 2); // after to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@110], 2, 2, 2); // after to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@6], 2, 2, 2); // after to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@100 to:@7], 2, 2, 2); // after to in between + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@0], 6, 6, 2); // key in map to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@100], 6, n * 2 + 2, 2); // key in map to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@10], 6, 10, 2); // key in map to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@6 to:@11], 6, 12, 2); // key in map to in between + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@0], 8, 8, 2); // in between to before + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@100], 8, n * 2 + 2, 2); // in between to after + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@10], 8, 10, 2); // in between to key in map + ASSERT_ENUMERATOR([map keyEnumeratorFrom:@7 to:@13], 8, 14, 2); // in between to in between +} + +- (void)testReverseEnumeratorFrom { + NSUInteger n = 20; + NSMutableArray *toInsert = [NSMutableArray arrayWithCapacity:n]; + + for (int i = 0; i < n; i++) { + [toInsert addObject:@(i * 2 + 2)]; + } + + [self shuffleArray:toInsert]; + + FSTImmutableSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for (NSUInteger i = 0; i < n; i++) { + map = [map dictionaryBySettingObject:toInsert[i] forKey:toInsert[i]]; + } + XCTAssertTrue(map.count == n, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:FSTTreeSortedDictionary.class], + @"Make sure we still have a tree backed dictionary"); + + // Test from before keys. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@0], 0, 0, -2); + + // Test from after keys. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@100], n * 2, 0, -2); + + // Test from key in map. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@10], 10, 0, -2); + + // Test from in between keys. + ASSERT_ENUMERATOR([map reverseKeyEnumeratorFrom:@11], 10, 0, -2); +} + +#undef ASSERT_ENUMERATOR + +- (void)testIndexOf { + FSTTreeSortedDictionary *map = + [[FSTTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + map = [map dictionaryBySettingObject:@1 forKey:@1]; + map = [map dictionaryBySettingObject:@50 forKey:@50]; + map = [map dictionaryBySettingObject:@3 forKey:@3]; + map = [map dictionaryBySettingObject:@4 forKey:@4]; + map = [map dictionaryBySettingObject:@7 forKey:@7]; + map = [map dictionaryBySettingObject:@9 forKey:@9]; + + XCTAssertEqual([map indexOfKey:@0], NSNotFound); + XCTAssertEqual([map indexOfKey:@1], 0); + XCTAssertEqual([map indexOfKey:@2], NSNotFound); + XCTAssertEqual([map indexOfKey:@3], 1); + XCTAssertEqual([map indexOfKey:@4], 2); + XCTAssertEqual([map indexOfKey:@5], NSNotFound); + XCTAssertEqual([map indexOfKey:@6], NSNotFound); + XCTAssertEqual([map indexOfKey:@7], 3); + XCTAssertEqual([map indexOfKey:@8], NSNotFound); + XCTAssertEqual([map indexOfKey:@9], 4); + XCTAssertEqual([map indexOfKey:@50], 5); +} + +@end diff --git a/README.md b/README.md index 780a73f..fbc7fa8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Firebase iOS Open Source Development [![Build Status](https://travis-ci.org/firebase/firebase-ios-sdk.svg?branch=master)](https://travis-ci.org/firebase/firebase-ios-sdk) This repository contains a subset of the Firebase iOS SDK source. It currently -includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseMessaging, and -FirebaseStorage. +includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseMessaging, +FirebaseStorage, and Firestore. Firebase is an app development platform with tools to help you build, grow and monetize your app. More information about Firebase can be found at @@ -18,6 +18,10 @@ unit tests, integration tests, and reference samples. Note, however, that the resulting FirebaseCommunity pod is NOT interoperable with the official Firebase release pods because of different pod dependency definitions. +Firestore has not yet been integrated with FirebaseCommunity. In the +meantime, it has a self contained Xcode project. See +[Firestore/README.md](Firestore/README.md). + Instructions and a script to build replaceable static library frameworks at [BuildFrameworks](BuildFrameworks). The resulting frameworks can be used to replace frameworks delivered by CocoaPods or diff --git a/scripts/style.sh b/scripts/style.sh index 1948188..3c2ac10 100755 --- a/scripts/style.sh +++ b/scripts/style.sh @@ -22,4 +22,5 @@ find . \ -name 'Pods' -prune -o \ -name '*.[mh]' \ -not -name '*.pbobjc.*' \ + -not -name '*.pbrpc.*' \ -print0 | xargs -0 clang-format -style=file -i diff --git a/test.sh b/test.sh index 370a6b7..627fbad 100755 --- a/test.sh +++ b/test.sh @@ -52,4 +52,7 @@ if [ $RESULT == 65 ]; then test_macOS; RESULT=$? fi -exit $RESULT +if [ $RESULT != 0 ]; then exit $RESULT; fi + +# Also test Firestore +Firestore/test.sh -- cgit v1.2.3